# -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase from hypothesis import given, strategies as st, settings class TestNonReceiptOperationBehavior(TransactionCase): """ Property-based tests for non-receipt operation standard behavior. Feature: quality-check-lot-preserve, Property 6: Non-receipt operation standard behavior Property: For any quality check associated with non-receipt operations (manufacturing, internal, outgoing), the standard Odoo behavior (state reset on lot assignment) should occur. Validates: Requirements 4.2 """ def setUp(self): super(TestNonReceiptOperationBehavior, self).setUp() # Get required models self.StockMoveLine = self.env['stock.move.line'] self.QualityCheck = self.env['quality.check'] self.StockPicking = self.env['stock.picking'] self.StockMove = self.env['stock.move'] self.ProductProduct = self.env['product.product'] self.StockLocation = self.env['stock.location'] self.StockPickingType = self.env['stock.picking.type'] self.StockLot = self.env['stock.lot'] self.QualityPoint = self.env['quality.point'] # Get or create locations self.stock_location = self.env.ref('stock.stock_location_stock') self.customer_location = self.env.ref('stock.stock_location_customers') # Get or create different picking types for non-receipt operations self.internal_picking_type = self._get_or_create_picking_type('internal', 'Internal Transfers') self.outgoing_picking_type = self._get_or_create_picking_type('outgoing', 'Delivery Orders') def _get_or_create_picking_type(self, code, name): """Helper to get or create a picking type""" picking_type = self.env['stock.picking.type'].search([ ('code', '=', code) ], limit=1) if not picking_type: warehouse = self.env['stock.warehouse'].search([], limit=1) if not warehouse: # Create a minimal warehouse if none exists warehouse = self.env['stock.warehouse'].create({ 'name': 'Test Warehouse', 'code': 'TEST', }) picking_type = self.env['stock.picking.type'].create({ 'name': name, 'code': code, 'sequence_code': code.upper()[:5], 'warehouse_id': warehouse.id, }) return picking_type def _create_product_with_tracking(self, name): """Helper to create a product with lot tracking enabled""" return self.ProductProduct.create({ 'name': name, 'type': 'consu', # 'consu' for consumable/storable product in Odoo 18 'tracking': 'lot', }) def _create_picking(self, picking_type, product, location_id, location_dest_id): """Helper to create a picking with a move line for the given product""" picking = self.StockPicking.create({ 'picking_type_id': picking_type.id, 'location_id': location_id.id, 'location_dest_id': location_dest_id.id, }) move = self.StockMove.create({ 'name': product.name, 'product_id': product.id, 'product_uom_qty': 10.0, 'product_uom': product.uom_id.id, 'picking_id': picking.id, 'location_id': location_id.id, 'location_dest_id': location_dest_id.id, }) # Confirm the picking to create move lines picking.action_confirm() return picking, move def _create_lot(self, product, lot_name): """Helper to create a lot for a product""" return self.StockLot.create({ 'name': lot_name, 'product_id': product.id, 'company_id': self.env.company.id, }) def _create_quality_check_for_move_line(self, move_line, picking_type, state='none'): """Helper to create a quality check linked to a move line""" # Get or create a quality team quality_team = self.env['quality.alert.team'].search([], limit=1) if not quality_team: quality_team = self.env['quality.alert.team'].create({ 'name': 'Test Quality Team', }) # Create a quality point for the product if needed quality_point = self.QualityPoint.search([ ('product_ids', 'in', [move_line.product_id.id]), ('picking_type_ids', 'in', [picking_type.id]), ], limit=1) if not quality_point: # Get a test type - use passfail which should always exist test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False) if not test_type: # If no test type exists, create a minimal one test_type = self.env['quality.point.test_type'].create({ 'name': 'Pass/Fail Test', 'technical_name': 'passfail', }) quality_point = self.QualityPoint.create({ 'title': f'Quality Check for {move_line.product_id.name}', 'product_ids': [(4, move_line.product_id.id)], 'picking_type_ids': [(4, picking_type.id)], 'test_type_id': test_type.id, 'team_id': quality_team.id, }) return self.QualityCheck.create({ 'product_id': move_line.product_id.id, 'picking_id': move_line.picking_id.id, 'move_line_id': move_line.id, 'quality_state': state, 'point_id': quality_point.id, 'team_id': quality_team.id, }) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), operation_type=st.sampled_from(['internal', 'outgoing']), ) def test_property_non_receipt_operation_not_identified_as_receipt(self, lot_name_seed, operation_type): """ Property: For any quality check associated with non-receipt operations, the system should correctly identify it as NOT a receipt operation. This test verifies Requirement 4.2: - When a quality check is associated with non-receipt operations (manufacturing, internal transfers, delivery), the system should follow standard Odoo quality check behavior Test strategy: 1. Create a non-receipt operation (internal or outgoing) 2. Create a quality check for that operation 3. Verify that: a) The quality check is NOT identified as a receipt operation b) The preservation behavior is NOT activated (_should_preserve_state returns False) """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Select the appropriate picking type based on operation_type if operation_type == 'internal': picking_type = self.internal_picking_type location_id = self.stock_location location_dest_id = self.stock_location # Internal transfer within same location else: # outgoing picking_type = self.outgoing_picking_type location_id = self.stock_location location_dest_id = self.customer_location # Create non-receipt picking picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) # Verify this is NOT a receipt operation self.assertNotEqual( picking.picking_type_id.code, 'incoming', f"Picking should NOT be a receipt operation (code is '{picking.picking_type_id.code}')" ) # Get the move line move_line = picking.move_line_ids[0] # Verify the move line is NOT identified as a receipt operation self.assertFalse( move_line._is_receipt_operation(), f"Move line should NOT be identified as a receipt operation for {operation_type} operations" ) # Create quality check quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') # PROPERTY VERIFICATION PART 1: Non-receipt operation detection # The quality check should NOT be identified as a receipt operation self.assertFalse( quality_check._is_receipt_operation(), f"Quality check should NOT be identified as a receipt operation for {operation_type} operations" ) # PROPERTY VERIFICATION PART 2: Preservation behavior NOT activated # The quality check should indicate that state should NOT be preserved self.assertFalse( quality_check._should_preserve_state(), f"Quality check should indicate that state preservation is NOT active for {operation_type} operations" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), operation_type=st.sampled_from(['internal', 'outgoing']), ) def test_property_non_receipt_operation_no_automatic_lot_propagation(self, lot_name_seed, operation_type): """ Property: For any non-receipt operation, when a lot is assigned to a stock move line, the lot should NOT be automatically propagated to quality checks by our module. This verifies that our custom lot propagation logic only applies to receipt operations. Validates: Requirement 4.2 (standard behavior for non-receipt operations) """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Select the appropriate picking type based on operation_type if operation_type == 'internal': picking_type = self.internal_picking_type location_id = self.stock_location location_dest_id = self.stock_location else: # outgoing picking_type = self.outgoing_picking_type location_id = self.stock_location location_dest_id = self.customer_location # Create non-receipt picking picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) # Get the move line move_line = picking.move_line_ids[0] # Create quality check WITHOUT a lot assigned quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') # Verify no lot is assigned initially self.assertFalse( quality_check.lot_id, "Quality check should not have a lot assigned initially" ) # Create and assign a lot to the move line lot = self._create_lot(product, f'LOT-{lot_name_seed}') move_line.write({'lot_id': lot.id}) # Refresh quality check from database quality_check.invalidate_recordset() # PROPERTY VERIFICATION: Our custom lot propagation should NOT occur for non-receipt operations # The quality check's lot_id should remain unset (or be set by standard Odoo behavior, not our module) # We verify that our module's _update_quality_check_lot method was NOT triggered # by checking that the quality check is correctly identified as non-receipt self.assertFalse( quality_check._is_receipt_operation(), f"Quality check should be identified as non-receipt operation for {operation_type}" ) # Our module should not have propagated the lot for non-receipt operations # Note: Standard Odoo may or may not propagate the lot depending on its own logic, # but our module should not interfere with non-receipt operations @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), operation_type=st.sampled_from(['internal', 'outgoing']), ) def test_property_non_receipt_operation_standard_behavior_preserved(self, lot_name_seed, operation_type): """ Property: For any non-receipt operation, the module should not interfere with standard Odoo quality check behavior. This test verifies that our module's overrides correctly delegate to standard behavior for non-receipt operations. Validates: Requirement 4.2 """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Select the appropriate picking type based on operation_type if operation_type == 'internal': picking_type = self.internal_picking_type location_id = self.stock_location location_dest_id = self.stock_location else: # outgoing picking_type = self.outgoing_picking_type location_id = self.stock_location location_dest_id = self.customer_location # Create non-receipt picking picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) # Get the move line move_line = picking.move_line_ids[0] # Create quality check quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') # PROPERTY VERIFICATION: The module should correctly identify this as non-receipt # and delegate to standard behavior self.assertFalse( quality_check._is_receipt_operation(), f"Quality check should be identified as non-receipt for {operation_type}" ) # Verify that _should_preserve_state returns False for non-receipt operations self.assertFalse( quality_check._should_preserve_state(), f"State preservation should NOT be active for {operation_type} operations" ) # Verify that _update_lot_from_lot_line delegates to parent for non-receipt operations # This method should return the result of super() for non-receipt operations # We can't easily test the return value, but we can verify the operation type detection works self.assertEqual( picking.picking_type_id.code, operation_type, f"Picking type code should be '{operation_type}'" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), ) def test_property_non_receipt_internal_operation_detection(self, lot_name_seed): """ Property: For any internal transfer operation, the system should correctly identify it as a non-receipt operation and not apply lot preservation behavior. Validates: Requirement 4.2 (internal transfers use standard behavior) """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Create internal transfer picking, move = self._create_picking( self.internal_picking_type, product, self.stock_location, self.stock_location ) # Verify this is an internal operation self.assertEqual( picking.picking_type_id.code, 'internal', "Picking should be an internal transfer" ) # Get the move line move_line = picking.move_line_ids[0] # Create quality check quality_check = self._create_quality_check_for_move_line( move_line, self.internal_picking_type, state='pass' ) # PROPERTY VERIFICATION: Internal operations should not trigger preservation behavior self.assertFalse( quality_check._is_receipt_operation(), "Internal transfer should NOT be identified as receipt operation" ) self.assertFalse( quality_check._should_preserve_state(), "State preservation should NOT be active for internal transfers" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), ) def test_property_non_receipt_outgoing_operation_detection(self, lot_name_seed): """ Property: For any outgoing/delivery operation, the system should correctly identify it as a non-receipt operation and not apply lot preservation behavior. Validates: Requirement 4.2 (delivery operations use standard behavior) """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Create outgoing/delivery operation picking, move = self._create_picking( self.outgoing_picking_type, product, self.stock_location, self.customer_location ) # Verify this is an outgoing operation self.assertEqual( picking.picking_type_id.code, 'outgoing', "Picking should be an outgoing/delivery operation" ) # Get the move line move_line = picking.move_line_ids[0] # Create quality check quality_check = self._create_quality_check_for_move_line( move_line, self.outgoing_picking_type, state='pass' ) # PROPERTY VERIFICATION: Outgoing operations should not trigger preservation behavior self.assertFalse( quality_check._is_receipt_operation(), "Outgoing/delivery operation should NOT be identified as receipt operation" ) self.assertFalse( quality_check._should_preserve_state(), "State preservation should NOT be active for outgoing/delivery operations" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), operation_type=st.sampled_from(['internal', 'outgoing']), ) def test_property_non_receipt_operation_type_consistency(self, lot_name_seed, operation_type): """ Property: For any non-receipt operation, the operation type detection should be consistent across multiple checks and throughout the quality check lifecycle. Validates: Requirement 4.2 """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Select the appropriate picking type if operation_type == 'internal': picking_type = self.internal_picking_type location_id = self.stock_location location_dest_id = self.stock_location else: # outgoing picking_type = self.outgoing_picking_type location_id = self.stock_location location_dest_id = self.customer_location # Create non-receipt picking picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) # Get the move line move_line = picking.move_line_ids[0] # Create quality check quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') # PROPERTY VERIFICATION: Operation type detection should be consistent # Check 1: Initial detection is_receipt_1 = quality_check._is_receipt_operation() self.assertFalse(is_receipt_1, f"Should not be receipt operation (check 1) for {operation_type}") # Check 2: After invalidating cache quality_check.invalidate_recordset() is_receipt_2 = quality_check._is_receipt_operation() self.assertFalse(is_receipt_2, f"Should not be receipt operation (check 2) for {operation_type}") # Check 3: Consistency between checks self.assertEqual( is_receipt_1, is_receipt_2, "Operation type detection should be consistent across multiple checks" ) # Check 4: _should_preserve_state should also be consistent should_preserve_1 = quality_check._should_preserve_state() quality_check.invalidate_recordset() should_preserve_2 = quality_check._should_preserve_state() self.assertFalse(should_preserve_1, "Should not preserve state for non-receipt operations") self.assertFalse(should_preserve_2, "Should not preserve state for non-receipt operations") self.assertEqual( should_preserve_1, should_preserve_2, "State preservation detection should be consistent" )