quality_check_lot_preserve/tests/test_property_lot_propagation.py
2025-11-27 10:00:38 +07:00

255 lines
10 KiB
Python

# -*- 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"
)