# -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase from hypothesis import given, strategies as st, settings, assume from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule, invariant class TestLotNumberPropagation(TransactionCase): """ Property-based tests for lot number propagation to quality checks. Feature: quality-check-lot-preserve, Property 2: Lot number propagation to correct quality check Property: For any stock move line on a receipt operation with an assigned lot number, the lot number should be copied to the related quality check for that specific product and move line, and not to unrelated quality checks. Validates: Requirements 2.1, 2.3 """ def setUp(self): super(TestLotNumberPropagation, 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, products): """Helper to create a receipt picking with move lines for given products""" 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, }) moves = [] for product in products: 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, }) moves.append(move) # Confirm the picking to create move lines picking.action_confirm() return picking, moves 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( num_products=st.integers(min_value=1, max_value=3), lot_name_seed=st.integers(min_value=1, max_value=1000000), ) def test_property_lot_propagation_to_correct_check(self, num_products, lot_name_seed): """ Property: For any stock move line on a receipt operation with an assigned lot number, the lot number should be copied to the related quality check for that specific product and move line, and not to unrelated quality checks. This test verifies Requirements 2.1 and 2.3: - 2.1: Lot number is copied to the related quality check - 2.3: Lot number is copied only to the correct quality check, not unrelated ones """ # Create products products = [] for i in range(num_products): product = self._create_product_with_tracking(f'Test Product {lot_name_seed}_{i}') products.append(product) # Create receipt picking with move lines picking, moves = self._create_receipt_picking(products) # Get the move lines move_lines = picking.move_line_ids assume(len(move_lines) > 0) # Create quality checks for each move line quality_checks = [] for move_line in move_lines: qc = self._create_quality_check_for_move_line(move_line, state='pass') quality_checks.append(qc) # Select the first move line to assign a lot number target_move_line = move_lines[0] target_quality_check = quality_checks[0] # Create and assign a lot number to the target move line lot = self._create_lot(target_move_line.product_id, f'LOT-{lot_name_seed}') # Record initial lot_id values for all quality checks initial_lot_ids = {qc.id: qc.lot_id.id for qc in quality_checks} # Assign the lot to the target move line target_move_line.write({'lot_id': lot.id}) # Refresh quality checks from database for qc in quality_checks: qc.invalidate_recordset() # PROPERTY VERIFICATION: # 1. The target quality check should have the lot number assigned (Requirement 2.1) target_quality_check.invalidate_recordset() self.assertEqual( target_quality_check.lot_id.id, lot.id, f"Quality check {target_quality_check.id} should have lot {lot.name} assigned" ) # 2. Other quality checks should NOT have this lot number (Requirement 2.3) for i, qc in enumerate(quality_checks[1:], start=1): qc.invalidate_recordset() # The lot_id should remain unchanged (either False or the initial value) self.assertEqual( qc.lot_id.id, initial_lot_ids[qc.id], f"Unrelated quality check {qc.id} (for product {qc.product_id.name}) " f"should not have lot {lot.name} assigned" ) # Additionally, if a lot was assigned, it should not be the target lot if qc.lot_id: self.assertNotEqual( qc.lot_id.id, lot.id, f"Unrelated quality check {qc.id} should not have the target lot assigned" ) @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_lot_propagation_single_check(self, lot_name_seed, quality_state): """ Simplified property test: For any single quality check on a receipt operation, when a lot number is assigned to its move line, the lot number should be copied to the quality check. Validates: Requirement 2.1 """ # Create a product with lot tracking product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') # Create receipt picking picking, moves = 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=quality_state) # Create and assign a lot number lot = self._create_lot(product, f'LOT-{lot_name_seed}') # Assign the lot to the move line move_line.write({'lot_id': lot.id}) # Refresh quality check from database quality_check.invalidate_recordset() # PROPERTY VERIFICATION: The quality check should have the lot number assigned self.assertEqual( quality_check.lot_id.id, lot.id, f"Quality check should have lot {lot.name} assigned" )