484 lines
18 KiB
Python
484 lines
18 KiB
Python
"""
|
|
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 |