# -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase from hypothesis import given, strategies as st, settings, assume class TestReceiptOperationBehavior(TransactionCase): """ Property-based tests for receipt operation behavior activation. Feature: quality-check-lot-preserve, Property 5: Receipt operation behavior activation Property: For any quality check associated with a receipt operation (picking type code = 'incoming'), the lot preservation behavior should be applied when lot numbers are assigned. Validates: Requirements 4.1 """ def setUp(self): super(TestReceiptOperationBehavior, 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.supplier_location = self.env.ref('stock.stock_location_suppliers') self.stock_location = self.env.ref('stock.stock_location_stock') # Get or create receipt picking type self.receipt_picking_type = self.env['stock.picking.type'].search([ ('code', '=', 'incoming') ], limit=1) if not self.receipt_picking_type: self.receipt_picking_type = self.env['stock.picking.type'].create({ 'name': 'Receipts', 'code': 'incoming', 'sequence_code': 'IN', 'warehouse_id': self.env['stock.warehouse'].search([], limit=1).id, }) 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_receipt_picking(self, product): """Helper to create a receipt picking with a move line for the given product""" picking = self.StockPicking.create({ 'picking_type_id': self.receipt_picking_type.id, 'location_id': self.supplier_location.id, 'location_dest_id': self.stock_location.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': self.supplier_location.id, 'location_dest_id': self.stock_location.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, 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', [self.receipt_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, self.receipt_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), quality_state=st.sampled_from(['none', 'pass', 'fail']), ) def test_property_receipt_operation_activates_preservation(self, lot_name_seed, quality_state): """ Property: For any quality check associated with a receipt operation (picking type code = 'incoming'), the lot preservation behavior should be applied when lot numbers are assigned. This test verifies Requirement 4.1: - When a quality check is associated with a receipt operation (incoming shipment), the system applies the lot preservation behavior Test strategy: 1. Create a receipt operation (picking_type_code = 'incoming') 2. Create a quality check with a specific state 3. Assign a lot number to the stock move line 4. Verify that: a) The quality check is correctly identified as a receipt operation b) The lot preservation behavior is activated (_should_preserve_state returns True) c) The quality check state is preserved (demonstrating the behavior is active) d) The lot number is propagated to the quality check """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Create receipt picking (this is the key: picking_type_code = 'incoming') picking, move = self._create_receipt_picking(product) # Verify this is indeed a receipt operation self.assertEqual( picking.picking_type_id.code, 'incoming', "Picking should be a receipt operation (incoming)" ) # Get the move line move_line = picking.move_line_ids[0] # Verify the move line is identified as a receipt operation self.assertTrue( move_line._is_receipt_operation(), "Move line should be identified as a receipt operation" ) # Create quality check with the specified state quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) # PROPERTY VERIFICATION PART 1: Receipt operation detection # The quality check should be correctly identified as a receipt operation self.assertTrue( quality_check._is_receipt_operation(), "Quality check should be identified as a receipt operation" ) # PROPERTY VERIFICATION PART 2: Preservation behavior activation # The quality check should indicate that state should be preserved self.assertTrue( quality_check._should_preserve_state(), "Quality check should indicate that state preservation is active for receipt operations" ) # Record the initial quality check state initial_state = quality_check.quality_state # Create and assign a lot number 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 PART 3: State preservation (demonstrating behavior is active) # The quality check state should remain unchanged, proving the preservation behavior is active self.assertEqual( quality_check.quality_state, initial_state, f"Quality check state should remain '{initial_state}' after lot assignment, " f"demonstrating that lot preservation behavior is active for receipt operations" ) # PROPERTY VERIFICATION PART 4: Lot propagation (demonstrating behavior is active) # The lot should be propagated to the quality check self.assertEqual( quality_check.lot_id.id, lot.id, f"Quality check should have lot {lot.name} assigned, " f"demonstrating that lot propagation is active for receipt operations" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), ) def test_property_receipt_operation_detection_robustness(self, lot_name_seed): """ Property: For any receipt operation, the system should correctly identify it as a receipt operation through multiple detection paths. This test verifies that the receipt operation detection is robust and works through different access paths (picking_id, move_id, etc.). Validates: Requirement 4.1 (operation type identification) """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Create receipt picking picking, move = self._create_receipt_picking(product) # 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, state='pass') # PROPERTY VERIFICATION: Multiple detection paths should all identify this as a receipt operation # Path 1: Through picking_id.picking_type_id.code self.assertEqual( picking.picking_type_id.code, 'incoming', "Picking type code should be 'incoming'" ) # Path 2: Through move_line._is_receipt_operation() self.assertTrue( move_line._is_receipt_operation(), "Move line should be identified as receipt operation" ) # Path 3: Through quality_check._is_receipt_operation() self.assertTrue( quality_check._is_receipt_operation(), "Quality check should be identified as receipt operation" ) # Path 4: Through quality_check.picking_id.picking_type_id.code if quality_check.picking_id: self.assertEqual( quality_check.picking_id.picking_type_id.code, 'incoming', "Quality check's picking should have 'incoming' code" ) # Path 5: Through quality_check.move_line_id.picking_id.picking_type_id.code if quality_check.move_line_id and quality_check.move_line_id.picking_id: self.assertEqual( quality_check.move_line_id.picking_id.picking_type_id.code, 'incoming', "Quality check's move line's picking should have 'incoming' code" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), initial_state=st.sampled_from(['pass', 'fail']), ) def test_property_receipt_operation_preserves_completed_checks(self, lot_name_seed, initial_state): """ Property: For any completed quality check (pass or fail) on a receipt operation, the lot preservation behavior should prevent state reset when lot is assigned. This is a critical test that demonstrates the core value of the module: quality checks completed before lot assignment should not be reset. Validates: Requirement 4.1 (receipt operation behavior application) """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Create receipt picking picking, move = self._create_receipt_picking(product) # Get the move line (no lot assigned yet) move_line = picking.move_line_ids[0] # Create quality check with completed state (pass or fail) quality_check = self._create_quality_check_for_move_line(move_line, state=initial_state) # Verify this is a receipt operation self.assertTrue( quality_check._is_receipt_operation(), "Quality check should be for a receipt operation" ) # Verify the quality check is in the expected completed state self.assertEqual( quality_check.quality_state, initial_state, f"Quality check should be in '{initial_state}' state before lot assignment" ) # Now assign a lot number (this is when standard Odoo would reset the state) 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: The completed quality check state should be preserved # This demonstrates that the receipt operation behavior is active and working self.assertEqual( quality_check.quality_state, initial_state, f"Quality check should remain in '{initial_state}' state after lot assignment. " f"This demonstrates that the lot preservation behavior is active for receipt operations." ) # Additionally verify the lot was assigned self.assertEqual( quality_check.lot_id.id, lot.id, "Quality check should have the lot assigned" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), ) def test_property_receipt_operation_behavior_consistency(self, lot_name_seed): """ Property: For any receipt operation, the behavior should be consistent across multiple lot assignments and updates. This test verifies that the receipt operation behavior remains active throughout the lifecycle of the quality check. Validates: Requirement 4.1 """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Create receipt picking picking, move = self._create_receipt_picking(product) # Get the move line move_line = picking.move_line_ids[0] # Create quality check with 'pass' state quality_check = self._create_quality_check_for_move_line(move_line, state='pass') # Verify receipt operation detection self.assertTrue( quality_check._is_receipt_operation(), "Quality check should be identified as receipt operation" ) # First lot assignment lot1 = self._create_lot(product, f'LOT-1-{lot_name_seed}') move_line.write({'lot_id': lot1.id}) quality_check.invalidate_recordset() # Verify state preserved after first assignment self.assertEqual( quality_check.quality_state, 'pass', "State should be preserved after first lot assignment" ) # Verify receipt operation detection still works self.assertTrue( quality_check._is_receipt_operation(), "Quality check should still be identified as receipt operation after lot assignment" ) # Second lot assignment (lot change) lot2 = self._create_lot(product, f'LOT-2-{lot_name_seed}') move_line.write({'lot_id': lot2.id}) quality_check.invalidate_recordset() # PROPERTY VERIFICATION: State should still be preserved after lot change self.assertEqual( quality_check.quality_state, 'pass', "State should be preserved after lot change, demonstrating consistent behavior" ) # Verify the new lot is assigned self.assertEqual( quality_check.lot_id.id, lot2.id, "Quality check should have the new lot assigned" ) # Verify receipt operation detection still works after multiple updates self.assertTrue( quality_check._is_receipt_operation(), "Quality check should still be identified as receipt operation after multiple updates" )