Improve the frontend modals
This commit is contained in:
@@ -280,6 +280,11 @@ class InventoryService:
|
||||
# Create stock entry
|
||||
stock = await stock_repo.create_stock_entry(stock_data, tenant_id)
|
||||
|
||||
# Get current stock level before this movement
|
||||
current_stock = await stock_repo.get_total_stock_by_ingredient(tenant_id, UUID(stock_data.ingredient_id))
|
||||
quantity_before = current_stock['total_available']
|
||||
quantity_after = quantity_before + stock_data.current_quantity
|
||||
|
||||
# Create stock movement record
|
||||
movement_data = StockMovementCreate(
|
||||
ingredient_id=stock_data.ingredient_id,
|
||||
@@ -289,14 +294,22 @@ class InventoryService:
|
||||
unit_cost=stock_data.unit_cost,
|
||||
notes=f"Initial stock entry - Batch: {stock_data.batch_number or 'N/A'}"
|
||||
)
|
||||
await movement_repo.create_movement(movement_data, tenant_id, user_id)
|
||||
|
||||
# Update ingredient's last purchase price
|
||||
await movement_repo.create_movement(movement_data, tenant_id, user_id, quantity_before, quantity_after)
|
||||
|
||||
# Update ingredient's last purchase price and weighted average cost
|
||||
if stock_data.unit_cost:
|
||||
await ingredient_repo.update_last_purchase_price(
|
||||
UUID(stock_data.ingredient_id),
|
||||
UUID(stock_data.ingredient_id),
|
||||
float(stock_data.unit_cost)
|
||||
)
|
||||
|
||||
# Calculate and update weighted average cost
|
||||
await ingredient_repo.update_weighted_average_cost(
|
||||
ingredient_id=UUID(stock_data.ingredient_id),
|
||||
current_stock_quantity=quantity_before,
|
||||
new_purchase_quantity=stock_data.current_quantity,
|
||||
new_unit_cost=float(stock_data.unit_cost)
|
||||
)
|
||||
|
||||
# Convert to response schema
|
||||
response = StockResponse(**stock.to_dict())
|
||||
@@ -333,19 +346,28 @@ class InventoryService:
|
||||
|
||||
# Reserve stock first
|
||||
reservations = await stock_repo.reserve_stock(tenant_id, ingredient_id, quantity, fifo)
|
||||
|
||||
|
||||
if not reservations:
|
||||
raise ValueError("Insufficient stock available")
|
||||
|
||||
|
||||
# Get current stock level before this consumption
|
||||
current_stock = await stock_repo.get_total_stock_by_ingredient(tenant_id, ingredient_id)
|
||||
running_stock_level = current_stock['total_available']
|
||||
|
||||
consumed_items = []
|
||||
for reservation in reservations:
|
||||
stock_id = UUID(reservation['stock_id'])
|
||||
reserved_qty = reservation['reserved_quantity']
|
||||
|
||||
|
||||
# Calculate before/after for this specific batch
|
||||
batch_quantity_before = running_stock_level
|
||||
batch_quantity_after = running_stock_level - reserved_qty
|
||||
running_stock_level = batch_quantity_after # Update for next iteration
|
||||
|
||||
# Consume from reserved stock
|
||||
consumed_stock = await stock_repo.consume_stock(stock_id, reserved_qty, from_reserved=True)
|
||||
|
||||
# Create movement record
|
||||
|
||||
# Create movement record with progressive tracking
|
||||
movement_data = StockMovementCreate(
|
||||
ingredient_id=str(ingredient_id),
|
||||
stock_id=str(stock_id),
|
||||
@@ -354,7 +376,7 @@ class InventoryService:
|
||||
reference_number=reference_number,
|
||||
notes=notes or f"Stock consumption - Batch: {reservation.get('batch_number', 'N/A')}"
|
||||
)
|
||||
await movement_repo.create_movement(movement_data, tenant_id, user_id)
|
||||
await movement_repo.create_movement(movement_data, tenant_id, user_id, batch_quantity_before, batch_quantity_after)
|
||||
|
||||
consumed_items.append({
|
||||
'stock_id': str(stock_id),
|
||||
@@ -650,6 +672,187 @@ class InventoryService:
|
||||
logger.error("Failed to get stock entries", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def create_stock_movement(
|
||||
self,
|
||||
movement_data: StockMovementCreate,
|
||||
tenant_id: UUID,
|
||||
user_id: Optional[UUID] = None
|
||||
) -> StockMovementResponse:
|
||||
"""Create a stock movement record with proper quantity tracking"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
movement_repo = StockMovementRepository(db)
|
||||
ingredient_repo = IngredientRepository(db)
|
||||
stock_repo = StockRepository(db)
|
||||
|
||||
# Validate ingredient exists
|
||||
ingredient = await ingredient_repo.get_by_id(UUID(movement_data.ingredient_id))
|
||||
if not ingredient or ingredient.tenant_id != tenant_id:
|
||||
raise ValueError("Ingredient not found")
|
||||
|
||||
# Get current stock level before this movement
|
||||
current_stock = await stock_repo.get_total_stock_by_ingredient(tenant_id, UUID(movement_data.ingredient_id))
|
||||
quantity_before = current_stock['total_available']
|
||||
|
||||
# Calculate quantity_after based on movement type
|
||||
movement_quantity = movement_data.quantity or 0
|
||||
if movement_data.movement_type in [StockMovementType.PURCHASE, StockMovementType.TRANSFORMATION, StockMovementType.INITIAL_STOCK]:
|
||||
# These are additions to stock
|
||||
quantity_after = quantity_before + movement_quantity
|
||||
else:
|
||||
# These are subtractions from stock (PRODUCTION_USE, WASTE, ADJUSTMENT)
|
||||
quantity_after = quantity_before - movement_quantity
|
||||
|
||||
# Create stock movement record
|
||||
movement = await movement_repo.create_movement(
|
||||
movement_data,
|
||||
tenant_id,
|
||||
user_id,
|
||||
quantity_before,
|
||||
quantity_after
|
||||
)
|
||||
|
||||
# Convert to response schema
|
||||
response = StockMovementResponse(**movement.to_dict())
|
||||
response.ingredient = IngredientResponse(**ingredient.to_dict())
|
||||
|
||||
logger.info(
|
||||
"Stock movement created successfully",
|
||||
movement_id=movement.id,
|
||||
ingredient_id=movement.ingredient_id,
|
||||
quantity=movement.quantity,
|
||||
quantity_before=quantity_before,
|
||||
quantity_after=quantity_after
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to create stock movement", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_stock_entry(
|
||||
self,
|
||||
stock_id: UUID,
|
||||
tenant_id: UUID
|
||||
) -> Optional[StockResponse]:
|
||||
"""Get a single stock entry by ID"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
stock_repo = StockRepository(db)
|
||||
ingredient_repo = IngredientRepository(db)
|
||||
|
||||
# Get stock entry
|
||||
stock = await stock_repo.get_by_id(stock_id)
|
||||
|
||||
# Check if stock exists and belongs to tenant
|
||||
if not stock or stock.tenant_id != tenant_id:
|
||||
return None
|
||||
|
||||
# Get ingredient information
|
||||
ingredient = await ingredient_repo.get_by_id(stock.ingredient_id)
|
||||
|
||||
response = StockResponse(**stock.to_dict())
|
||||
if ingredient:
|
||||
ingredient_dict = ingredient.to_dict()
|
||||
# Map category field based on product type
|
||||
if ingredient.product_type and ingredient.product_type.value == 'finished_product':
|
||||
ingredient_dict['category'] = ingredient.product_category.value if ingredient.product_category else None
|
||||
else:
|
||||
ingredient_dict['category'] = ingredient.ingredient_category.value if ingredient.ingredient_category else None
|
||||
response.ingredient = IngredientResponse(**ingredient_dict)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get stock entry", error=str(e), stock_id=stock_id, tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def update_stock(
|
||||
self,
|
||||
stock_id: UUID,
|
||||
stock_data: StockUpdate,
|
||||
tenant_id: UUID
|
||||
) -> Optional[StockResponse]:
|
||||
"""Update a stock entry"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
stock_repo = StockRepository(db)
|
||||
ingredient_repo = IngredientRepository(db)
|
||||
|
||||
# Check if stock exists and belongs to tenant
|
||||
existing_stock = await stock_repo.get_by_id(stock_id)
|
||||
if not existing_stock or existing_stock.tenant_id != tenant_id:
|
||||
return None
|
||||
|
||||
# Prepare update data
|
||||
update_data = stock_data.model_dump(exclude_unset=True)
|
||||
|
||||
# Recalculate available_quantity if current_quantity or reserved_quantity changed
|
||||
if 'current_quantity' in update_data or 'reserved_quantity' in update_data:
|
||||
current_qty = update_data.get('current_quantity', existing_stock.current_quantity)
|
||||
reserved_qty = update_data.get('reserved_quantity', existing_stock.reserved_quantity)
|
||||
update_data['available_quantity'] = max(0, current_qty - reserved_qty)
|
||||
|
||||
# Recalculate total cost if unit_cost or current_quantity changed
|
||||
if 'unit_cost' in update_data or 'current_quantity' in update_data:
|
||||
unit_cost = update_data.get('unit_cost', existing_stock.unit_cost)
|
||||
current_qty = update_data.get('current_quantity', existing_stock.current_quantity)
|
||||
if unit_cost is not None and current_qty is not None:
|
||||
from decimal import Decimal
|
||||
update_data['total_cost'] = Decimal(str(unit_cost)) * Decimal(str(current_qty))
|
||||
|
||||
# Update the stock entry
|
||||
updated_stock = await stock_repo.update(stock_id, update_data)
|
||||
if not updated_stock:
|
||||
return None
|
||||
|
||||
# Get ingredient information
|
||||
ingredient = await ingredient_repo.get_by_id(updated_stock.ingredient_id)
|
||||
|
||||
response = StockResponse(**updated_stock.to_dict())
|
||||
if ingredient:
|
||||
ingredient_dict = ingredient.to_dict()
|
||||
# Map category field based on product type
|
||||
if ingredient.product_type and ingredient.product_type.value == 'finished_product':
|
||||
ingredient_dict['category'] = ingredient.product_category.value if ingredient.product_category else None
|
||||
else:
|
||||
ingredient_dict['category'] = ingredient.ingredient_category.value if ingredient.ingredient_category else None
|
||||
response.ingredient = IngredientResponse(**ingredient_dict)
|
||||
|
||||
logger.info("Stock entry updated successfully", stock_id=stock_id, tenant_id=tenant_id)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to update stock entry", error=str(e), stock_id=stock_id, tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def delete_stock(
|
||||
self,
|
||||
stock_id: UUID,
|
||||
tenant_id: UUID
|
||||
) -> bool:
|
||||
"""Delete a stock entry"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
stock_repo = StockRepository(db)
|
||||
|
||||
# Check if stock exists and belongs to tenant
|
||||
existing_stock = await stock_repo.get_by_id(stock_id)
|
||||
if not existing_stock or existing_stock.tenant_id != tenant_id:
|
||||
return False
|
||||
|
||||
# Delete the stock entry
|
||||
success = await stock_repo.delete_by_id(stock_id)
|
||||
|
||||
if success:
|
||||
logger.info("Stock entry deleted successfully", stock_id=stock_id, tenant_id=tenant_id)
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete stock entry", error=str(e), stock_id=stock_id, tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
# ===== DELETION METHODS =====
|
||||
|
||||
async def hard_delete_ingredient(
|
||||
|
||||
Reference in New Issue
Block a user