# -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase from hypothesis import given, strategies as st, settings, assume class TestRelationshipIntegrity(TransactionCase): """ Property-based tests for relationship integrity during lot number updates. Feature: quality-check-lot-preserve, Property 4: Relationship integrity during updates Property: For any quality check linked to a stock move line, when lot number updates occur, the link between the quality check and stock move line should remain intact (move_line_id unchanged). Validates: Requirements 3.3 """ def setUp(self): super(TestRelationshipIntegrity, 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', '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_relationship_integrity_on_initial_lot_assignment(self, lot_name_seed, quality_state): """ Property: For any quality check linked to a stock move line, when a lot number is initially assigned to the move line, the link between the quality check and stock move line should remain intact. This test verifies Requirement 3.3: - When a lot number update occurs, the system maintains the link between the quality check and the stock move line Test strategy: 1. Create a receipt with a quality check linked to a move line 2. Record the initial move_line_id relationship 3. Assign a lot number to the move line 4. Verify the move_line_id relationship remains unchanged """ # 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 linked to the move line quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) # Record the initial move_line_id relationship initial_move_line_id = quality_check.move_line_id.id # Verify the relationship is established self.assertEqual( initial_move_line_id, move_line.id, "Quality check should be linked to the move line initially" ) # 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: # The move_line_id relationship should remain unchanged after lot assignment self.assertEqual( quality_check.move_line_id.id, initial_move_line_id, f"Quality check should still be linked to move line {initial_move_line_id} " f"after lot assignment" ) # Additionally verify the lot was actually assigned self.assertEqual( quality_check.lot_id.id, lot.id, "Quality check should have the lot assigned" ) @settings(max_examples=100, deadline=None) @given( initial_lot_seed=st.integers(min_value=1, max_value=1000000), updated_lot_seed=st.integers(min_value=1, max_value=1000000), quality_state=st.sampled_from(['none', 'pass', 'fail']), ) def test_property_relationship_integrity_on_lot_update(self, initial_lot_seed, updated_lot_seed, quality_state): """ Property: For any quality check linked to a stock move line with an assigned lot, when the lot number is changed on the move line, the link between the quality check and stock move line should remain intact. This test verifies that relationship integrity is maintained even when lot numbers are updated (not just initially assigned). Validates: Requirement 3.3 """ # Ensure the two lot numbers are different assume(initial_lot_seed != updated_lot_seed) # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {initial_lot_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 linked to the move line quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) # Record the initial move_line_id relationship initial_move_line_id = quality_check.move_line_id.id # Assign initial lot number initial_lot = self._create_lot(product, f'LOT-INITIAL-{initial_lot_seed}') move_line.write({'lot_id': initial_lot.id}) # Refresh and verify relationship is still intact after first assignment quality_check.invalidate_recordset() self.assertEqual( quality_check.move_line_id.id, initial_move_line_id, "Relationship should be intact after initial lot assignment" ) # Create a new lot and update the move line updated_lot = self._create_lot(product, f'LOT-UPDATED-{updated_lot_seed}') move_line.write({'lot_id': updated_lot.id}) # Refresh quality check from database quality_check.invalidate_recordset() # PROPERTY VERIFICATION: # The move_line_id relationship should remain unchanged after lot update self.assertEqual( quality_check.move_line_id.id, initial_move_line_id, f"Quality check should still be linked to move line {initial_move_line_id} " f"after lot update" ) # Verify the lot was actually updated self.assertEqual( quality_check.lot_id.id, updated_lot.id, "Quality check should have the updated lot assigned" ) @settings(max_examples=100, deadline=None) @given( lot_seed_1=st.integers(min_value=1, max_value=1000000), lot_seed_2=st.integers(min_value=1, max_value=1000000), lot_seed_3=st.integers(min_value=1, max_value=1000000), ) def test_property_relationship_integrity_multiple_updates(self, lot_seed_1, lot_seed_2, lot_seed_3): """ Property: For any quality check linked to a stock move line, when multiple consecutive lot number updates occur, the link between the quality check and stock move line should remain intact throughout all updates. This test verifies that relationship integrity is maintained even with multiple sequential lot updates. Validates: Requirement 3.3 """ # Ensure all lot numbers are different assume(lot_seed_1 != lot_seed_2) assume(lot_seed_2 != lot_seed_3) assume(lot_seed_1 != lot_seed_3) # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_seed_1}') # Create receipt picking picking, move = self._create_receipt_picking(product) # Get the move line move_line = picking.move_line_ids[0] # Create quality check linked to the move line quality_check = self._create_quality_check_for_move_line(move_line, state='pass') # Record the initial move_line_id relationship initial_move_line_id = quality_check.move_line_id.id # Create three different lots lot_1 = self._create_lot(product, f'LOT-1-{lot_seed_1}') lot_2 = self._create_lot(product, f'LOT-2-{lot_seed_2}') lot_3 = self._create_lot(product, f'LOT-3-{lot_seed_3}') # First update: assign lot_1 move_line.write({'lot_id': lot_1.id}) quality_check.invalidate_recordset() self.assertEqual( quality_check.move_line_id.id, initial_move_line_id, "Relationship should be intact after first lot assignment" ) # Second update: change to lot_2 move_line.write({'lot_id': lot_2.id}) quality_check.invalidate_recordset() self.assertEqual( quality_check.move_line_id.id, initial_move_line_id, "Relationship should be intact after second lot update" ) # Third update: change to lot_3 move_line.write({'lot_id': lot_3.id}) quality_check.invalidate_recordset() # PROPERTY VERIFICATION: # The move_line_id relationship should remain unchanged after all updates self.assertEqual( quality_check.move_line_id.id, initial_move_line_id, f"Quality check should still be linked to move line {initial_move_line_id} " f"after multiple lot updates" ) # Verify the final lot was assigned self.assertEqual( quality_check.lot_id.id, lot_3.id, "Quality check should have the final lot assigned" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), num_updates=st.integers(min_value=1, max_value=5), ) def test_property_relationship_integrity_variable_updates(self, lot_name_seed, num_updates): """ Property: For any quality check linked to a stock move line, regardless of the number of lot updates, the move_line_id relationship should never change. This test uses a variable number of updates to verify relationship integrity across different update patterns. Validates: Requirement 3.3 """ # 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 linked to the move line quality_check = self._create_quality_check_for_move_line(move_line, state='pass') # Record the initial move_line_id relationship initial_move_line_id = quality_check.move_line_id.id # Perform multiple lot updates for i in range(num_updates): lot = self._create_lot(product, f'LOT-{lot_name_seed}-{i}') move_line.write({'lot_id': lot.id}) # Refresh and verify relationship after each update quality_check.invalidate_recordset() # PROPERTY VERIFICATION: Relationship should be intact after each update self.assertEqual( quality_check.move_line_id.id, initial_move_line_id, f"Quality check should still be linked to move line {initial_move_line_id} " f"after update {i+1} of {num_updates}" ) # Verify the lot was updated self.assertEqual( quality_check.lot_id.id, lot.id, f"Quality check should have lot {lot.name} assigned after update {i+1}" )