first commit
This commit is contained in:
commit
086eb52c83
153
README.md
Normal file
153
README.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Quality Check Lot Preserve
|
||||
|
||||
## Overview
|
||||
|
||||
This module modifies the standard Odoo 18 quality check behavior for receipt operations to preserve quality check states when lot numbers are assigned after quality checks have been completed.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
In standard Odoo, when a quality check is completed before a lot number is assigned, the quality check automatically resets to "Todo" state when the lot number is later assigned. This creates inefficiency in warehouse operations where:
|
||||
|
||||
1. Quality checks are performed immediately upon receipt
|
||||
2. Lot numbers are generated or assigned later in the process
|
||||
3. Quality checks must be re-done unnecessarily
|
||||
|
||||
## Solution
|
||||
|
||||
This module provides the following functionality:
|
||||
|
||||
### 1. State Preservation
|
||||
- Quality check states (Pass, Fail, In Progress) are preserved when lot numbers are assigned
|
||||
- Applies only to receipt operations (incoming shipments)
|
||||
- Other operation types (manufacturing, internal transfers, delivery) continue with standard Odoo behavior
|
||||
|
||||
### 2. Automatic Lot Number Synchronization
|
||||
- Lot numbers assigned to stock move lines are automatically copied to related quality checks
|
||||
- Lot number changes are synchronized in real-time
|
||||
- Maintains traceability between inventory and quality records
|
||||
|
||||
### 3. Selective Application
|
||||
- Custom behavior applies only to receipt operations
|
||||
- Standard Odoo workflows remain unchanged for other operation types
|
||||
- Seamless integration with existing quality control processes
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy this module to your Odoo addons directory (e.g., `customaddons/quality_check_lot_preserve`)
|
||||
2. Update the apps list in Odoo
|
||||
3. Install the "Quality Check Lot Preserve" module
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `stock` - Odoo Inventory Management
|
||||
- `quality_control` - Odoo Quality Control (Enterprise module)
|
||||
|
||||
**Note:** This module requires Odoo Enterprise edition as it depends on the `quality_control` module.
|
||||
|
||||
## Usage
|
||||
|
||||
### Typical Workflow
|
||||
|
||||
1. **Receive Products**: Create a purchase order and validate the receipt
|
||||
2. **Perform Quality Checks**: Complete quality checks immediately upon receipt (before lot assignment)
|
||||
3. **Assign Lot Numbers**: Generate or manually assign lot numbers to the stock move lines
|
||||
4. **Result**: Quality check states remain unchanged, and lot numbers are automatically copied to quality checks
|
||||
|
||||
### Example Scenario
|
||||
|
||||
**Before this module:**
|
||||
- Receive 100 units of Product A
|
||||
- Complete quality check → Status: "Pass"
|
||||
- Assign lot number "LOT001"
|
||||
- Quality check resets → Status: "Todo" ❌
|
||||
- Must re-do quality check
|
||||
|
||||
**With this module:**
|
||||
- Receive 100 units of Product A
|
||||
- Complete quality check → Status: "Pass"
|
||||
- Assign lot number "LOT001"
|
||||
- Quality check remains → Status: "Pass" ✓
|
||||
- Lot number automatically copied to quality check ✓
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Extended Models
|
||||
|
||||
#### stock.move.line
|
||||
- Monitors `lot_id` and `lot_name` field changes
|
||||
- Identifies related quality checks for receipt operations
|
||||
- Triggers quality check lot number updates
|
||||
|
||||
#### quality.check
|
||||
- Prevents state resets during lot assignment for receipt operations
|
||||
- Accepts lot number updates from stock move lines
|
||||
- Maintains standard behavior for non-receipt operations
|
||||
|
||||
### Operation Type Detection
|
||||
|
||||
The module identifies receipt operations by checking:
|
||||
- `picking_type_code == 'incoming'`
|
||||
- Fallback to `picking_id.picking_type_id.code`
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Stock Move Line (Lot Assigned)
|
||||
↓
|
||||
Detect Change (write method)
|
||||
↓
|
||||
Find Related Quality Checks
|
||||
↓
|
||||
Check Operation Type (Receipt?)
|
||||
↓ Yes
|
||||
Copy Lot Number + Preserve State
|
||||
↓
|
||||
Update Quality Check
|
||||
```
|
||||
|
||||
### Security and Access Rights
|
||||
|
||||
This module extends existing Odoo models (`stock.move.line` and `quality.check`) without creating new models or data structures. Therefore:
|
||||
|
||||
- **No new access rights are required**
|
||||
- Access rights are inherited from the base modules (`stock` and `quality_control`)
|
||||
- Users with existing quality control and inventory permissions can use this module
|
||||
- The `security/ir.model.access.csv` file is present but empty (only contains the header row)
|
||||
|
||||
The module respects all existing Odoo security rules and group permissions.
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required. The module works automatically once installed.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Odoo Version**: 18.0
|
||||
- **Edition**: Enterprise (requires quality_control module)
|
||||
- **Database**: PostgreSQL
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only applies to receipt operations (incoming shipments)
|
||||
- Requires the quality_control Enterprise module
|
||||
- Does not modify historical quality check records
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or feature requests, please contact your Odoo implementation partner or system administrator.
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3
|
||||
|
||||
## Author
|
||||
|
||||
Your Company
|
||||
|
||||
## Version History
|
||||
|
||||
### 1.0.0 (Initial Release)
|
||||
- State preservation for quality checks during lot assignment
|
||||
- Automatic lot number synchronization
|
||||
- Receipt operation filtering
|
||||
- Full integration with standard Odoo quality control workflows
|
||||
3
__init__.py
Normal file
3
__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
45
__manifest__.py
Normal file
45
__manifest__.py
Normal file
@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Quality Check Lot Preserve',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Inventory/Quality',
|
||||
'summary': 'Preserve quality check states when lot numbers are assigned during receipt operations',
|
||||
'description': """
|
||||
Quality Check Lot Preservation
|
||||
===============================
|
||||
|
||||
This module modifies the standard Odoo 18 quality check behavior for receipt operations.
|
||||
|
||||
Key Features:
|
||||
-------------
|
||||
* Preserves quality check states when lot numbers are assigned after quality checks are completed
|
||||
* Automatically copies lot numbers from stock move lines to related quality checks
|
||||
* Maintains synchronization when lot numbers are changed
|
||||
* Applies only to receipt operations, leaving other workflows unchanged
|
||||
|
||||
Use Case:
|
||||
---------
|
||||
In standard Odoo, when a quality check is completed before a lot number is assigned,
|
||||
the quality check resets to "Todo" state when the lot number is later assigned.
|
||||
This module prevents that reset and automatically updates the quality check with the lot number.
|
||||
|
||||
Technical Details:
|
||||
------------------
|
||||
* Extends stock.move.line to detect lot number changes
|
||||
* Extends quality.check to prevent state resets for receipt operations
|
||||
* Maintains full compatibility with standard Odoo quality control workflows
|
||||
""",
|
||||
'author': 'Your Company',
|
||||
'website': 'https://www.yourcompany.com',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'stock',
|
||||
'quality_control',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/__manifest__.cpython-312.pyc
Normal file
BIN
__pycache__/__manifest__.cpython-312.pyc
Normal file
Binary file not shown.
4
models/__init__.py
Normal file
4
models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock_move_line
|
||||
from . import quality_check
|
||||
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/quality_check.cpython-312.pyc
Normal file
BIN
models/__pycache__/quality_check.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/stock_move_line.cpython-312.pyc
Normal file
BIN
models/__pycache__/stock_move_line.cpython-312.pyc
Normal file
Binary file not shown.
197
models/quality_check.py
Normal file
197
models/quality_check.py
Normal file
@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, api, fields
|
||||
|
||||
|
||||
class QualityCheck(models.Model):
|
||||
_inherit = 'quality.check'
|
||||
|
||||
# Add a context flag to track when we're preserving state
|
||||
_preserve_state_context_key = 'quality_check_preserve_state'
|
||||
|
||||
def _is_receipt_operation(self):
|
||||
"""
|
||||
Check if the current quality check is part of a receipt operation.
|
||||
Receipt operations are identified by picking_type_code == 'incoming'.
|
||||
|
||||
This method implements robust operation type identification with multiple
|
||||
fallback paths to ensure reliable detection across different scenarios.
|
||||
|
||||
Returns:
|
||||
bool: True if this is a receipt operation, False otherwise
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Primary check: picking_type_code through the picking
|
||||
if self.picking_id and self.picking_id.picking_type_id:
|
||||
return self.picking_id.picking_type_id.code == 'incoming'
|
||||
|
||||
# Fallback 1: check through move_line_id if picking_id is not available
|
||||
if self.move_line_id:
|
||||
if self.move_line_id.picking_id and self.move_line_id.picking_id.picking_type_id:
|
||||
return self.move_line_id.picking_id.picking_type_id.code == 'incoming'
|
||||
|
||||
# Fallback 2: check through move_id.picking_type_id
|
||||
if self.move_line_id.move_id and self.move_line_id.move_id.picking_type_id:
|
||||
return self.move_line_id.move_id.picking_type_id.code == 'incoming'
|
||||
|
||||
# Fallback 3: check through move_id if available
|
||||
if hasattr(self, 'move_id') and self.move_id and self.move_id.picking_type_id:
|
||||
return self.move_id.picking_type_id.code == 'incoming'
|
||||
|
||||
# Default: not a receipt operation if we can't determine the type
|
||||
return False
|
||||
|
||||
def _should_preserve_state(self):
|
||||
"""
|
||||
Determine if the quality check state should be preserved during lot assignment.
|
||||
State preservation applies only to receipt operations.
|
||||
|
||||
Returns:
|
||||
bool: True if state should be preserved, False otherwise
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self._is_receipt_operation()
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Override write to prevent state reset when lot_id is updated on receipt operations.
|
||||
"""
|
||||
# Store current states before write for receipt operations
|
||||
state_data = {}
|
||||
for record in self:
|
||||
if record._should_preserve_state() and record.quality_state != 'none':
|
||||
# Only preserve if we're not explicitly changing the quality_state
|
||||
if 'quality_state' not in vals:
|
||||
state_data[record.id] = {
|
||||
'quality_state': record.quality_state,
|
||||
'user_id': record.user_id.id if record.user_id else False,
|
||||
'control_date': record.control_date,
|
||||
}
|
||||
|
||||
# Perform the write
|
||||
result = super(QualityCheck, self).write(vals)
|
||||
|
||||
# Restore states if they were changed
|
||||
if state_data:
|
||||
for record in self:
|
||||
if record.id in state_data:
|
||||
stored = state_data[record.id]
|
||||
if record.quality_state != stored['quality_state']:
|
||||
# Use SQL to restore state without triggering write again
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
UPDATE quality_check
|
||||
SET quality_state = %s, user_id = %s, control_date = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(stored['quality_state'], stored['user_id'],
|
||||
stored['control_date'], record.id)
|
||||
)
|
||||
record.invalidate_recordset(['quality_state', 'user_id', 'control_date'])
|
||||
|
||||
return result
|
||||
|
||||
@api.depends('move_line_id.lot_id')
|
||||
def _compute_lot_line_id(self):
|
||||
"""
|
||||
Override the compute method to preserve quality check state during lot updates.
|
||||
This prevents the state from being reset when lot numbers are assigned.
|
||||
"""
|
||||
for qc in self:
|
||||
# Always update lot_line_id
|
||||
qc.lot_line_id = qc.move_line_id.lot_id
|
||||
|
||||
# Check if we should update lot_id
|
||||
if qc.lot_line_id and qc._update_lot_from_lot_line():
|
||||
# For receipt operations, preserve the state
|
||||
if qc._should_preserve_state():
|
||||
# Store current state before updating lot_id
|
||||
current_state = qc.quality_state
|
||||
|
||||
# Directly update lot_id field in database without triggering ORM
|
||||
if current_state != 'none':
|
||||
# Use SQL to update lot_id while preserving state
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
UPDATE quality_check
|
||||
SET lot_id = %s
|
||||
WHERE id = %s AND quality_state = %s
|
||||
""",
|
||||
(qc.lot_line_id.id if qc.lot_line_id else None, qc.id, current_state)
|
||||
)
|
||||
# Invalidate cache for lot_id only
|
||||
qc.invalidate_recordset(['lot_id'])
|
||||
else:
|
||||
# If state is 'none', use normal assignment
|
||||
qc.lot_id = qc.lot_line_id
|
||||
else:
|
||||
# For non-receipt operations, use standard behavior
|
||||
qc.lot_id = qc.lot_line_id
|
||||
|
||||
def _update_lot_from_lot_line(self):
|
||||
"""
|
||||
Override the standard method from quality_control module.
|
||||
This method is called by _compute_lot_line_id to determine if the lot_id
|
||||
should be automatically updated from the move_line_id.lot_id.
|
||||
|
||||
For receipt operations, we return True to allow the update, but we handle
|
||||
state preservation in the overridden _compute_lot_line_id method.
|
||||
|
||||
For non-receipt operations, we preserve standard Odoo behavior.
|
||||
|
||||
Returns:
|
||||
bool: True to allow lot update (state preservation handled separately)
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Always return True to allow lot updates
|
||||
# State preservation is handled in _compute_lot_line_id
|
||||
return super(QualityCheck, self)._update_lot_from_lot_line()
|
||||
|
||||
def _check_to_unlink(self):
|
||||
"""
|
||||
Override to prevent deletion of completed quality checks on receipt operations.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# For receipt operations with completed quality checks, prevent deletion
|
||||
if self._should_preserve_state() and self.quality_state != 'none':
|
||||
return False
|
||||
|
||||
# For other cases, use standard behavior
|
||||
return super(QualityCheck, self)._check_to_unlink()
|
||||
|
||||
def _update_lot_from_move_line_manual(self, lot_id):
|
||||
"""
|
||||
Manually update the lot_id field while preserving the current quality check state.
|
||||
This method is called from stock.move.line when a lot number is assigned or changed
|
||||
on receipt operations.
|
||||
|
||||
Args:
|
||||
lot_id: The ID of the lot to assign (or False to clear)
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Store the current quality state before updating lot_id
|
||||
current_state = self.quality_state
|
||||
current_user = self.user_id
|
||||
current_control_date = self.control_date
|
||||
|
||||
# Use context flag to preserve state during write
|
||||
self.with_context(quality_check_preserve_state=True).sudo().write({'lot_id': lot_id})
|
||||
|
||||
# Force restore the quality state if it was changed during the write
|
||||
if current_state != 'none':
|
||||
# Use direct SQL update to avoid triggering any compute methods
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
UPDATE quality_check
|
||||
SET quality_state = %s, user_id = %s, control_date = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(current_state, current_user.id if current_user else None,
|
||||
current_control_date, self.id)
|
||||
)
|
||||
# Invalidate cache to ensure the updated values are reflected
|
||||
self.invalidate_recordset(['quality_state', 'user_id', 'control_date'])
|
||||
138
models/stock_move_line.py
Normal file
138
models/stock_move_line.py
Normal file
@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = 'stock.move.line'
|
||||
|
||||
def _create_check(self):
|
||||
"""
|
||||
Override to prevent creating duplicate quality checks when move lines are recreated.
|
||||
For receipt operations with existing completed quality checks, relink them instead.
|
||||
"""
|
||||
# Check each move line
|
||||
move_lines_to_process = self.env['stock.move.line']
|
||||
|
||||
for ml in self:
|
||||
if ml._is_receipt_operation() and ml.picking_id:
|
||||
# Search for existing quality checks (including orphaned ones)
|
||||
existing_checks = self.env['quality.check'].search([
|
||||
('picking_id', '=', ml.picking_id.id),
|
||||
('product_id', '=', ml.product_id.id),
|
||||
('quality_state', '!=', 'none'),
|
||||
'|',
|
||||
('move_line_id', '=', False),
|
||||
('move_line_id', '=', ml.id)
|
||||
])
|
||||
|
||||
# If there are completed quality checks, relink them instead of creating new ones
|
||||
if existing_checks:
|
||||
# Relink to this move line
|
||||
existing_checks.write({'move_line_id': ml.id})
|
||||
# Skip creating new checks for this move line
|
||||
continue
|
||||
|
||||
# Add to list for standard processing
|
||||
move_lines_to_process |= ml
|
||||
|
||||
# Call parent method only for move lines that need new checks
|
||||
if move_lines_to_process:
|
||||
return super(StockMoveLine, move_lines_to_process)._create_check()
|
||||
|
||||
return self.env['quality.check']
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Override write method to detect lot_id and lot_name changes.
|
||||
When a lot number is assigned or changed on a receipt operation,
|
||||
update related quality checks with the new lot number.
|
||||
"""
|
||||
# Call parent write first to ensure the changes are saved
|
||||
result = super(StockMoveLine, self).write(vals)
|
||||
|
||||
# Check if lot_id or lot_name was changed
|
||||
if 'lot_id' in vals or 'lot_name' in vals:
|
||||
# Process each move line that was updated
|
||||
for move_line in self:
|
||||
# Check if this is a receipt operation
|
||||
if move_line._is_receipt_operation():
|
||||
# Get the lot_id to propagate to quality checks
|
||||
lot_id = move_line.lot_id.id if move_line.lot_id else False
|
||||
# Update related quality checks
|
||||
move_line._update_quality_check_lot(lot_id)
|
||||
|
||||
return result
|
||||
|
||||
def _is_receipt_operation(self):
|
||||
"""
|
||||
Check if the current move line is part of a receipt operation.
|
||||
Receipt operations are identified by picking_type_code == 'incoming'.
|
||||
|
||||
This method implements robust operation type identification with multiple
|
||||
fallback paths to ensure reliable detection across different scenarios.
|
||||
|
||||
Returns:
|
||||
bool: True if this is a receipt operation, False otherwise
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Primary check: picking_type_code through the picking
|
||||
if self.picking_id and self.picking_id.picking_type_id:
|
||||
return self.picking_id.picking_type_id.code == 'incoming'
|
||||
|
||||
# Fallback 1: check through move_id.picking_type_id
|
||||
if self.move_id and self.move_id.picking_type_id:
|
||||
return self.move_id.picking_type_id.code == 'incoming'
|
||||
|
||||
# Fallback 2: check through move_id.picking_id (in case picking_type_id is not directly on move)
|
||||
if self.move_id and self.move_id.picking_id and self.move_id.picking_id.picking_type_id:
|
||||
return self.move_id.picking_id.picking_type_id.code == 'incoming'
|
||||
|
||||
# Default: not a receipt operation if we can't determine the type
|
||||
return False
|
||||
|
||||
def _update_quality_check_lot(self, lot_id):
|
||||
"""
|
||||
Update related quality checks with the new lot number.
|
||||
This method searches for quality checks linked to this move line
|
||||
and updates their lot_id field without triggering state reset.
|
||||
|
||||
Also handles orphaned quality checks (where move_line_id was cleared).
|
||||
|
||||
Args:
|
||||
lot_id: The ID of the lot to assign to quality checks (or False)
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
QualityCheck = self.env['quality.check']
|
||||
|
||||
# Search for quality checks related to this move line
|
||||
quality_checks = QualityCheck.search([
|
||||
('move_line_id', '=', self.id)
|
||||
])
|
||||
|
||||
# Also search for orphaned quality checks from the same picking
|
||||
# that might have been unlinked from their move_line
|
||||
if self.picking_id:
|
||||
orphaned_checks = QualityCheck.search([
|
||||
('picking_id', '=', self.picking_id.id),
|
||||
('move_line_id', '=', False),
|
||||
('product_id', '=', self.product_id.id),
|
||||
('quality_state', '!=', 'none')
|
||||
])
|
||||
|
||||
# Relink orphaned checks to this move line
|
||||
if orphaned_checks:
|
||||
orphaned_checks.write({'move_line_id': self.id})
|
||||
quality_checks |= orphaned_checks
|
||||
|
||||
# Filter to ensure we only update quality checks for receipt operations
|
||||
receipt_quality_checks = quality_checks.filtered(
|
||||
lambda qc: qc._is_receipt_operation()
|
||||
)
|
||||
|
||||
# Update the lot_id on each quality check without triggering state reset
|
||||
for quality_check in receipt_quality_checks:
|
||||
# Use the quality check's own method to update lot without state reset
|
||||
quality_check._update_lot_from_move_line_manual(lot_id)
|
||||
1
security/ir.model.access.csv
Normal file
1
security/ir.model.access.csv
Normal file
@ -0,0 +1 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
11
tests/__init__.py
Normal file
11
tests/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_basic_functionality
|
||||
from . import test_property_lot_propagation
|
||||
from . import test_property_lot_update
|
||||
from . import test_property_state_preservation
|
||||
from . import test_property_relationship_integrity
|
||||
from . import test_property_receipt_operation
|
||||
from . import test_property_non_receipt_operation
|
||||
from . import test_edge_cases
|
||||
from . import test_integration
|
||||
BIN
tests/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
tests/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_basic_functionality.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_basic_functionality.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_edge_cases.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_edge_cases.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_integration.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_integration.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_property_lot_propagation.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_property_lot_propagation.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_property_lot_update.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_property_lot_update.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
42
tests/test_basic_functionality.py
Normal file
42
tests/test_basic_functionality.py
Normal file
@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestBasicFunctionality(TransactionCase):
|
||||
"""Basic smoke tests to verify the module loads and core methods exist"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestBasicFunctionality, self).setUp()
|
||||
self.StockMoveLine = self.env['stock.move.line']
|
||||
self.QualityCheck = self.env['quality.check']
|
||||
|
||||
def test_stock_move_line_model_exists(self):
|
||||
"""Test that stock.move.line model can be accessed"""
|
||||
self.assertIsNotNone(self.StockMoveLine, "stock.move.line model should be accessible")
|
||||
self.assertEqual(self.StockMoveLine._name, 'stock.move.line')
|
||||
|
||||
def test_quality_check_model_exists(self):
|
||||
"""Test that quality.check model can be accessed"""
|
||||
self.assertIsNotNone(self.QualityCheck, "quality.check model should be accessible")
|
||||
self.assertEqual(self.QualityCheck._name, 'quality.check')
|
||||
|
||||
def test_stock_move_line_has_custom_methods(self):
|
||||
"""Test that custom methods exist on stock.move.line"""
|
||||
# Create a dummy move line to test methods exist
|
||||
move_line = self.StockMoveLine.new({})
|
||||
self.assertTrue(hasattr(move_line, '_is_receipt_operation'),
|
||||
"stock.move.line should have _is_receipt_operation method")
|
||||
self.assertTrue(hasattr(move_line, '_update_quality_check_lot'),
|
||||
"stock.move.line should have _update_quality_check_lot method")
|
||||
|
||||
def test_quality_check_has_custom_methods(self):
|
||||
"""Test that custom methods exist on quality.check"""
|
||||
# Create a dummy quality check to test methods exist
|
||||
quality_check = self.QualityCheck.new({})
|
||||
self.assertTrue(hasattr(quality_check, '_is_receipt_operation'),
|
||||
"quality.check should have _is_receipt_operation method")
|
||||
self.assertTrue(hasattr(quality_check, '_should_preserve_state'),
|
||||
"quality.check should have _should_preserve_state method")
|
||||
self.assertTrue(hasattr(quality_check, '_update_lot_from_move_line_manual'),
|
||||
"quality.check should have _update_lot_from_move_line_manual method")
|
||||
399
tests/test_edge_cases.py
Normal file
399
tests/test_edge_cases.py
Normal file
@ -0,0 +1,399 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestEdgeCases(TransactionCase):
|
||||
"""Unit tests for edge cases in quality check lot preservation"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestEdgeCases, self).setUp()
|
||||
|
||||
# Get model references
|
||||
self.StockMoveLine = self.env['stock.move.line']
|
||||
self.QualityCheck = self.env['quality.check']
|
||||
self.Product = self.env['product.product']
|
||||
self.StockLocation = self.env['stock.location']
|
||||
self.StockPicking = self.env['stock.picking']
|
||||
self.StockPickingType = self.env['stock.picking.type']
|
||||
self.StockMove = self.env['stock.move']
|
||||
self.StockLot = self.env['stock.lot']
|
||||
self.QualityPoint = self.env['quality.point']
|
||||
|
||||
# Create test product
|
||||
self.product = self.Product.create({
|
||||
'name': 'Test Product',
|
||||
'type': 'consu', # Use 'consu' for consumable/storable product
|
||||
'tracking': 'lot',
|
||||
})
|
||||
|
||||
# Get or create locations
|
||||
self.location_supplier = self.env.ref('stock.stock_location_suppliers')
|
||||
self.location_stock = self.env.ref('stock.stock_location_stock')
|
||||
self.location_customer = self.env.ref('stock.stock_location_customers')
|
||||
|
||||
# Get picking types
|
||||
self.picking_type_in = self.env.ref('stock.picking_type_in')
|
||||
self.picking_type_out = self.env.ref('stock.picking_type_out')
|
||||
self.picking_type_internal = self.env.ref('stock.picking_type_internal')
|
||||
|
||||
# Try to get manufacturing picking type if available
|
||||
try:
|
||||
self.picking_type_mrp = self.env.ref('mrp.picking_type_manufacturing')
|
||||
except ValueError:
|
||||
# Create a mock manufacturing picking type if MRP is not installed
|
||||
warehouse = self.picking_type_in.warehouse_id
|
||||
self.picking_type_mrp = self.StockPickingType.create({
|
||||
'name': 'Manufacturing',
|
||||
'code': 'mrp_operation',
|
||||
'sequence_code': 'MRP',
|
||||
'warehouse_id': warehouse.id,
|
||||
'default_location_src_id': warehouse.lot_stock_id.id,
|
||||
'default_location_dest_id': warehouse.lot_stock_id.id,
|
||||
})
|
||||
|
||||
# Get or create a quality team
|
||||
self.quality_team = self.env['quality.alert.team'].search([], limit=1)
|
||||
if not self.quality_team:
|
||||
self.quality_team = self.env['quality.alert.team'].create({
|
||||
'name': 'Test Quality Team',
|
||||
})
|
||||
|
||||
# Get test type
|
||||
self.test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False)
|
||||
if not self.test_type:
|
||||
self.test_type = self.env['quality.point.test_type'].create({
|
||||
'name': 'Pass/Fail Test',
|
||||
'technical_name': 'passfail',
|
||||
})
|
||||
|
||||
# Create quality point for the product
|
||||
self.quality_point = self.QualityPoint.create({
|
||||
'product_ids': [(4, self.product.id)],
|
||||
'picking_type_ids': [(4, self.picking_type_in.id)],
|
||||
'test_type_id': self.test_type.id,
|
||||
'team_id': self.quality_team.id,
|
||||
})
|
||||
|
||||
def _create_receipt_picking(self):
|
||||
"""Helper to create a receipt picking"""
|
||||
return self.StockPicking.create({
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
'location_id': self.location_supplier.id,
|
||||
'location_dest_id': self.location_stock.id,
|
||||
})
|
||||
|
||||
def _create_stock_move(self, picking):
|
||||
"""Helper to create a stock move"""
|
||||
return self.StockMove.create({
|
||||
'name': 'Test Move',
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'location_id': picking.location_id.id,
|
||||
'location_dest_id': picking.location_dest_id.id,
|
||||
})
|
||||
|
||||
def _create_move_line(self, move, lot_id=False):
|
||||
"""Helper to create a stock move line"""
|
||||
return self.StockMoveLine.create({
|
||||
'move_id': move.id,
|
||||
'product_id': self.product.id,
|
||||
'product_uom_id': self.product.uom_id.id,
|
||||
'location_id': move.location_id.id,
|
||||
'location_dest_id': move.location_dest_id.id,
|
||||
'picking_id': move.picking_id.id,
|
||||
'lot_id': lot_id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
|
||||
def _create_lot(self, name):
|
||||
"""Helper to create a lot"""
|
||||
return self.StockLot.create({
|
||||
'name': name,
|
||||
'product_id': self.product.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
# Subtask 7.1: Test missing quality check scenario
|
||||
def test_lot_assignment_without_quality_check(self):
|
||||
"""
|
||||
Test lot assignment when no quality check exists.
|
||||
Verify no errors occur and standard behavior continues.
|
||||
Requirements: 2.1
|
||||
"""
|
||||
# Create receipt picking and move
|
||||
picking = self._create_receipt_picking()
|
||||
move = self._create_stock_move(picking)
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Verify no quality check exists
|
||||
quality_checks = self.QualityCheck.search([
|
||||
('move_line_id', '=', move_line.id)
|
||||
])
|
||||
self.assertEqual(len(quality_checks), 0,
|
||||
"No quality check should exist initially")
|
||||
|
||||
# Assign lot number - should not raise error
|
||||
lot = self._create_lot('LOT-NO-QC-001')
|
||||
try:
|
||||
move_line.write({'lot_id': lot.id})
|
||||
success = True
|
||||
except Exception as e:
|
||||
success = False
|
||||
error_msg = str(e)
|
||||
|
||||
self.assertTrue(success,
|
||||
"Lot assignment should succeed even without quality check")
|
||||
self.assertEqual(move_line.lot_id.id, lot.id,
|
||||
"Lot should be assigned to move line")
|
||||
|
||||
# Subtask 7.2: Test multiple quality checks scenario
|
||||
def test_multiple_quality_checks_update(self):
|
||||
"""
|
||||
Test that multiple quality checks for same move line are all updated.
|
||||
Verify unrelated quality checks are not affected.
|
||||
Requirements: 2.3
|
||||
"""
|
||||
# Create receipt picking and move
|
||||
picking = self._create_receipt_picking()
|
||||
move = self._create_stock_move(picking)
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Create multiple quality checks for the same move line
|
||||
qc1 = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': picking.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type': 'passfail',
|
||||
'quality_state': 'pass',
|
||||
})
|
||||
|
||||
qc2 = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': picking.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type': 'passfail',
|
||||
'quality_state': 'fail',
|
||||
})
|
||||
|
||||
# Create an unrelated quality check (different move line)
|
||||
other_move_line = self._create_move_line(move)
|
||||
qc_unrelated = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': picking.id,
|
||||
'move_line_id': other_move_line.id,
|
||||
'test_type': 'passfail',
|
||||
'quality_state': 'none',
|
||||
})
|
||||
|
||||
# Assign lot number to first move line
|
||||
lot = self._create_lot('LOT-MULTI-001')
|
||||
move_line.write({'lot_id': lot.id})
|
||||
|
||||
# Verify both related quality checks are updated
|
||||
self.assertEqual(qc1.lot_id.id, lot.id,
|
||||
"First quality check should have lot assigned")
|
||||
self.assertEqual(qc2.lot_id.id, lot.id,
|
||||
"Second quality check should have lot assigned")
|
||||
|
||||
# Verify states are preserved
|
||||
self.assertEqual(qc1.quality_state, 'pass',
|
||||
"First quality check state should be preserved")
|
||||
self.assertEqual(qc2.quality_state, 'fail',
|
||||
"Second quality check state should be preserved")
|
||||
|
||||
# Verify unrelated quality check is not affected
|
||||
self.assertFalse(qc_unrelated.lot_id,
|
||||
"Unrelated quality check should not have lot assigned")
|
||||
|
||||
# Subtask 7.3: Test quality check without move line
|
||||
def test_quality_check_without_move_line(self):
|
||||
"""
|
||||
Test quality check without move_line_id.
|
||||
Verify no automatic updates occur.
|
||||
Requirements: 3.3
|
||||
"""
|
||||
# Create receipt picking
|
||||
picking = self._create_receipt_picking()
|
||||
|
||||
# Create quality check without move_line_id
|
||||
qc = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': picking.id,
|
||||
'test_type': 'passfail',
|
||||
'quality_state': 'pass',
|
||||
})
|
||||
|
||||
# Create a move line with lot and assign it
|
||||
move = self._create_stock_move(picking)
|
||||
lot = self._create_lot('LOT-NO-LINK-001')
|
||||
move_line = self._create_move_line(move, lot_id=lot.id)
|
||||
|
||||
# Verify quality check is not automatically updated
|
||||
self.assertFalse(qc.lot_id,
|
||||
"Quality check without move_line_id should not be auto-updated")
|
||||
self.assertEqual(qc.quality_state, 'pass',
|
||||
"Quality check state should remain unchanged")
|
||||
|
||||
# Subtask 7.4: Test each quality check state preservation
|
||||
def test_pass_state_preservation(self):
|
||||
"""
|
||||
Test "Pass" state preservation during lot assignment.
|
||||
Requirements: 1.2
|
||||
"""
|
||||
picking = self._create_receipt_picking()
|
||||
move = self._create_stock_move(picking)
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
qc = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': picking.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type': 'passfail',
|
||||
'quality_state': 'pass',
|
||||
})
|
||||
|
||||
lot = self._create_lot('LOT-PASS-001')
|
||||
move_line.write({'lot_id': lot.id})
|
||||
|
||||
self.assertEqual(qc.quality_state, 'pass',
|
||||
"Pass state should be preserved")
|
||||
self.assertEqual(qc.lot_id.id, lot.id,
|
||||
"Lot should be assigned")
|
||||
|
||||
def test_fail_state_preservation(self):
|
||||
"""
|
||||
Test "Fail" state preservation during lot assignment.
|
||||
Requirements: 1.2
|
||||
"""
|
||||
picking = self._create_receipt_picking()
|
||||
move = self._create_stock_move(picking)
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
qc = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': picking.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type': 'passfail',
|
||||
'quality_state': 'fail',
|
||||
})
|
||||
|
||||
lot = self._create_lot('LOT-FAIL-001')
|
||||
move_line.write({'lot_id': lot.id})
|
||||
|
||||
self.assertEqual(qc.quality_state, 'fail',
|
||||
"Fail state should be preserved")
|
||||
self.assertEqual(qc.lot_id.id, lot.id,
|
||||
"Lot should be assigned")
|
||||
|
||||
def test_none_state_preservation(self):
|
||||
"""
|
||||
Test "In Progress" (none) state preservation during lot assignment.
|
||||
Requirements: 1.2
|
||||
"""
|
||||
picking = self._create_receipt_picking()
|
||||
move = self._create_stock_move(picking)
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
qc = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': picking.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type': 'passfail',
|
||||
'quality_state': 'none',
|
||||
})
|
||||
|
||||
lot = self._create_lot('LOT-NONE-001')
|
||||
move_line.write({'lot_id': lot.id})
|
||||
|
||||
self.assertEqual(qc.quality_state, 'none',
|
||||
"None/In Progress state should be preserved")
|
||||
self.assertEqual(qc.lot_id.id, lot.id,
|
||||
"Lot should be assigned")
|
||||
|
||||
# Subtask 7.5: Test different operation types
|
||||
def test_manufacturing_operation_standard_behavior(self):
|
||||
"""
|
||||
Test manufacturing operation uses standard behavior.
|
||||
Requirements: 4.2
|
||||
"""
|
||||
# Create manufacturing picking
|
||||
picking = self.StockPicking.create({
|
||||
'picking_type_id': self.picking_type_mrp.id,
|
||||
'location_id': self.location_stock.id,
|
||||
'location_dest_id': self.location_stock.id,
|
||||
})
|
||||
|
||||
move = self.StockMove.create({
|
||||
'name': 'Test Move',
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'location_id': picking.location_id.id,
|
||||
'location_dest_id': picking.location_dest_id.id,
|
||||
})
|
||||
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Verify it's not a receipt operation
|
||||
self.assertFalse(move_line._is_receipt_operation(),
|
||||
"Manufacturing operation should not be receipt")
|
||||
|
||||
def test_internal_transfer_standard_behavior(self):
|
||||
"""
|
||||
Test internal transfer uses standard behavior.
|
||||
Requirements: 4.2
|
||||
"""
|
||||
# Create internal transfer picking
|
||||
picking = self.StockPicking.create({
|
||||
'picking_type_id': self.picking_type_internal.id,
|
||||
'location_id': self.location_stock.id,
|
||||
'location_dest_id': self.location_stock.id,
|
||||
})
|
||||
|
||||
move = self.StockMove.create({
|
||||
'name': 'Test Move',
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'location_id': picking.location_id.id,
|
||||
'location_dest_id': picking.location_dest_id.id,
|
||||
})
|
||||
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Verify it's not a receipt operation
|
||||
self.assertFalse(move_line._is_receipt_operation(),
|
||||
"Internal transfer should not be receipt")
|
||||
|
||||
def test_delivery_operation_standard_behavior(self):
|
||||
"""
|
||||
Test delivery operation uses standard behavior.
|
||||
Requirements: 4.2
|
||||
"""
|
||||
# Create delivery picking
|
||||
picking = self.StockPicking.create({
|
||||
'picking_type_id': self.picking_type_out.id,
|
||||
'location_id': self.location_stock.id,
|
||||
'location_dest_id': self.location_customer.id,
|
||||
})
|
||||
|
||||
move = self.StockMove.create({
|
||||
'name': 'Test Move',
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'location_id': picking.location_id.id,
|
||||
'location_dest_id': picking.location_dest_id.id,
|
||||
})
|
||||
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Verify it's not a receipt operation
|
||||
self.assertFalse(move_line._is_receipt_operation(),
|
||||
"Delivery operation should not be receipt")
|
||||
305
tests/test_integration.py
Normal file
305
tests/test_integration.py
Normal file
@ -0,0 +1,305 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestIntegration(TransactionCase):
|
||||
"""Integration tests for quality check lot preservation end-to-end workflows"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestIntegration, self).setUp()
|
||||
|
||||
# Get model references
|
||||
self.StockMoveLine = self.env['stock.move.line']
|
||||
self.QualityCheck = self.env['quality.check']
|
||||
self.Product = self.env['product.product']
|
||||
self.StockLocation = self.env['stock.location']
|
||||
self.StockPicking = self.env['stock.picking']
|
||||
self.StockPickingType = self.env['stock.picking.type']
|
||||
self.StockMove = self.env['stock.move']
|
||||
self.StockLot = self.env['stock.lot']
|
||||
self.QualityPoint = self.env['quality.point']
|
||||
self.Partner = self.env['res.partner']
|
||||
|
||||
# Create test product
|
||||
self.product = self.Product.create({
|
||||
'name': 'Test Product for Integration',
|
||||
'type': 'consu',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
|
||||
# Get or create locations
|
||||
self.location_supplier = self.env.ref('stock.stock_location_suppliers')
|
||||
self.location_stock = self.env.ref('stock.stock_location_stock')
|
||||
|
||||
# Get picking types
|
||||
self.picking_type_in = self.env.ref('stock.picking_type_in')
|
||||
|
||||
# Create test supplier
|
||||
self.supplier = self.Partner.create({
|
||||
'name': 'Test Supplier',
|
||||
})
|
||||
|
||||
# Get or create a quality team
|
||||
self.quality_team = self.env['quality.alert.team'].search([], limit=1)
|
||||
if not self.quality_team:
|
||||
self.quality_team = self.env['quality.alert.team'].create({
|
||||
'name': 'Test Quality Team',
|
||||
})
|
||||
|
||||
# Get test type
|
||||
self.test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False)
|
||||
if not self.test_type:
|
||||
self.test_type = self.env['quality.point.test_type'].create({
|
||||
'name': 'Pass/Fail Test',
|
||||
'technical_name': 'passfail',
|
||||
})
|
||||
|
||||
# Create quality point for the product
|
||||
self.quality_point = self.QualityPoint.create({
|
||||
'product_ids': [(4, self.product.id)],
|
||||
'picking_type_ids': [(4, self.picking_type_in.id)],
|
||||
'test_type_id': self.test_type.id,
|
||||
'team_id': self.quality_team.id,
|
||||
})
|
||||
|
||||
def _create_receipt_picking(self):
|
||||
"""Helper to create a receipt picking"""
|
||||
return self.StockPicking.create({
|
||||
'picking_type_id': self.picking_type_in.id,
|
||||
'location_id': self.location_supplier.id,
|
||||
'location_dest_id': self.location_stock.id,
|
||||
'partner_id': self.supplier.id,
|
||||
})
|
||||
|
||||
def _create_stock_move(self, picking, qty=1.0):
|
||||
"""Helper to create a stock move"""
|
||||
return self.StockMove.create({
|
||||
'name': 'Test Move',
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': qty,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'location_id': picking.location_id.id,
|
||||
'location_dest_id': picking.location_dest_id.id,
|
||||
})
|
||||
|
||||
def _create_move_line(self, move, lot_id=False):
|
||||
"""Helper to create a stock move line"""
|
||||
return self.StockMoveLine.create({
|
||||
'move_id': move.id,
|
||||
'product_id': self.product.id,
|
||||
'product_uom_id': self.product.uom_id.id,
|
||||
'location_id': move.location_id.id,
|
||||
'location_dest_id': move.location_dest_id.id,
|
||||
'picking_id': move.picking_id.id,
|
||||
'lot_id': lot_id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
|
||||
def _create_lot(self, name):
|
||||
"""Helper to create a lot"""
|
||||
return self.StockLot.create({
|
||||
'name': name,
|
||||
'product_id': self.product.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
# Subtask 8.1: Create end-to-end receipt workflow test
|
||||
def test_end_to_end_receipt_workflow(self):
|
||||
"""
|
||||
End-to-end test: Create receipt, generate quality checks,
|
||||
complete quality checks before lot assignment, assign lot numbers,
|
||||
verify quality check states are preserved and lot numbers are copied.
|
||||
Requirements: 1.1, 2.1, 3.1
|
||||
"""
|
||||
# Step 1: Create receipt picking
|
||||
receipt = self._create_receipt_picking()
|
||||
self.assertEqual(receipt.picking_type_id.code, 'incoming',
|
||||
"Picking should be a receipt operation")
|
||||
|
||||
# Create stock move
|
||||
move = self._create_stock_move(receipt, qty=5.0)
|
||||
|
||||
# Step 2: Confirm the picking and create move line
|
||||
receipt.action_confirm()
|
||||
|
||||
# Create move line
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Create quality check manually
|
||||
quality_check = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': receipt.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type_id': self.test_type.id,
|
||||
'quality_state': 'none',
|
||||
'team_id': self.quality_team.id,
|
||||
})
|
||||
|
||||
# Step 3: Complete quality checks before lot assignment
|
||||
initial_state = 'pass'
|
||||
quality_check.write({'quality_state': initial_state})
|
||||
|
||||
self.assertEqual(quality_check.quality_state, initial_state,
|
||||
"Quality check should be marked as pass")
|
||||
self.assertFalse(quality_check.lot_id,
|
||||
"Quality check should not have lot assigned yet")
|
||||
self.assertFalse(move_line.lot_id,
|
||||
"Move line should not have lot assigned yet")
|
||||
|
||||
# Step 4: Assign lot numbers to stock move lines
|
||||
lot = self._create_lot('LOT-E2E-001')
|
||||
move_line.write({'lot_id': lot.id})
|
||||
|
||||
# Step 5: Verify quality check states are preserved
|
||||
self.assertEqual(quality_check.quality_state, initial_state,
|
||||
"Quality check state should be preserved after lot assignment")
|
||||
|
||||
# Step 6: Verify lot numbers are copied to quality checks
|
||||
self.assertEqual(quality_check.lot_id.id, lot.id,
|
||||
"Lot number should be copied to quality check")
|
||||
|
||||
# Additional verification: Ensure the relationship is intact
|
||||
self.assertEqual(quality_check.move_line_id.id, move_line.id,
|
||||
"Quality check should still be linked to move line")
|
||||
|
||||
def test_end_to_end_receipt_workflow_with_fail_state(self):
|
||||
"""
|
||||
End-to-end test with quality check in 'fail' state.
|
||||
Verify fail state is preserved after lot assignment.
|
||||
Requirements: 1.1, 1.2, 2.1
|
||||
"""
|
||||
# Create receipt picking
|
||||
receipt = self._create_receipt_picking()
|
||||
move = self._create_stock_move(receipt, qty=3.0)
|
||||
receipt.action_confirm()
|
||||
|
||||
# Create move line
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Create quality check and mark as fail
|
||||
quality_check = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': receipt.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type_id': self.test_type.id,
|
||||
'quality_state': 'fail',
|
||||
'team_id': self.quality_team.id,
|
||||
})
|
||||
|
||||
# Assign lot number
|
||||
lot = self._create_lot('LOT-E2E-FAIL-001')
|
||||
move_line.write({'lot_id': lot.id})
|
||||
|
||||
# Verify fail state is preserved
|
||||
self.assertEqual(quality_check.quality_state, 'fail',
|
||||
"Fail state should be preserved after lot assignment")
|
||||
self.assertEqual(quality_check.lot_id.id, lot.id,
|
||||
"Lot should be assigned to quality check")
|
||||
|
||||
# Subtask 8.2: Create lot number change workflow test
|
||||
def test_lot_number_change_workflow(self):
|
||||
"""
|
||||
Test lot number change workflow: Create receipt with quality check and initial lot,
|
||||
change lot number on stock move line, verify quality check is updated with new lot,
|
||||
verify quality check state remains unchanged.
|
||||
Requirements: 3.1, 3.2
|
||||
"""
|
||||
# Step 1: Create receipt with quality check and initial lot number
|
||||
receipt = self._create_receipt_picking()
|
||||
move = self._create_stock_move(receipt, qty=2.0)
|
||||
receipt.action_confirm()
|
||||
|
||||
# Create move line
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Create initial lot and assign it
|
||||
initial_lot = self._create_lot('LOT-CHANGE-INITIAL')
|
||||
move_line.write({'lot_id': initial_lot.id})
|
||||
|
||||
# Create quality check with pass state
|
||||
quality_check = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': receipt.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type_id': self.test_type.id,
|
||||
'quality_state': 'pass',
|
||||
'lot_id': initial_lot.id,
|
||||
'team_id': self.quality_team.id,
|
||||
})
|
||||
|
||||
initial_state = quality_check.quality_state
|
||||
self.assertEqual(initial_state, 'pass',
|
||||
"Quality check should be in pass state")
|
||||
self.assertEqual(quality_check.lot_id.id, initial_lot.id,
|
||||
"Quality check should have initial lot")
|
||||
|
||||
# Step 2: Change lot number on stock move line
|
||||
new_lot = self._create_lot('LOT-CHANGE-NEW')
|
||||
move_line.write({'lot_id': new_lot.id})
|
||||
|
||||
# Step 3: Verify quality check is updated with new lot number
|
||||
self.assertEqual(quality_check.lot_id.id, new_lot.id,
|
||||
"Quality check should be updated with new lot number")
|
||||
|
||||
# Step 4: Verify quality check state remains unchanged
|
||||
self.assertEqual(quality_check.quality_state, initial_state,
|
||||
"Quality check state should remain unchanged after lot change")
|
||||
|
||||
# Additional verification: Ensure the relationship is intact
|
||||
self.assertEqual(quality_check.move_line_id.id, move_line.id,
|
||||
"Quality check should still be linked to move line")
|
||||
|
||||
def test_lot_number_change_workflow_multiple_changes(self):
|
||||
"""
|
||||
Test multiple lot number changes in sequence.
|
||||
Verify quality check is updated each time and state is preserved.
|
||||
Requirements: 3.1, 3.2
|
||||
"""
|
||||
# Create receipt with quality check
|
||||
receipt = self._create_receipt_picking()
|
||||
move = self._create_stock_move(receipt, qty=1.0)
|
||||
receipt.action_confirm()
|
||||
|
||||
# Create move line
|
||||
move_line = self._create_move_line(move)
|
||||
|
||||
# Create quality check with fail state
|
||||
quality_check = self.QualityCheck.create({
|
||||
'product_id': self.product.id,
|
||||
'picking_id': receipt.id,
|
||||
'move_line_id': move_line.id,
|
||||
'test_type_id': self.test_type.id,
|
||||
'quality_state': 'fail',
|
||||
'team_id': self.quality_team.id,
|
||||
})
|
||||
|
||||
initial_state = 'fail'
|
||||
|
||||
# First lot assignment
|
||||
lot1 = self._create_lot('LOT-MULTI-CHANGE-001')
|
||||
move_line.write({'lot_id': lot1.id})
|
||||
|
||||
self.assertEqual(quality_check.lot_id.id, lot1.id,
|
||||
"Quality check should have first lot")
|
||||
self.assertEqual(quality_check.quality_state, initial_state,
|
||||
"State should be preserved after first change")
|
||||
|
||||
# Second lot change
|
||||
lot2 = self._create_lot('LOT-MULTI-CHANGE-002')
|
||||
move_line.write({'lot_id': lot2.id})
|
||||
|
||||
self.assertEqual(quality_check.lot_id.id, lot2.id,
|
||||
"Quality check should have second lot")
|
||||
self.assertEqual(quality_check.quality_state, initial_state,
|
||||
"State should be preserved after second change")
|
||||
|
||||
# Third lot change
|
||||
lot3 = self._create_lot('LOT-MULTI-CHANGE-003')
|
||||
move_line.write({'lot_id': lot3.id})
|
||||
|
||||
self.assertEqual(quality_check.lot_id.id, lot3.id,
|
||||
"Quality check should have third lot")
|
||||
self.assertEqual(quality_check.quality_state, initial_state,
|
||||
"State should be preserved after third change")
|
||||
254
tests/test_property_lot_propagation.py
Normal file
254
tests/test_property_lot_propagation.py
Normal file
@ -0,0 +1,254 @@
|
||||
# -*- 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"
|
||||
)
|
||||
282
tests/test_property_lot_update.py
Normal file
282
tests/test_property_lot_update.py
Normal file
@ -0,0 +1,282 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from hypothesis import given, strategies as st, settings, assume
|
||||
|
||||
|
||||
class TestLotNumberUpdateSynchronization(TransactionCase):
|
||||
"""
|
||||
Property-based tests for lot number update synchronization.
|
||||
|
||||
Feature: quality-check-lot-preserve, Property 3: Lot number update synchronization
|
||||
|
||||
Property: For any quality check with an assigned lot number, when the lot number
|
||||
on the related stock move line is changed, the quality check should be updated
|
||||
with the new lot number.
|
||||
|
||||
Validates: Requirements 3.1
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestLotNumberUpdateSynchronization, 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(
|
||||
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_lot_update_synchronization(self, initial_lot_seed, updated_lot_seed, quality_state):
|
||||
"""
|
||||
Property: For any quality check with an assigned lot number, when the lot number
|
||||
on the related stock move line is changed, the quality check should be updated
|
||||
with the new lot number.
|
||||
|
||||
This test verifies Requirement 3.1:
|
||||
- When a lot number on a stock move line is changed after initial assignment,
|
||||
the related quality check is updated with the new lot number
|
||||
|
||||
Test strategy:
|
||||
1. Create a receipt with a quality check
|
||||
2. Assign an initial lot number to the move line
|
||||
3. Verify the quality check receives the initial lot number
|
||||
4. Change the lot number on the move line to a different lot
|
||||
5. Verify the quality check is updated with the new lot number
|
||||
"""
|
||||
# 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 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
|
||||
|
||||
# Create and assign the initial lot number
|
||||
initial_lot = self._create_lot(product, f'LOT-INITIAL-{initial_lot_seed}')
|
||||
move_line.write({'lot_id': initial_lot.id})
|
||||
|
||||
# Refresh quality check from database
|
||||
quality_check.invalidate_recordset()
|
||||
|
||||
# Verify the quality check received the initial lot number
|
||||
self.assertEqual(
|
||||
quality_check.lot_id.id,
|
||||
initial_lot.id,
|
||||
f"Quality check should have initial lot {initial_lot.name} assigned"
|
||||
)
|
||||
|
||||
# Create a new lot number
|
||||
updated_lot = self._create_lot(product, f'LOT-UPDATED-{updated_lot_seed}')
|
||||
|
||||
# Change the lot number on the move line
|
||||
move_line.write({'lot_id': updated_lot.id})
|
||||
|
||||
# Refresh quality check from database
|
||||
quality_check.invalidate_recordset()
|
||||
|
||||
# PROPERTY VERIFICATION:
|
||||
# 1. The quality check should be updated with the new lot number (Requirement 3.1)
|
||||
self.assertEqual(
|
||||
quality_check.lot_id.id,
|
||||
updated_lot.id,
|
||||
f"Quality check should be updated with new lot {updated_lot.name} "
|
||||
f"(was {initial_lot.name})"
|
||||
)
|
||||
|
||||
# 2. The quality check state should remain unchanged (Requirement 3.2)
|
||||
self.assertEqual(
|
||||
quality_check.quality_state,
|
||||
initial_state,
|
||||
f"Quality check state should remain {initial_state} after lot update"
|
||||
)
|
||||
|
||||
# 3. The relationship between quality check and move line should remain intact (Requirement 3.3)
|
||||
self.assertEqual(
|
||||
quality_check.move_line_id.id,
|
||||
move_line.id,
|
||||
"Quality check should still be linked to the same move line"
|
||||
)
|
||||
|
||||
@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_multiple_lot_updates(self, lot_seed_1, lot_seed_2, lot_seed_3):
|
||||
"""
|
||||
Property: For any quality check, multiple consecutive lot number changes
|
||||
on the move line should result in the quality check always reflecting
|
||||
the most recent lot number.
|
||||
|
||||
This test verifies that the synchronization works correctly even with
|
||||
multiple updates in sequence.
|
||||
"""
|
||||
# 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
|
||||
quality_check = self._create_quality_check_for_move_line(move_line, state='pass')
|
||||
|
||||
# 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.lot_id.id, lot_1.id)
|
||||
|
||||
# Second update: change to lot_2
|
||||
move_line.write({'lot_id': lot_2.id})
|
||||
quality_check.invalidate_recordset()
|
||||
self.assertEqual(quality_check.lot_id.id, lot_2.id)
|
||||
|
||||
# Third update: change to lot_3
|
||||
move_line.write({'lot_id': lot_3.id})
|
||||
quality_check.invalidate_recordset()
|
||||
|
||||
# PROPERTY VERIFICATION: Quality check should have the final lot number
|
||||
self.assertEqual(
|
||||
quality_check.lot_id.id,
|
||||
lot_3.id,
|
||||
f"Quality check should have the final lot {lot_3.name} after multiple updates"
|
||||
)
|
||||
|
||||
# State should still be 'pass'
|
||||
self.assertEqual(
|
||||
quality_check.quality_state,
|
||||
'pass',
|
||||
"Quality check state should remain 'pass' after multiple lot updates"
|
||||
)
|
||||
500
tests/test_property_non_receipt_operation.py
Normal file
500
tests/test_property_non_receipt_operation.py
Normal file
@ -0,0 +1,500 @@
|
||||
# -*- 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"
|
||||
)
|
||||
421
tests/test_property_receipt_operation.py
Normal file
421
tests/test_property_receipt_operation.py
Normal file
@ -0,0 +1,421 @@
|
||||
# -*- 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"
|
||||
)
|
||||
399
tests/test_property_relationship_integrity.py
Normal file
399
tests/test_property_relationship_integrity.py
Normal file
@ -0,0 +1,399 @@
|
||||
# -*- 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}"
|
||||
)
|
||||
369
tests/test_property_state_preservation.py
Normal file
369
tests/test_property_state_preservation.py
Normal file
@ -0,0 +1,369 @@
|
||||
# -*- 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"
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user