New enterprise feature
This commit is contained in:
484
services/inventory/app/services/internal_transfer_service.py
Normal file
484
services/inventory/app/services/internal_transfer_service.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""
|
||||
Internal Transfer Service for Inventory Management
|
||||
Handles inventory ownership changes during internal transfers
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
import uuid
|
||||
|
||||
from shared.clients.tenant_client import TenantServiceClient
|
||||
from shared.clients.inventory_client import InventoryServiceClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InternalTransferInventoryService:
|
||||
"""
|
||||
Service for handling inventory transfers during enterprise internal transfers
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tenant_client: TenantServiceClient,
|
||||
inventory_client: InventoryServiceClient
|
||||
):
|
||||
self.tenant_client = tenant_client
|
||||
self.inventory_client = inventory_client
|
||||
|
||||
async def process_internal_delivery(
|
||||
self,
|
||||
parent_tenant_id: str,
|
||||
child_tenant_id: str,
|
||||
shipment_items: List[Dict[str, Any]],
|
||||
shipment_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Process inventory ownership transfer when internal shipment is delivered
|
||||
|
||||
Args:
|
||||
parent_tenant_id: Source tenant (central production)
|
||||
child_tenant_id: Destination tenant (retail outlet)
|
||||
shipment_items: List of items being transferred with quantities
|
||||
shipment_id: ID of the shipment for reference
|
||||
|
||||
Returns:
|
||||
Dict with transfer results
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
"Processing internal inventory transfer",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
shipment_id=shipment_id,
|
||||
item_count=len(shipment_items)
|
||||
)
|
||||
|
||||
# Process each item in the shipment
|
||||
successful_transfers = []
|
||||
failed_transfers = []
|
||||
|
||||
for item in shipment_items:
|
||||
product_id = item.get('product_id')
|
||||
quantity = Decimal(str(item.get('delivered_quantity', item.get('quantity', 0))))
|
||||
|
||||
if not product_id or quantity <= 0:
|
||||
logger.warning(
|
||||
"Skipping invalid transfer item",
|
||||
product_id=product_id,
|
||||
quantity=quantity
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
# Step 1: Deduct inventory from parent (central production)
|
||||
parent_subtraction_result = await self._subtract_from_parent_inventory(
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=quantity,
|
||||
shipment_id=shipment_id
|
||||
)
|
||||
|
||||
# Step 2: Add inventory to child (retail outlet)
|
||||
child_addition_result = await self._add_to_child_inventory(
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=quantity,
|
||||
shipment_id=shipment_id
|
||||
)
|
||||
|
||||
successful_transfers.append({
|
||||
'product_id': product_id,
|
||||
'quantity': float(quantity),
|
||||
'parent_result': parent_subtraction_result,
|
||||
'child_result': child_addition_result
|
||||
})
|
||||
|
||||
logger.info(
|
||||
"Internal inventory transfer completed",
|
||||
product_id=product_id,
|
||||
quantity=float(quantity),
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id
|
||||
)
|
||||
|
||||
except Exception as item_error:
|
||||
logger.error(
|
||||
"Failed to process inventory transfer for item",
|
||||
product_id=product_id,
|
||||
quantity=float(quantity),
|
||||
error=str(item_error),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
failed_transfers.append({
|
||||
'product_id': product_id,
|
||||
'quantity': float(quantity),
|
||||
'error': str(item_error)
|
||||
})
|
||||
|
||||
# Update shipment status in inventory records to reflect completed transfer
|
||||
await self._mark_shipment_as_completed_in_inventory(
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
shipment_id=shipment_id
|
||||
)
|
||||
|
||||
total_transferred = sum(item['quantity'] for item in successful_transfers)
|
||||
|
||||
result = {
|
||||
'shipment_id': shipment_id,
|
||||
'parent_tenant_id': parent_tenant_id,
|
||||
'child_tenant_id': child_tenant_id,
|
||||
'transfers_completed': len(successful_transfers),
|
||||
'transfers_failed': len(failed_transfers),
|
||||
'total_quantity_transferred': total_transferred,
|
||||
'successful_transfers': successful_transfers,
|
||||
'failed_transfers': failed_transfers,
|
||||
'status': 'completed' if failed_transfers == 0 else 'partial_success',
|
||||
'processed_at': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Internal inventory transfer processing completed",
|
||||
shipment_id=shipment_id,
|
||||
successfully_processed=len(successful_transfers),
|
||||
failed_count=len(failed_transfers)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing internal inventory transfer",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
shipment_id=shipment_id,
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
async def _subtract_from_parent_inventory(
|
||||
self,
|
||||
parent_tenant_id: str,
|
||||
product_id: str,
|
||||
quantity: Decimal,
|
||||
shipment_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Subtract inventory from parent tenant (central production)
|
||||
"""
|
||||
try:
|
||||
# Check current inventory level in parent
|
||||
parent_stock = await self.inventory_client.get_product_stock(
|
||||
tenant_id=parent_tenant_id,
|
||||
product_id=product_id
|
||||
)
|
||||
|
||||
current_stock = Decimal(str(parent_stock.get('available_quantity', 0)))
|
||||
|
||||
if current_stock < quantity:
|
||||
raise ValueError(
|
||||
f"Insufficient inventory in parent tenant {parent_tenant_id}. "
|
||||
f"Required: {quantity}, Available: {current_stock}"
|
||||
)
|
||||
|
||||
# Create stock movement record with negative quantity
|
||||
stock_movement_data = {
|
||||
'product_id': product_id,
|
||||
'movement_type': 'INTERNAL_TRANSFER_OUT',
|
||||
'quantity': float(-quantity), # Negative for outbound
|
||||
'reference_type': 'internal_transfer',
|
||||
'reference_id': shipment_id,
|
||||
'source_tenant_id': parent_tenant_id,
|
||||
'destination_tenant_id': parent_tenant_id, # Self-reference for tracking
|
||||
'notes': f'Shipment to child tenant #{shipment_id}'
|
||||
}
|
||||
|
||||
# Execute the stock movement
|
||||
movement_result = await self.inventory_client.create_stock_movement(
|
||||
tenant_id=parent_tenant_id,
|
||||
movement_data=stock_movement_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Inventory subtracted from parent",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=float(quantity),
|
||||
movement_id=movement_result.get('id')
|
||||
)
|
||||
|
||||
return {
|
||||
'movement_id': movement_result.get('id'),
|
||||
'quantity_subtracted': float(quantity),
|
||||
'new_balance': float(current_stock - quantity),
|
||||
'status': 'success'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error subtracting from parent inventory",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=float(quantity),
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
async def _add_to_child_inventory(
|
||||
self,
|
||||
child_tenant_id: str,
|
||||
product_id: str,
|
||||
quantity: Decimal,
|
||||
shipment_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Add inventory to child tenant (retail outlet)
|
||||
"""
|
||||
try:
|
||||
# Create stock movement record with positive quantity
|
||||
stock_movement_data = {
|
||||
'product_id': product_id,
|
||||
'movement_type': 'INTERNAL_TRANSFER_IN',
|
||||
'quantity': float(quantity), # Positive for inbound
|
||||
'reference_type': 'internal_transfer',
|
||||
'reference_id': shipment_id,
|
||||
'source_tenant_id': child_tenant_id, # Self-reference from parent
|
||||
'destination_tenant_id': child_tenant_id,
|
||||
'notes': f'Internal transfer from parent tenant shipment #{shipment_id}'
|
||||
}
|
||||
|
||||
# Execute the stock movement
|
||||
movement_result = await self.inventory_client.create_stock_movement(
|
||||
tenant_id=child_tenant_id,
|
||||
movement_data=stock_movement_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Inventory added to child",
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=float(quantity),
|
||||
movement_id=movement_result.get('id')
|
||||
)
|
||||
|
||||
return {
|
||||
'movement_id': movement_result.get('id'),
|
||||
'quantity_added': float(quantity),
|
||||
'status': 'success'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error adding to child inventory",
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=float(quantity),
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
async def _mark_shipment_as_completed_in_inventory(
|
||||
self,
|
||||
parent_tenant_id: str,
|
||||
child_tenant_id: str,
|
||||
shipment_id: str
|
||||
):
|
||||
"""
|
||||
Update inventory records to mark shipment as completed
|
||||
"""
|
||||
try:
|
||||
# In a real implementation, this would update inventory tracking records
|
||||
# to reflect that the internal transfer is complete
|
||||
# For now, we'll just log that we're tracking this
|
||||
|
||||
logger.info(
|
||||
"Marked internal transfer as completed in inventory tracking",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
shipment_id=shipment_id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error updating inventory completion status",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
shipment_id=shipment_id,
|
||||
error=str(e)
|
||||
)
|
||||
# This is not critical enough to fail the entire operation
|
||||
|
||||
async def get_internal_transfer_history(
|
||||
self,
|
||||
parent_tenant_id: str,
|
||||
child_tenant_id: str = None,
|
||||
start_date: str = None,
|
||||
end_date: str = None,
|
||||
limit: int = 100
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get history of internal inventory transfers
|
||||
|
||||
Args:
|
||||
parent_tenant_id: Parent tenant ID
|
||||
child_tenant_id: Optional child tenant ID to filter by
|
||||
start_date: Optional start date filter
|
||||
end_date: Optional end date filter
|
||||
limit: Max results to return
|
||||
|
||||
Returns:
|
||||
List of internal transfer records
|
||||
"""
|
||||
try:
|
||||
# Build filter conditions
|
||||
filters = {
|
||||
'reference_type': 'internal_transfer'
|
||||
}
|
||||
|
||||
if child_tenant_id:
|
||||
filters['destination_tenant_id'] = child_tenant_id
|
||||
if start_date:
|
||||
filters['created_after'] = start_date
|
||||
if end_date:
|
||||
filters['created_before'] = end_date
|
||||
|
||||
# Query inventory movements for internal transfers
|
||||
parent_movements = await self.inventory_client.get_stock_movements(
|
||||
tenant_id=parent_tenant_id,
|
||||
filters=filters,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# Filter for outbound transfers (negative values)
|
||||
outbound_transfers = [m for m in parent_movements if m.get('quantity', 0) < 0]
|
||||
|
||||
# Also get inbound transfers for the children if specified
|
||||
all_transfers = outbound_transfers
|
||||
|
||||
if child_tenant_id:
|
||||
child_movements = await self.inventory_client.get_stock_movements(
|
||||
tenant_id=child_tenant_id,
|
||||
filters=filters,
|
||||
limit=limit
|
||||
)
|
||||
# Filter for inbound transfers (positive values)
|
||||
inbound_transfers = [m for m in child_movements if m.get('quantity', 0) > 0]
|
||||
all_transfers.extend(inbound_transfers)
|
||||
|
||||
# Sort by creation date (most recent first)
|
||||
all_transfers.sort(key=lambda x: x.get('created_at', ''), reverse=True)
|
||||
|
||||
return all_transfers[:limit]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting internal transfer history",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
async def validate_internal_transfer_eligibility(
|
||||
self,
|
||||
parent_tenant_id: str,
|
||||
child_tenant_id: str,
|
||||
items: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate that internal transfer is possible (sufficient inventory, etc.)
|
||||
|
||||
Args:
|
||||
parent_tenant_id: Parent tenant ID (supplier)
|
||||
child_tenant_id: Child tenant ID (recipient)
|
||||
items: List of items to transfer
|
||||
|
||||
Returns:
|
||||
Dict with validation results
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
"Validating internal transfer eligibility",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
item_count=len(items)
|
||||
)
|
||||
|
||||
validation_results = {
|
||||
'eligible': True,
|
||||
'errors': [],
|
||||
'warnings': [],
|
||||
'inventory_check': []
|
||||
}
|
||||
|
||||
for item in items:
|
||||
product_id = item.get('product_id')
|
||||
quantity = Decimal(str(item.get('quantity', 0)))
|
||||
|
||||
if quantity <= 0:
|
||||
validation_results['errors'].append({
|
||||
'product_id': product_id,
|
||||
'error': 'Quantity must be greater than 0',
|
||||
'quantity': float(quantity)
|
||||
})
|
||||
continue
|
||||
|
||||
# Check if parent has sufficient inventory
|
||||
try:
|
||||
parent_stock = await self.inventory_client.get_product_stock(
|
||||
tenant_id=parent_tenant_id,
|
||||
product_id=product_id
|
||||
)
|
||||
|
||||
available_quantity = Decimal(str(parent_stock.get('available_quantity', 0)))
|
||||
|
||||
if available_quantity < quantity:
|
||||
validation_results['errors'].append({
|
||||
'product_id': product_id,
|
||||
'error': 'Insufficient inventory in parent tenant',
|
||||
'available': float(available_quantity),
|
||||
'requested': float(quantity)
|
||||
})
|
||||
else:
|
||||
validation_results['inventory_check'].append({
|
||||
'product_id': product_id,
|
||||
'available': float(available_quantity),
|
||||
'requested': float(quantity),
|
||||
'sufficient': True
|
||||
})
|
||||
|
||||
except Exception as stock_error:
|
||||
logger.error(
|
||||
"Error checking parent inventory for validation",
|
||||
product_id=product_id,
|
||||
error=str(stock_error)
|
||||
)
|
||||
validation_results['errors'].append({
|
||||
'product_id': product_id,
|
||||
'error': f'Error checking inventory: {str(stock_error)}'
|
||||
})
|
||||
|
||||
# Overall eligibility based on errors
|
||||
validation_results['eligible'] = len(validation_results['errors']) == 0
|
||||
|
||||
logger.info(
|
||||
"Internal transfer validation completed",
|
||||
eligible=validation_results['eligible'],
|
||||
error_count=len(validation_results['errors'])
|
||||
)
|
||||
|
||||
return validation_results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error validating internal transfer eligibility",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
Reference in New Issue
Block a user