# -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase from hypothesis import given, strategies as st, settings, assume class TestLotNumberUpdateSynchronization(TransactionCase): """ Property-based tests for lot number update synchronization. Feature: quality-check-lot-preserve, Property 3: Lot number update synchronization Property: For any quality check with an assigned lot number, when the lot number on the related stock move line is changed, the quality check should be updated with the new lot number. Validates: Requirements 3.1 """ def setUp(self): super(TestLotNumberUpdateSynchronization, 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( 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_lot_update_synchronization(self, initial_lot_seed, updated_lot_seed, quality_state): """ Property: For any quality check with an assigned lot number, when the lot number on the related stock move line is changed, the quality check should be updated with the new lot number. This test verifies Requirement 3.1: - When a lot number on a stock move line is changed after initial assignment, the related quality check is updated with the new lot number Test strategy: 1. Create a receipt with a quality check 2. Assign an initial lot number to the move line 3. Verify the quality check receives the initial lot number 4. Change the lot number on the move line to a different lot 5. Verify the quality check is updated with the new lot number """ # 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 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 # Create and assign the initial lot number initial_lot = self._create_lot(product, f'LOT-INITIAL-{initial_lot_seed}') move_line.write({'lot_id': initial_lot.id}) # Refresh quality check from database quality_check.invalidate_recordset() # Verify the quality check received the initial lot number self.assertEqual( quality_check.lot_id.id, initial_lot.id, f"Quality check should have initial lot {initial_lot.name} assigned" ) # Create a new lot number updated_lot = self._create_lot(product, f'LOT-UPDATED-{updated_lot_seed}') # Change the lot number on the move line move_line.write({'lot_id': updated_lot.id}) # Refresh quality check from database quality_check.invalidate_recordset() # PROPERTY VERIFICATION: # 1. The quality check should be updated with the new lot number (Requirement 3.1) self.assertEqual( quality_check.lot_id.id, updated_lot.id, f"Quality check should be updated with new lot {updated_lot.name} " f"(was {initial_lot.name})" ) # 2. The quality check state should remain unchanged (Requirement 3.2) self.assertEqual( quality_check.quality_state, initial_state, f"Quality check state should remain {initial_state} after lot update" ) # 3. The relationship between quality check and move line should remain intact (Requirement 3.3) self.assertEqual( quality_check.move_line_id.id, move_line.id, "Quality check should still be linked to the same move line" ) @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_multiple_lot_updates(self, lot_seed_1, lot_seed_2, lot_seed_3): """ Property: For any quality check, multiple consecutive lot number changes on the move line should result in the quality check always reflecting the most recent lot number. This test verifies that the synchronization works correctly even with multiple updates in sequence. """ # 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 quality_check = self._create_quality_check_for_move_line(move_line, state='pass') # 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.lot_id.id, lot_1.id) # Second update: change to lot_2 move_line.write({'lot_id': lot_2.id}) quality_check.invalidate_recordset() self.assertEqual(quality_check.lot_id.id, lot_2.id) # Third update: change to lot_3 move_line.write({'lot_id': lot_3.id}) quality_check.invalidate_recordset() # PROPERTY VERIFICATION: Quality check should have the final lot number self.assertEqual( quality_check.lot_id.id, lot_3.id, f"Quality check should have the final lot {lot_3.name} after multiple updates" ) # State should still be 'pass' self.assertEqual( quality_check.quality_state, 'pass', "Quality check state should remain 'pass' after multiple lot updates" )