Improve the frontend modals

This commit is contained in:
Urtzi Alfaro
2025-10-27 16:33:26 +01:00
parent 61376b7a9f
commit 858d985c92
143 changed files with 9289 additions and 2306 deletions

View File

@@ -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(