501 lines
21 KiB
Python
501 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from odoo.tests.common import TransactionCase
|
|
from hypothesis import given, strategies as st, settings
|
|
|
|
|
|
class TestNonReceiptOperationBehavior(TransactionCase):
|
|
"""
|
|
Property-based tests for non-receipt operation standard behavior.
|
|
|
|
Feature: quality-check-lot-preserve, Property 6: Non-receipt operation standard behavior
|
|
|
|
Property: For any quality check associated with non-receipt operations (manufacturing, internal, outgoing),
|
|
the standard Odoo behavior (state reset on lot assignment) should occur.
|
|
|
|
Validates: Requirements 4.2
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(TestNonReceiptOperationBehavior, 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.stock_location = self.env.ref('stock.stock_location_stock')
|
|
self.customer_location = self.env.ref('stock.stock_location_customers')
|
|
|
|
# Get or create different picking types for non-receipt operations
|
|
self.internal_picking_type = self._get_or_create_picking_type('internal', 'Internal Transfers')
|
|
self.outgoing_picking_type = self._get_or_create_picking_type('outgoing', 'Delivery Orders')
|
|
|
|
def _get_or_create_picking_type(self, code, name):
|
|
"""Helper to get or create a picking type"""
|
|
picking_type = self.env['stock.picking.type'].search([
|
|
('code', '=', code)
|
|
], limit=1)
|
|
|
|
if not picking_type:
|
|
warehouse = self.env['stock.warehouse'].search([], limit=1)
|
|
if not warehouse:
|
|
# Create a minimal warehouse if none exists
|
|
warehouse = self.env['stock.warehouse'].create({
|
|
'name': 'Test Warehouse',
|
|
'code': 'TEST',
|
|
})
|
|
|
|
picking_type = self.env['stock.picking.type'].create({
|
|
'name': name,
|
|
'code': code,
|
|
'sequence_code': code.upper()[:5],
|
|
'warehouse_id': warehouse.id,
|
|
})
|
|
|
|
return picking_type
|
|
|
|
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_picking(self, picking_type, product, location_id, location_dest_id):
|
|
"""Helper to create a picking with a move line for the given product"""
|
|
picking = self.StockPicking.create({
|
|
'picking_type_id': picking_type.id,
|
|
'location_id': location_id.id,
|
|
'location_dest_id': location_dest_id.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': location_id.id,
|
|
'location_dest_id': location_dest_id.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, picking_type, 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', [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, 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),
|
|
operation_type=st.sampled_from(['internal', 'outgoing']),
|
|
)
|
|
def test_property_non_receipt_operation_not_identified_as_receipt(self, lot_name_seed, operation_type):
|
|
"""
|
|
Property: For any quality check associated with non-receipt operations,
|
|
the system should correctly identify it as NOT a receipt operation.
|
|
|
|
This test verifies Requirement 4.2:
|
|
- When a quality check is associated with non-receipt operations (manufacturing, internal transfers, delivery),
|
|
the system should follow standard Odoo quality check behavior
|
|
|
|
Test strategy:
|
|
1. Create a non-receipt operation (internal or outgoing)
|
|
2. Create a quality check for that operation
|
|
3. Verify that:
|
|
a) The quality check is NOT identified as a receipt operation
|
|
b) The preservation behavior is NOT activated (_should_preserve_state returns False)
|
|
"""
|
|
# Create a product with lot tracking
|
|
product = self._create_product_with_tracking(f'Test Product {lot_name_seed}')
|
|
|
|
# Select the appropriate picking type based on operation_type
|
|
if operation_type == 'internal':
|
|
picking_type = self.internal_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.stock_location # Internal transfer within same location
|
|
else: # outgoing
|
|
picking_type = self.outgoing_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.customer_location
|
|
|
|
# Create non-receipt picking
|
|
picking, move = self._create_picking(picking_type, product, location_id, location_dest_id)
|
|
|
|
# Verify this is NOT a receipt operation
|
|
self.assertNotEqual(
|
|
picking.picking_type_id.code,
|
|
'incoming',
|
|
f"Picking should NOT be a receipt operation (code is '{picking.picking_type_id.code}')"
|
|
)
|
|
|
|
# Get the move line
|
|
move_line = picking.move_line_ids[0]
|
|
|
|
# Verify the move line is NOT identified as a receipt operation
|
|
self.assertFalse(
|
|
move_line._is_receipt_operation(),
|
|
f"Move line should NOT be identified as a receipt operation for {operation_type} operations"
|
|
)
|
|
|
|
# Create quality check
|
|
quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass')
|
|
|
|
# PROPERTY VERIFICATION PART 1: Non-receipt operation detection
|
|
# The quality check should NOT be identified as a receipt operation
|
|
self.assertFalse(
|
|
quality_check._is_receipt_operation(),
|
|
f"Quality check should NOT be identified as a receipt operation for {operation_type} operations"
|
|
)
|
|
|
|
# PROPERTY VERIFICATION PART 2: Preservation behavior NOT activated
|
|
# The quality check should indicate that state should NOT be preserved
|
|
self.assertFalse(
|
|
quality_check._should_preserve_state(),
|
|
f"Quality check should indicate that state preservation is NOT active for {operation_type} operations"
|
|
)
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
operation_type=st.sampled_from(['internal', 'outgoing']),
|
|
)
|
|
def test_property_non_receipt_operation_no_automatic_lot_propagation(self, lot_name_seed, operation_type):
|
|
"""
|
|
Property: For any non-receipt operation, when a lot is assigned to a stock move line,
|
|
the lot should NOT be automatically propagated to quality checks by our module.
|
|
|
|
This verifies that our custom lot propagation logic only applies to receipt operations.
|
|
|
|
Validates: Requirement 4.2 (standard behavior for non-receipt operations)
|
|
"""
|
|
# Create a product with lot tracking
|
|
product = self._create_product_with_tracking(f'Test Product {lot_name_seed}')
|
|
|
|
# Select the appropriate picking type based on operation_type
|
|
if operation_type == 'internal':
|
|
picking_type = self.internal_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.stock_location
|
|
else: # outgoing
|
|
picking_type = self.outgoing_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.customer_location
|
|
|
|
# Create non-receipt picking
|
|
picking, move = self._create_picking(picking_type, product, location_id, location_dest_id)
|
|
|
|
# Get the move line
|
|
move_line = picking.move_line_ids[0]
|
|
|
|
# Create quality check WITHOUT a lot assigned
|
|
quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass')
|
|
|
|
# Verify no lot is assigned initially
|
|
self.assertFalse(
|
|
quality_check.lot_id,
|
|
"Quality check should not have a lot assigned initially"
|
|
)
|
|
|
|
# Create and assign a lot 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: Our custom lot propagation should NOT occur for non-receipt operations
|
|
# The quality check's lot_id should remain unset (or be set by standard Odoo behavior, not our module)
|
|
# We verify that our module's _update_quality_check_lot method was NOT triggered
|
|
# by checking that the quality check is correctly identified as non-receipt
|
|
self.assertFalse(
|
|
quality_check._is_receipt_operation(),
|
|
f"Quality check should be identified as non-receipt operation for {operation_type}"
|
|
)
|
|
|
|
# Our module should not have propagated the lot for non-receipt operations
|
|
# Note: Standard Odoo may or may not propagate the lot depending on its own logic,
|
|
# but our module should not interfere with non-receipt operations
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
operation_type=st.sampled_from(['internal', 'outgoing']),
|
|
)
|
|
def test_property_non_receipt_operation_standard_behavior_preserved(self, lot_name_seed, operation_type):
|
|
"""
|
|
Property: For any non-receipt operation, the module should not interfere with
|
|
standard Odoo quality check behavior.
|
|
|
|
This test verifies that our module's overrides correctly delegate to standard
|
|
behavior for non-receipt operations.
|
|
|
|
Validates: Requirement 4.2
|
|
"""
|
|
# Create a product with lot tracking
|
|
product = self._create_product_with_tracking(f'Test Product {lot_name_seed}')
|
|
|
|
# Select the appropriate picking type based on operation_type
|
|
if operation_type == 'internal':
|
|
picking_type = self.internal_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.stock_location
|
|
else: # outgoing
|
|
picking_type = self.outgoing_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.customer_location
|
|
|
|
# Create non-receipt picking
|
|
picking, move = self._create_picking(picking_type, product, location_id, location_dest_id)
|
|
|
|
# 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, picking_type, state='pass')
|
|
|
|
# PROPERTY VERIFICATION: The module should correctly identify this as non-receipt
|
|
# and delegate to standard behavior
|
|
self.assertFalse(
|
|
quality_check._is_receipt_operation(),
|
|
f"Quality check should be identified as non-receipt for {operation_type}"
|
|
)
|
|
|
|
# Verify that _should_preserve_state returns False for non-receipt operations
|
|
self.assertFalse(
|
|
quality_check._should_preserve_state(),
|
|
f"State preservation should NOT be active for {operation_type} operations"
|
|
)
|
|
|
|
# Verify that _update_lot_from_lot_line delegates to parent for non-receipt operations
|
|
# This method should return the result of super() for non-receipt operations
|
|
# We can't easily test the return value, but we can verify the operation type detection works
|
|
self.assertEqual(
|
|
picking.picking_type_id.code,
|
|
operation_type,
|
|
f"Picking type code should be '{operation_type}'"
|
|
)
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
)
|
|
def test_property_non_receipt_internal_operation_detection(self, lot_name_seed):
|
|
"""
|
|
Property: For any internal transfer operation, the system should correctly
|
|
identify it as a non-receipt operation and not apply lot preservation behavior.
|
|
|
|
Validates: Requirement 4.2 (internal transfers use standard behavior)
|
|
"""
|
|
# Create a product with lot tracking
|
|
product = self._create_product_with_tracking(f'Test Product {lot_name_seed}')
|
|
|
|
# Create internal transfer
|
|
picking, move = self._create_picking(
|
|
self.internal_picking_type,
|
|
product,
|
|
self.stock_location,
|
|
self.stock_location
|
|
)
|
|
|
|
# Verify this is an internal operation
|
|
self.assertEqual(
|
|
picking.picking_type_id.code,
|
|
'internal',
|
|
"Picking should be an internal transfer"
|
|
)
|
|
|
|
# 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,
|
|
self.internal_picking_type,
|
|
state='pass'
|
|
)
|
|
|
|
# PROPERTY VERIFICATION: Internal operations should not trigger preservation behavior
|
|
self.assertFalse(
|
|
quality_check._is_receipt_operation(),
|
|
"Internal transfer should NOT be identified as receipt operation"
|
|
)
|
|
|
|
self.assertFalse(
|
|
quality_check._should_preserve_state(),
|
|
"State preservation should NOT be active for internal transfers"
|
|
)
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
)
|
|
def test_property_non_receipt_outgoing_operation_detection(self, lot_name_seed):
|
|
"""
|
|
Property: For any outgoing/delivery operation, the system should correctly
|
|
identify it as a non-receipt operation and not apply lot preservation behavior.
|
|
|
|
Validates: Requirement 4.2 (delivery operations use standard behavior)
|
|
"""
|
|
# Create a product with lot tracking
|
|
product = self._create_product_with_tracking(f'Test Product {lot_name_seed}')
|
|
|
|
# Create outgoing/delivery operation
|
|
picking, move = self._create_picking(
|
|
self.outgoing_picking_type,
|
|
product,
|
|
self.stock_location,
|
|
self.customer_location
|
|
)
|
|
|
|
# Verify this is an outgoing operation
|
|
self.assertEqual(
|
|
picking.picking_type_id.code,
|
|
'outgoing',
|
|
"Picking should be an outgoing/delivery operation"
|
|
)
|
|
|
|
# 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,
|
|
self.outgoing_picking_type,
|
|
state='pass'
|
|
)
|
|
|
|
# PROPERTY VERIFICATION: Outgoing operations should not trigger preservation behavior
|
|
self.assertFalse(
|
|
quality_check._is_receipt_operation(),
|
|
"Outgoing/delivery operation should NOT be identified as receipt operation"
|
|
)
|
|
|
|
self.assertFalse(
|
|
quality_check._should_preserve_state(),
|
|
"State preservation should NOT be active for outgoing/delivery operations"
|
|
)
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
lot_name_seed=st.integers(min_value=1, max_value=1000000),
|
|
operation_type=st.sampled_from(['internal', 'outgoing']),
|
|
)
|
|
def test_property_non_receipt_operation_type_consistency(self, lot_name_seed, operation_type):
|
|
"""
|
|
Property: For any non-receipt operation, the operation type detection should be
|
|
consistent across multiple checks and throughout the quality check lifecycle.
|
|
|
|
Validates: Requirement 4.2
|
|
"""
|
|
# Create a product with lot tracking
|
|
product = self._create_product_with_tracking(f'Test Product {lot_name_seed}')
|
|
|
|
# Select the appropriate picking type
|
|
if operation_type == 'internal':
|
|
picking_type = self.internal_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.stock_location
|
|
else: # outgoing
|
|
picking_type = self.outgoing_picking_type
|
|
location_id = self.stock_location
|
|
location_dest_id = self.customer_location
|
|
|
|
# Create non-receipt picking
|
|
picking, move = self._create_picking(picking_type, product, location_id, location_dest_id)
|
|
|
|
# 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, picking_type, state='pass')
|
|
|
|
# PROPERTY VERIFICATION: Operation type detection should be consistent
|
|
|
|
# Check 1: Initial detection
|
|
is_receipt_1 = quality_check._is_receipt_operation()
|
|
self.assertFalse(is_receipt_1, f"Should not be receipt operation (check 1) for {operation_type}")
|
|
|
|
# Check 2: After invalidating cache
|
|
quality_check.invalidate_recordset()
|
|
is_receipt_2 = quality_check._is_receipt_operation()
|
|
self.assertFalse(is_receipt_2, f"Should not be receipt operation (check 2) for {operation_type}")
|
|
|
|
# Check 3: Consistency between checks
|
|
self.assertEqual(
|
|
is_receipt_1,
|
|
is_receipt_2,
|
|
"Operation type detection should be consistent across multiple checks"
|
|
)
|
|
|
|
# Check 4: _should_preserve_state should also be consistent
|
|
should_preserve_1 = quality_check._should_preserve_state()
|
|
quality_check.invalidate_recordset()
|
|
should_preserve_2 = quality_check._should_preserve_state()
|
|
|
|
self.assertFalse(should_preserve_1, "Should not preserve state for non-receipt operations")
|
|
self.assertFalse(should_preserve_2, "Should not preserve state for non-receipt operations")
|
|
self.assertEqual(
|
|
should_preserve_1,
|
|
should_preserve_2,
|
|
"State preservation detection should be consistent"
|
|
)
|