# -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase from hypothesis import given, strategies as st, settings, assume class TestStatePreservation(TransactionCase): """ Property-based tests for quality check state preservation during lot assignment. Feature: quality-check-lot-preserve, Property 1: State preservation during lot assignment Property: For any quality check on a receipt operation in any state (Pass, Fail, In Progress, Todo), when a lot number is assigned to the related stock move line, the quality check state should remain unchanged. Validates: Requirements 1.1, 1.2 """ def setUp(self): super(TestStatePreservation, 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_state_preservation_on_lot_assignment(self, lot_name_seed, quality_state): """ Property: For any quality check on a receipt operation in any state, when a lot number is assigned to the related stock move line, the quality check state should remain unchanged. This test verifies Requirements 1.1 and 1.2: - 1.1: Quality check state is preserved regardless of subsequent lot number assignment - 1.2: Quality check maintains its state (Pass, Fail, In Progress, Todo) when lot is assigned Test strategy: 1. Create a receipt operation with a quality check in a specific state 2. Record the initial state 3. Assign a lot number to the stock move line 4. Verify the quality check state 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 with the specified state quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) # Record the initial quality check state initial_state = quality_check.quality_state # Verify the quality check is for a receipt operation self.assertTrue( quality_check._is_receipt_operation(), "Quality check should be identified as a receipt operation" ) # Create and assign a lot number to the move line lot = self._create_lot(product, f'LOT-{lot_name_seed}') # Assign the lot to the move line (this is the critical action) move_line.write({'lot_id': lot.id}) # Refresh quality check from database to get latest state quality_check.invalidate_recordset() # PROPERTY VERIFICATION: # The quality check state should remain unchanged after lot assignment self.assertEqual( quality_check.quality_state, initial_state, f"Quality check state should remain '{initial_state}' after lot assignment, " f"but found '{quality_check.quality_state}'" ) # Additionally verify the lot was actually assigned to the quality check self.assertEqual( quality_check.lot_id.id, lot.id, f"Quality check should have lot {lot.name} assigned" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), ) def test_property_pass_state_preservation(self, lot_name_seed): """ Specific property test for "Pass" state preservation. Property: For any quality check marked as "Pass" on a receipt operation, when a lot number is assigned, the quality check should remain in "Pass" state. This test specifically validates Requirement 1.3: - When a quality check is marked as "Pass" before lot assignment, the system keeps the quality check as "Pass" after lot number assignment """ # 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 initial state is "pass" self.assertEqual( quality_check.quality_state, 'pass', "Quality check should initially be in 'pass' state" ) # Create and assign a lot number 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: Quality check should still be in "pass" state self.assertEqual( quality_check.quality_state, 'pass', "Quality check should remain in 'pass' state after lot assignment" ) @settings(max_examples=100, deadline=None) @given( lot_name_seed=st.integers(min_value=1, max_value=1000000), initial_state=st.sampled_from(['none', 'pass', 'fail']), ) def test_property_state_preservation_without_initial_lot(self, lot_name_seed, initial_state): """ Property: For any quality check created without a lot number, when a lot number is later assigned to the move line, the quality check state should be preserved. This tests the common workflow where quality checks are performed before lot numbers are generated or assigned. """ # 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] # Verify no lot is assigned initially self.assertFalse(move_line.lot_id, "Move line should not have a lot initially") # Create quality check with the specified state (no lot assigned) quality_check = self._create_quality_check_for_move_line(move_line, state=initial_state) # Verify no lot is assigned to quality check initially self.assertFalse(quality_check.lot_id, "Quality check should not have a lot initially") # Record the initial state recorded_state = quality_check.quality_state # Now assign a lot number (simulating lot generation after quality check) 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: # 1. State should be preserved self.assertEqual( quality_check.quality_state, recorded_state, f"Quality check state should remain '{recorded_state}' after lot assignment" ) # 2. Lot should be assigned to quality check self.assertEqual( quality_check.lot_id.id, lot.id, "Quality check should have the 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), quality_state=st.sampled_from(['none', 'pass', 'fail']), ) def test_property_state_preservation_on_lot_change(self, lot_seed_1, lot_seed_2, quality_state): """ Property: For any quality check with an assigned lot number, when the lot number is changed on the move line, the quality check state should remain unchanged. This verifies that state preservation works not just for initial assignment, but also for lot number changes. """ # Ensure the two lot numbers are different assume(lot_seed_1 != lot_seed_2) # 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 with the specified state quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) # Assign initial lot number lot_1 = self._create_lot(product, f'LOT-1-{lot_seed_1}') move_line.write({'lot_id': lot_1.id}) # Refresh and record the state after first lot assignment quality_check.invalidate_recordset() state_after_first_assignment = quality_check.quality_state # Verify state is still the original state self.assertEqual( state_after_first_assignment, quality_state, "State should be preserved after first lot assignment" ) # Now change the lot number lot_2 = self._create_lot(product, f'LOT-2-{lot_seed_2}') move_line.write({'lot_id': lot_2.id}) # Refresh quality check from database quality_check.invalidate_recordset() # PROPERTY VERIFICATION: # State should still be the original state after lot change self.assertEqual( quality_check.quality_state, quality_state, f"Quality check state should remain '{quality_state}' after lot change" ) # Verify the lot was actually changed self.assertEqual( quality_check.lot_id.id, lot_2.id, "Quality check should have the new lot assigned" )