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

370 lines
15 KiB
Python

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