748 lines
28 KiB
Python
748 lines
28 KiB
Python
# services/inventory/app/repositories/stock_repository.py
|
|
"""
|
|
Stock Repository using Repository Pattern
|
|
"""
|
|
|
|
from typing import List, Optional, Dict, Any, Tuple
|
|
from uuid import UUID
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
from sqlalchemy import select, func, and_, or_, desc, asc, update, exists
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
import structlog
|
|
|
|
from app.models.inventory import Stock, Ingredient
|
|
from app.schemas.inventory import StockCreate, StockUpdate
|
|
from shared.database.repository import BaseRepository
|
|
from shared.utils.batch_generator import BatchCountProvider
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class StockRepository(BaseRepository[Stock, StockCreate, StockUpdate], BatchCountProvider):
|
|
"""Repository for stock operations"""
|
|
|
|
def __init__(self, session: AsyncSession):
|
|
super().__init__(Stock, session)
|
|
|
|
async def create_stock_entry(self, stock_data: StockCreate, tenant_id: UUID) -> Stock:
|
|
"""Create a new stock entry"""
|
|
try:
|
|
# Prepare data
|
|
create_data = stock_data.model_dump()
|
|
create_data['tenant_id'] = tenant_id
|
|
|
|
# Ensure production_stage is properly converted to enum value
|
|
if 'production_stage' in create_data:
|
|
if hasattr(create_data['production_stage'], 'value'):
|
|
create_data['production_stage'] = create_data['production_stage'].value
|
|
elif isinstance(create_data['production_stage'], str):
|
|
# If it's a string, ensure it's the correct enum value
|
|
from app.models.inventory import ProductionStage
|
|
try:
|
|
enum_obj = ProductionStage[create_data['production_stage']]
|
|
create_data['production_stage'] = enum_obj.value
|
|
except KeyError:
|
|
# If it's already the value, keep it as is
|
|
pass
|
|
|
|
# Calculate available quantity
|
|
available_qty = create_data['current_quantity'] - create_data.get('reserved_quantity', 0)
|
|
create_data['available_quantity'] = max(0, available_qty)
|
|
|
|
# Calculate total cost if unit cost provided
|
|
if create_data.get('unit_cost') and create_data.get('current_quantity'):
|
|
unit_cost = create_data['unit_cost']
|
|
current_quantity = Decimal(str(create_data['current_quantity']))
|
|
create_data['total_cost'] = unit_cost * current_quantity
|
|
|
|
# Create record
|
|
record = await self.create(create_data)
|
|
logger.info(
|
|
"Created stock entry",
|
|
stock_id=record.id,
|
|
ingredient_id=record.ingredient_id,
|
|
quantity=record.current_quantity,
|
|
tenant_id=tenant_id
|
|
)
|
|
return record
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to create stock entry", error=str(e), tenant_id=tenant_id)
|
|
raise
|
|
|
|
async def get_stock_by_ingredient(
|
|
self,
|
|
tenant_id: UUID,
|
|
ingredient_id: UUID,
|
|
include_unavailable: bool = False
|
|
) -> List[Stock]:
|
|
"""Get all stock entries for a specific ingredient"""
|
|
try:
|
|
query = select(self.model).where(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.ingredient_id == ingredient_id
|
|
)
|
|
)
|
|
|
|
if not include_unavailable:
|
|
query = query.where(self.model.is_available == True)
|
|
|
|
query = query.order_by(asc(self.model.expiration_date))
|
|
|
|
result = await self.session.execute(query)
|
|
return result.scalars().all()
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get stock by ingredient", error=str(e), ingredient_id=ingredient_id)
|
|
raise
|
|
|
|
async def get_stock_by_product(
|
|
self,
|
|
tenant_id: UUID,
|
|
inventory_product_id: UUID,
|
|
include_unavailable: bool = False
|
|
) -> List[Stock]:
|
|
"""
|
|
Get all stock entries for a specific product.
|
|
|
|
Note: inventory_product_id and ingredient_id refer to the same entity.
|
|
The 'ingredients' table is used as a unified catalog for both raw ingredients
|
|
and finished products, distinguished by the product_type field.
|
|
|
|
This method is an alias for get_stock_by_ingredient for clarity when called
|
|
from contexts that use 'product' terminology (e.g., procurement service).
|
|
"""
|
|
return await self.get_stock_by_ingredient(
|
|
tenant_id=tenant_id,
|
|
ingredient_id=inventory_product_id,
|
|
include_unavailable=include_unavailable
|
|
)
|
|
|
|
async def get_total_stock_by_ingredient(self, tenant_id: UUID, ingredient_id: UUID) -> Dict[str, float]:
|
|
"""Get total stock quantities for an ingredient"""
|
|
try:
|
|
result = await self.session.execute(
|
|
select(
|
|
func.coalesce(func.sum(Stock.current_quantity), 0).label('total_quantity'),
|
|
func.coalesce(func.sum(Stock.reserved_quantity), 0).label('total_reserved'),
|
|
func.coalesce(func.sum(Stock.available_quantity), 0).label('total_available')
|
|
).where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.ingredient_id == ingredient_id,
|
|
Stock.is_available == True
|
|
)
|
|
)
|
|
)
|
|
|
|
row = result.first()
|
|
return {
|
|
'total_quantity': float(row.total_quantity) if row.total_quantity else 0.0,
|
|
'total_reserved': float(row.total_reserved) if row.total_reserved else 0.0,
|
|
'total_available': float(row.total_available) if row.total_available else 0.0
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get total stock", error=str(e), ingredient_id=ingredient_id)
|
|
raise
|
|
|
|
async def get_expiring_stock(
|
|
self,
|
|
tenant_id: UUID,
|
|
days_ahead: int = 7
|
|
) -> List[Tuple[Stock, Ingredient]]:
|
|
"""Get stock items expiring within specified days using state-dependent expiration logic"""
|
|
try:
|
|
expiry_date = datetime.now() + timedelta(days=days_ahead)
|
|
|
|
# Use final_expiration_date if available (for transformed products),
|
|
# otherwise use regular expiration_date
|
|
result = await self.session.execute(
|
|
select(Stock, Ingredient)
|
|
.join(Ingredient, Stock.ingredient_id == Ingredient.id)
|
|
.where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_available == True,
|
|
or_(
|
|
and_(
|
|
Stock.final_expiration_date.isnot(None),
|
|
Stock.final_expiration_date <= expiry_date
|
|
),
|
|
and_(
|
|
Stock.final_expiration_date.is_(None),
|
|
Stock.expiration_date.isnot(None),
|
|
Stock.expiration_date <= expiry_date
|
|
)
|
|
)
|
|
)
|
|
)
|
|
.order_by(
|
|
asc(
|
|
func.coalesce(Stock.final_expiration_date, Stock.expiration_date)
|
|
)
|
|
)
|
|
)
|
|
|
|
return result.all()
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get expiring stock", error=str(e), tenant_id=tenant_id)
|
|
raise
|
|
|
|
async def get_expired_stock(self, tenant_id: UUID) -> List[Tuple[Stock, Ingredient]]:
|
|
"""Get stock items that have expired using state-dependent expiration logic"""
|
|
try:
|
|
current_date = datetime.now()
|
|
|
|
# Use final_expiration_date if available (for transformed products),
|
|
# otherwise use regular expiration_date
|
|
result = await self.session.execute(
|
|
select(Stock, Ingredient)
|
|
.join(Ingredient, Stock.ingredient_id == Ingredient.id)
|
|
.where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_available == True,
|
|
or_(
|
|
and_(
|
|
Stock.final_expiration_date.isnot(None),
|
|
Stock.final_expiration_date < current_date
|
|
),
|
|
and_(
|
|
Stock.final_expiration_date.is_(None),
|
|
Stock.expiration_date.isnot(None),
|
|
Stock.expiration_date < current_date
|
|
)
|
|
)
|
|
)
|
|
)
|
|
.order_by(
|
|
desc(
|
|
func.coalesce(Stock.final_expiration_date, Stock.expiration_date)
|
|
)
|
|
)
|
|
)
|
|
|
|
return result.all()
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get expired stock", error=str(e), tenant_id=tenant_id)
|
|
raise
|
|
|
|
async def reserve_stock(
|
|
self,
|
|
tenant_id: UUID,
|
|
ingredient_id: UUID,
|
|
quantity: float,
|
|
fifo: bool = True
|
|
) -> List[Dict[str, Any]]:
|
|
"""Reserve stock using FIFO/LIFO method with state-dependent expiration"""
|
|
try:
|
|
# Order by appropriate expiration date based on transformation status
|
|
effective_expiration = func.coalesce(Stock.final_expiration_date, Stock.expiration_date)
|
|
order_clause = asc(effective_expiration) if fifo else desc(effective_expiration)
|
|
|
|
result = await self.session.execute(
|
|
select(Stock).where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.ingredient_id == ingredient_id,
|
|
Stock.is_available == True,
|
|
Stock.available_quantity > 0
|
|
)
|
|
).order_by(order_clause)
|
|
)
|
|
|
|
stock_items = result.scalars().all()
|
|
reservations = []
|
|
remaining_qty = quantity
|
|
|
|
for stock_item in stock_items:
|
|
if remaining_qty <= 0:
|
|
break
|
|
|
|
available = stock_item.available_quantity
|
|
to_reserve = min(remaining_qty, available)
|
|
|
|
# Update stock reservation
|
|
new_reserved = stock_item.reserved_quantity + to_reserve
|
|
new_available = stock_item.current_quantity - new_reserved
|
|
|
|
await self.session.execute(
|
|
update(Stock)
|
|
.where(Stock.id == stock_item.id)
|
|
.values(
|
|
reserved_quantity=new_reserved,
|
|
available_quantity=new_available
|
|
)
|
|
)
|
|
|
|
reservations.append({
|
|
'stock_id': stock_item.id,
|
|
'reserved_quantity': to_reserve,
|
|
'batch_number': stock_item.batch_number,
|
|
'expiration_date': stock_item.expiration_date
|
|
})
|
|
|
|
remaining_qty -= to_reserve
|
|
|
|
if remaining_qty > 0:
|
|
logger.warning(
|
|
"Insufficient stock for reservation",
|
|
ingredient_id=ingredient_id,
|
|
requested=quantity,
|
|
unfulfilled=remaining_qty
|
|
)
|
|
|
|
return reservations
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to reserve stock", error=str(e), ingredient_id=ingredient_id)
|
|
raise
|
|
|
|
async def release_stock_reservation(
|
|
self,
|
|
stock_id: UUID,
|
|
quantity: float
|
|
) -> Optional[Stock]:
|
|
"""Release reserved stock"""
|
|
try:
|
|
stock_item = await self.get_by_id(stock_id)
|
|
if not stock_item:
|
|
return None
|
|
|
|
# Calculate new quantities
|
|
new_reserved = max(0, stock_item.reserved_quantity - quantity)
|
|
new_available = stock_item.current_quantity - new_reserved
|
|
|
|
# Update stock
|
|
await self.session.execute(
|
|
update(Stock)
|
|
.where(Stock.id == stock_id)
|
|
.values(
|
|
reserved_quantity=new_reserved,
|
|
available_quantity=new_available
|
|
)
|
|
)
|
|
|
|
# Refresh and return updated stock
|
|
await self.session.refresh(stock_item)
|
|
return stock_item
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to release stock reservation", error=str(e), stock_id=stock_id)
|
|
raise
|
|
|
|
async def consume_stock(
|
|
self,
|
|
stock_id: UUID,
|
|
quantity: float,
|
|
from_reserved: bool = True
|
|
) -> Optional[Stock]:
|
|
"""Consume stock (reduce current quantity)"""
|
|
try:
|
|
stock_item = await self.get_by_id(stock_id)
|
|
if not stock_item:
|
|
return None
|
|
|
|
if from_reserved:
|
|
# Reduce from reserved quantity
|
|
new_reserved = max(0, stock_item.reserved_quantity - quantity)
|
|
new_current = max(0, stock_item.current_quantity - quantity)
|
|
new_available = new_current - new_reserved
|
|
else:
|
|
# Reduce from available quantity
|
|
new_current = max(0, stock_item.current_quantity - quantity)
|
|
new_available = max(0, stock_item.available_quantity - quantity)
|
|
new_reserved = stock_item.reserved_quantity
|
|
|
|
# Update stock
|
|
await self.session.execute(
|
|
update(Stock)
|
|
.where(Stock.id == stock_id)
|
|
.values(
|
|
current_quantity=new_current,
|
|
reserved_quantity=new_reserved,
|
|
available_quantity=new_available,
|
|
is_available=new_current > 0
|
|
)
|
|
)
|
|
|
|
# Refresh and return updated stock
|
|
await self.session.refresh(stock_item)
|
|
return stock_item
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to consume stock", error=str(e), stock_id=stock_id)
|
|
raise
|
|
|
|
async def get_stock_summary_by_tenant(self, tenant_id: UUID) -> Dict[str, Any]:
|
|
"""Get stock summary for tenant dashboard"""
|
|
try:
|
|
# Basic stock summary
|
|
basic_result = await self.session.execute(
|
|
select(
|
|
func.count(Stock.id).label('total_stock_items'),
|
|
func.coalesce(func.sum(Stock.total_cost), 0).label('total_stock_value'),
|
|
func.count(func.distinct(Stock.ingredient_id)).label('unique_ingredients')
|
|
).where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_available == True
|
|
)
|
|
)
|
|
)
|
|
basic_summary = basic_result.first()
|
|
|
|
# Count expired items
|
|
expired_result = await self.session.execute(
|
|
select(func.count(Stock.id)).where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_available == True,
|
|
Stock.expiration_date < datetime.now()
|
|
)
|
|
)
|
|
)
|
|
expired_count = expired_result.scalar() or 0
|
|
|
|
# Count expiring soon items
|
|
expiring_result = await self.session.execute(
|
|
select(func.count(Stock.id)).where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_available == True,
|
|
Stock.expiration_date.isnot(None),
|
|
Stock.expiration_date <= datetime.now() + timedelta(days=7)
|
|
)
|
|
)
|
|
)
|
|
expiring_count = expiring_result.scalar() or 0
|
|
|
|
# Count out of stock items (ingredients with no available stock)
|
|
out_of_stock_result = await self.session.execute(
|
|
select(func.count(Ingredient.id)).where(
|
|
and_(
|
|
Ingredient.tenant_id == tenant_id,
|
|
~exists(
|
|
select(1).where(
|
|
and_(
|
|
Stock.ingredient_id == Ingredient.id,
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_available == True,
|
|
Stock.available_quantity > 0
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
out_of_stock_count = out_of_stock_result.scalar() or 0
|
|
|
|
return {
|
|
'total_stock_items': basic_summary.total_stock_items or 0,
|
|
'total_stock_value': float(basic_summary.total_stock_value) if basic_summary.total_stock_value else 0.0,
|
|
'unique_ingredients': basic_summary.unique_ingredients or 0,
|
|
'expired_items': expired_count,
|
|
'expiring_soon_items': expiring_count,
|
|
'out_of_stock_count': out_of_stock_count
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get stock summary", error=str(e), tenant_id=tenant_id)
|
|
raise
|
|
|
|
async def mark_expired_stock(self, tenant_id: UUID) -> int:
|
|
"""Mark expired stock items as expired using state-dependent expiration logic"""
|
|
try:
|
|
current_date = datetime.now()
|
|
|
|
# Mark items as expired based on final_expiration_date or expiration_date
|
|
result = await self.session.execute(
|
|
update(Stock)
|
|
.where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_expired == False,
|
|
or_(
|
|
and_(
|
|
Stock.final_expiration_date.isnot(None),
|
|
Stock.final_expiration_date < current_date
|
|
),
|
|
and_(
|
|
Stock.final_expiration_date.is_(None),
|
|
Stock.expiration_date.isnot(None),
|
|
Stock.expiration_date < current_date
|
|
)
|
|
)
|
|
)
|
|
)
|
|
.values(is_expired=True, quality_status="expired")
|
|
)
|
|
|
|
expired_count = result.rowcount
|
|
logger.info(f"Marked {expired_count} stock items as expired using state-dependent logic", tenant_id=tenant_id)
|
|
|
|
return expired_count
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to mark expired stock", error=str(e), tenant_id=tenant_id)
|
|
raise
|
|
|
|
async def get_stock_by_production_stage(
|
|
self,
|
|
tenant_id: UUID,
|
|
production_stage: 'ProductionStage',
|
|
ingredient_id: Optional[UUID] = None
|
|
) -> List['Stock']:
|
|
"""Get stock items by production stage"""
|
|
try:
|
|
conditions = [
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.production_stage == production_stage,
|
|
Stock.is_available == True
|
|
]
|
|
|
|
if ingredient_id:
|
|
conditions.append(Stock.ingredient_id == ingredient_id)
|
|
|
|
result = await self.session.execute(
|
|
select(Stock)
|
|
.where(and_(*conditions))
|
|
.order_by(asc(func.coalesce(Stock.final_expiration_date, Stock.expiration_date)))
|
|
)
|
|
|
|
return result.scalars().all()
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get stock by production stage", error=str(e), production_stage=production_stage)
|
|
raise
|
|
|
|
async def get_stock_entries(
|
|
self,
|
|
tenant_id: UUID,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
ingredient_id: Optional[UUID] = None,
|
|
available_only: bool = True
|
|
) -> List[Stock]:
|
|
"""Get stock entries with filtering and pagination"""
|
|
try:
|
|
conditions = [Stock.tenant_id == tenant_id]
|
|
|
|
if available_only:
|
|
conditions.append(Stock.is_available == True)
|
|
|
|
if ingredient_id:
|
|
conditions.append(Stock.ingredient_id == ingredient_id)
|
|
|
|
query = (
|
|
select(Stock)
|
|
.where(and_(*conditions))
|
|
.order_by(desc(Stock.created_at))
|
|
.offset(skip)
|
|
.limit(limit)
|
|
)
|
|
|
|
result = await self.session.execute(query)
|
|
return result.scalars().all()
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get stock entries", error=str(e), tenant_id=tenant_id)
|
|
raise
|
|
|
|
async def delete_by_ingredient(self, ingredient_id: UUID, tenant_id: UUID) -> int:
|
|
"""Delete all stock entries for a specific ingredient"""
|
|
try:
|
|
from sqlalchemy import delete
|
|
|
|
stmt = delete(Stock).where(
|
|
and_(
|
|
Stock.ingredient_id == ingredient_id,
|
|
Stock.tenant_id == tenant_id
|
|
)
|
|
)
|
|
|
|
result = await self.session.execute(stmt)
|
|
deleted_count = result.rowcount
|
|
|
|
logger.info(
|
|
"Deleted stock entries for ingredient",
|
|
ingredient_id=str(ingredient_id),
|
|
tenant_id=str(tenant_id),
|
|
deleted_count=deleted_count
|
|
)
|
|
|
|
return deleted_count
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
"Failed to delete stock entries for ingredient",
|
|
error=str(e),
|
|
ingredient_id=str(ingredient_id),
|
|
tenant_id=str(tenant_id)
|
|
)
|
|
raise
|
|
|
|
async def get_daily_batch_count(
|
|
self,
|
|
tenant_id: str,
|
|
date_start: datetime,
|
|
date_end: datetime,
|
|
prefix: Optional[str] = None
|
|
) -> int:
|
|
"""Get the count of batches created today for the given tenant"""
|
|
try:
|
|
conditions = [
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.created_at >= date_start,
|
|
Stock.created_at <= date_end
|
|
]
|
|
|
|
if prefix:
|
|
conditions.append(Stock.batch_number.like(f"{prefix}-%"))
|
|
|
|
stmt = select(func.count(Stock.id)).where(and_(*conditions))
|
|
result = await self.session.execute(stmt)
|
|
count = result.scalar() or 0
|
|
|
|
logger.debug(
|
|
"Retrieved daily batch count",
|
|
tenant_id=tenant_id,
|
|
prefix=prefix,
|
|
count=count,
|
|
date_start=date_start,
|
|
date_end=date_end
|
|
)
|
|
|
|
return count
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
"Failed to get daily batch count",
|
|
error=str(e),
|
|
tenant_id=tenant_id,
|
|
prefix=prefix
|
|
)
|
|
raise
|
|
|
|
async def get_expired_batches_for_processing(self, tenant_id: UUID) -> List[Tuple[Stock, Ingredient]]:
|
|
"""Get expired batches that haven't been processed yet (for automatic processing)"""
|
|
try:
|
|
current_date = datetime.now()
|
|
|
|
# Find expired batches that are still available and not yet marked as expired
|
|
result = await self.session.execute(
|
|
select(Stock, Ingredient)
|
|
.join(Ingredient, Stock.ingredient_id == Ingredient.id)
|
|
.where(
|
|
and_(
|
|
Stock.tenant_id == tenant_id,
|
|
Stock.is_available == True,
|
|
Stock.is_expired == False,
|
|
Stock.current_quantity > 0,
|
|
or_(
|
|
and_(
|
|
Stock.final_expiration_date.isnot(None),
|
|
Stock.final_expiration_date <= current_date
|
|
),
|
|
and_(
|
|
Stock.final_expiration_date.is_(None),
|
|
Stock.expiration_date.isnot(None),
|
|
Stock.expiration_date <= current_date
|
|
)
|
|
)
|
|
)
|
|
)
|
|
.order_by(
|
|
asc(func.coalesce(Stock.final_expiration_date, Stock.expiration_date))
|
|
)
|
|
)
|
|
|
|
expired_batches = result.all()
|
|
logger.info("Found expired batches for processing",
|
|
tenant_id=str(tenant_id),
|
|
count=len(expired_batches))
|
|
|
|
return expired_batches
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get expired batches for processing",
|
|
error=str(e), tenant_id=tenant_id)
|
|
raise
|
|
|
|
async def mark_batch_as_expired(self, stock_id: UUID, tenant_id: UUID) -> bool:
|
|
"""Mark a specific batch as expired and unavailable"""
|
|
try:
|
|
result = await self.session.execute(
|
|
update(Stock)
|
|
.where(
|
|
and_(
|
|
Stock.id == stock_id,
|
|
Stock.tenant_id == tenant_id
|
|
)
|
|
)
|
|
.values(
|
|
is_expired=True,
|
|
is_available=False,
|
|
quality_status="expired",
|
|
updated_at=datetime.now()
|
|
)
|
|
)
|
|
|
|
if result.rowcount > 0:
|
|
logger.info("Marked batch as expired",
|
|
stock_id=str(stock_id),
|
|
tenant_id=str(tenant_id))
|
|
return True
|
|
else:
|
|
logger.warning("No batch found to mark as expired",
|
|
stock_id=str(stock_id),
|
|
tenant_id=str(tenant_id))
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to mark batch as expired",
|
|
error=str(e),
|
|
stock_id=str(stock_id),
|
|
tenant_id=str(tenant_id))
|
|
raise
|
|
|
|
async def update_stock_to_zero(self, stock_id: UUID, tenant_id: UUID) -> bool:
|
|
"""Update stock quantities to zero after moving to waste"""
|
|
try:
|
|
result = await self.session.execute(
|
|
update(Stock)
|
|
.where(
|
|
and_(
|
|
Stock.id == stock_id,
|
|
Stock.tenant_id == tenant_id
|
|
)
|
|
)
|
|
.values(
|
|
current_quantity=0,
|
|
available_quantity=0,
|
|
updated_at=datetime.now()
|
|
)
|
|
)
|
|
|
|
if result.rowcount > 0:
|
|
logger.info("Updated stock quantities to zero",
|
|
stock_id=str(stock_id),
|
|
tenant_id=str(tenant_id))
|
|
return True
|
|
else:
|
|
logger.warning("No stock found to update to zero",
|
|
stock_id=str(stock_id),
|
|
tenant_id=str(tenant_id))
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to update stock to zero",
|
|
error=str(e),
|
|
stock_id=str(stock_id),
|
|
tenant_id=str(tenant_id))
|
|
raise |