422 lines
17 KiB
Python
422 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from odoo.tests.common import TransactionCase
|
|
from hypothesis import given, strategies as st, settings, assume
|
|
|
|
|
|
class TestReceiptOperationBehavior(TransactionCase):
|
|
"""
|
|
Property-based tests for receipt operation behavior activation.
|
|
|
|
Feature: quality-check-lot-preserve, Property 5: Receipt operation behavior activation
|
|
|
|
Property: For any quality check associated with a receipt operation (picking type code = 'incoming'),
|
|
the lot preservation behavior should be applied when lot numbers are assigned.
|
|
|
|
Validates: Requirements 4.1
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(TestReceiptOperationBehavior, 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_receipt_operation_activates_preservation(self, lot_name_seed, quality_state):
|
|
"""
|
|
Property: For any quality check associated with a receipt operation (picking type code = 'incoming'),
|
|
the lot preservation behavior should be applied when lot numbers are assigned.
|
|
|
|
This test verifies Requirement 4.1:
|
|
- When a quality check is associated with a receipt operation (incoming shipment),
|
|
the system applies the lot preservation behavior
|
|
|
|
Test strategy:
|
|
1. Create a receipt operation (picking_type_code = 'incoming')
|
|
2. Create a quality check with a specific state
|
|
3. Assign a lot number to the stock move line
|
|
4. Verify that:
|
|
a) The quality check is correctly identified as a receipt operation
|
|
b) The lot preservation behavior is activated (_should_preserve_state returns True)
|
|
c) The quality check state is preserved (demonstrating the behavior is active)
|
|
d) The lot number is propagated to the quality check
|
|
"""
|
|
# Create a product with lot tracking
|
|
product = self._create_product_with_tracking(f'Test Product {lot_name_seed}')
|
|
|
|
# Create receipt picking (this is the key: picking_type_code = 'incoming')
|
|
picking, move = self._create_receipt_picking(product)
|
|
|
|
# Verify this is indeed a receipt operation
|
|
self.assertEqual(
|
|
picking.picking_type_id.code,
|
|
'incoming',
|
|
"Picking should be a receipt operation (incoming)"
|
|
)
|
|
|
|
# Get the move line
|
|
move_line = picking.move_line_ids[0]
|
|
|
|
# Verify the move line is identified as a receipt operation
|
|
self.assertTrue(
|
|
move_line._is_receipt_operation(),
|
|
"Move line should be identified as a receipt operation"
|
|
)
|
|
|
|
# Create quality check with the specified state
|
|
quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state)
|
|
|
|
# PROPERTY VERIFICATION PART 1: Receipt operation detection
|
|
# The quality check should be correctly identified as a receipt operation
|
|
self.assertTrue(
|
|
quality_check._is_receipt_operation(),
|
|
"Quality check should be identified as a receipt operation"
|
|
)
|
|
|
|
# PROPERTY VERIFICATION PART 2: Preservation behavior activation
|
|
# The quality check should indicate that state should be preserved
|
|
self.assertTrue(
|
|
quality_check._should_preserve_state(),
|
|
"Quality check should indicate that state preservation is active for receipt operations"
|
|
)
|
|
|
|
# Record the initial quality check state
|
|
initial_state = quality_check.quality_state
|
|
|
|
# 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 PART 3: State preservation (demonstrating behavior is active)
|
|
# The quality check state should remain unchanged, proving the preservation behavior is active
|
|
self.assertEqual(
|
|
quality_check.quality_state,
|
|
initial_state,
|
|
f"Quality check state should remain '{initial_state}' after lot assignment, "
|
|
f"demonstrating that lot preservation behavior is active for receipt operations"
|
|
)
|
|
|
|
# PROPERTY VERIFICATION PART 4: Lot propagation (demonstrating behavior is active)
|
|
# The lot should be propagated to the quality check
|
|
self.assertEqual(
|
|
quality_check.lot_id.id,
|
|
lot.id,
|
|
f"Quality check should have lot {lot.name} assigned, "
|
|
f"demonstrating that lot propagation is active for receipt operations"
|
|
)
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
)
|
|
def test_property_receipt_operation_detection_robustness(self, lot_name_seed):
|
|
"""
|
|
Property: For any receipt operation, the system should correctly identify it
|
|
as a receipt operation through multiple detection paths.
|
|
|
|
This test verifies that the receipt operation detection is robust and works
|
|
through different access paths (picking_id, move_id, etc.).
|
|
|
|
Validates: Requirement 4.1 (operation type identification)
|
|
"""
|
|
# 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
|
|
quality_check = self._create_quality_check_for_move_line(move_line, state='pass')
|
|
|
|
# PROPERTY VERIFICATION: Multiple detection paths should all identify this as a receipt operation
|
|
|
|
# Path 1: Through picking_id.picking_type_id.code
|
|
self.assertEqual(
|
|
picking.picking_type_id.code,
|
|
'incoming',
|
|
"Picking type code should be 'incoming'"
|
|
)
|
|
|
|
# Path 2: Through move_line._is_receipt_operation()
|
|
self.assertTrue(
|
|
move_line._is_receipt_operation(),
|
|
"Move line should be identified as receipt operation"
|
|
)
|
|
|
|
# Path 3: Through quality_check._is_receipt_operation()
|
|
self.assertTrue(
|
|
quality_check._is_receipt_operation(),
|
|
"Quality check should be identified as receipt operation"
|
|
)
|
|
|
|
# Path 4: Through quality_check.picking_id.picking_type_id.code
|
|
if quality_check.picking_id:
|
|
self.assertEqual(
|
|
quality_check.picking_id.picking_type_id.code,
|
|
'incoming',
|
|
"Quality check's picking should have 'incoming' code"
|
|
)
|
|
|
|
# Path 5: Through quality_check.move_line_id.picking_id.picking_type_id.code
|
|
if quality_check.move_line_id and quality_check.move_line_id.picking_id:
|
|
self.assertEqual(
|
|
quality_check.move_line_id.picking_id.picking_type_id.code,
|
|
'incoming',
|
|
"Quality check's move line's picking should have 'incoming' code"
|
|
)
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
initial_state=st.sampled_from(['pass', 'fail']),
|
|
)
|
|
def test_property_receipt_operation_preserves_completed_checks(self, lot_name_seed, initial_state):
|
|
"""
|
|
Property: For any completed quality check (pass or fail) on a receipt operation,
|
|
the lot preservation behavior should prevent state reset when lot is assigned.
|
|
|
|
This is a critical test that demonstrates the core value of the module:
|
|
quality checks completed before lot assignment should not be reset.
|
|
|
|
Validates: Requirement 4.1 (receipt operation behavior application)
|
|
"""
|
|
# 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]
|
|
|
|
# Create quality check with completed state (pass or fail)
|
|
quality_check = self._create_quality_check_for_move_line(move_line, state=initial_state)
|
|
|
|
# Verify this is a receipt operation
|
|
self.assertTrue(
|
|
quality_check._is_receipt_operation(),
|
|
"Quality check should be for a receipt operation"
|
|
)
|
|
|
|
# Verify the quality check is in the expected completed state
|
|
self.assertEqual(
|
|
quality_check.quality_state,
|
|
initial_state,
|
|
f"Quality check should be in '{initial_state}' state before lot assignment"
|
|
)
|
|
|
|
# Now assign a lot number (this is when standard Odoo would reset the state)
|
|
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 completed quality check state should be preserved
|
|
# This demonstrates that the receipt operation behavior is active and working
|
|
self.assertEqual(
|
|
quality_check.quality_state,
|
|
initial_state,
|
|
f"Quality check should remain in '{initial_state}' state after lot assignment. "
|
|
f"This demonstrates that the lot preservation behavior is active for receipt operations."
|
|
)
|
|
|
|
# Additionally verify the lot was assigned
|
|
self.assertEqual(
|
|
quality_check.lot_id.id,
|
|
lot.id,
|
|
"Quality check should have the lot assigned"
|
|
)
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
)
|
|
def test_property_receipt_operation_behavior_consistency(self, lot_name_seed):
|
|
"""
|
|
Property: For any receipt operation, the behavior should be consistent
|
|
across multiple lot assignments and updates.
|
|
|
|
This test verifies that the receipt operation behavior remains active
|
|
throughout the lifecycle of the quality check.
|
|
|
|
Validates: Requirement 4.1
|
|
"""
|
|
# 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 receipt operation detection
|
|
self.assertTrue(
|
|
quality_check._is_receipt_operation(),
|
|
"Quality check should be identified as receipt operation"
|
|
)
|
|
|
|
# First lot assignment
|
|
lot1 = self._create_lot(product, f'LOT-1-{lot_name_seed}')
|
|
move_line.write({'lot_id': lot1.id})
|
|
quality_check.invalidate_recordset()
|
|
|
|
# Verify state preserved after first assignment
|
|
self.assertEqual(
|
|
quality_check.quality_state,
|
|
'pass',
|
|
"State should be preserved after first lot assignment"
|
|
)
|
|
|
|
# Verify receipt operation detection still works
|
|
self.assertTrue(
|
|
quality_check._is_receipt_operation(),
|
|
"Quality check should still be identified as receipt operation after lot assignment"
|
|
)
|
|
|
|
# Second lot assignment (lot change)
|
|
lot2 = self._create_lot(product, f'LOT-2-{lot_name_seed}')
|
|
move_line.write({'lot_id': lot2.id})
|
|
quality_check.invalidate_recordset()
|
|
|
|
# PROPERTY VERIFICATION: State should still be preserved after lot change
|
|
self.assertEqual(
|
|
quality_check.quality_state,
|
|
'pass',
|
|
"State should be preserved after lot change, demonstrating consistent behavior"
|
|
)
|
|
|
|
# Verify the new lot is assigned
|
|
self.assertEqual(
|
|
quality_check.lot_id.id,
|
|
lot2.id,
|
|
"Quality check should have the new lot assigned"
|
|
)
|
|
|
|
# Verify receipt operation detection still works after multiple updates
|
|
self.assertTrue(
|
|
quality_check._is_receipt_operation(),
|
|
"Quality check should still be identified as receipt operation after multiple updates"
|
|
)
|