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

400 lines
16 KiB
Python

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