Improve the sales import

This commit is contained in:
Urtzi Alfaro
2025-10-15 21:09:42 +02:00
parent 8f9e9a7edc
commit dbb48d8e2c
21 changed files with 992 additions and 409 deletions

View File

@@ -365,3 +365,93 @@ async def classify_products_batch(
logger.error("Failed batch classification",
error=str(e), products_count=len(request.products), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Batch classification failed: {str(e)}")
class BatchProductResolutionRequest(BaseModel):
"""Request for batch product resolution or creation"""
products: List[Dict[str, Any]] = Field(..., description="Products to resolve or create")
class BatchProductResolutionResponse(BaseModel):
"""Response with product name to inventory ID mappings"""
product_mappings: Dict[str, str] = Field(..., description="Product name to inventory product ID mapping")
created_count: int = Field(..., description="Number of products created")
resolved_count: int = Field(..., description="Number of existing products resolved")
failed_count: int = Field(0, description="Number of products that failed")
@router.post(
route_builder.build_operations_route("resolve-or-create-products-batch"),
response_model=BatchProductResolutionResponse
)
async def resolve_or_create_products_batch(
request: BatchProductResolutionRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Resolve or create multiple products in a single optimized operation for sales import"""
try:
if not request.products:
raise HTTPException(status_code=400, detail="No products provided")
service = InventoryService()
product_mappings = {}
created_count = 0
resolved_count = 0
failed_count = 0
for product_data in request.products:
product_name = product_data.get('name', product_data.get('product_name', ''))
if not product_name:
failed_count += 1
continue
try:
existing = await service.search_ingredients_by_name(product_name, tenant_id, db)
if existing:
product_mappings[product_name] = str(existing.id)
resolved_count += 1
logger.debug("Resolved existing product", product=product_name, tenant_id=tenant_id)
else:
category = product_data.get('category', 'general')
ingredient_data = {
'name': product_name,
'type': 'finished_product',
'unit': 'unit',
'current_stock': 0,
'reorder_point': 0,
'cost_per_unit': 0,
'category': category
}
created = await service.create_ingredient_fast(ingredient_data, tenant_id, db)
product_mappings[product_name] = str(created.id)
created_count += 1
logger.debug("Created new product", product=product_name, tenant_id=tenant_id)
except Exception as e:
logger.warning("Failed to resolve/create product",
product=product_name, error=str(e), tenant_id=tenant_id)
failed_count += 1
continue
logger.info("Batch product resolution complete",
total=len(request.products),
created=created_count,
resolved=resolved_count,
failed=failed_count,
tenant_id=tenant_id)
return BatchProductResolutionResponse(
product_mappings=product_mappings,
created_count=created_count,
resolved_count=resolved_count,
failed_count=failed_count
)
except Exception as e:
logger.error("Batch product resolution failed",
error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Batch resolution failed: {str(e)}")

View File

@@ -753,6 +753,67 @@ class InventoryService:
)
raise
# ===== BATCH OPERATIONS FOR SALES IMPORT =====
async def search_ingredients_by_name(
self,
product_name: str,
tenant_id: UUID,
db
) -> Optional[Ingredient]:
"""Search for an ingredient by name (case-insensitive exact match)"""
try:
repository = IngredientRepository(db)
ingredients = await repository.search_ingredients(
tenant_id=tenant_id,
search_term=product_name,
skip=0,
limit=10
)
product_name_lower = product_name.lower().strip()
for ingredient in ingredients:
if ingredient.name.lower().strip() == product_name_lower:
return ingredient
return None
except Exception as e:
logger.warning("Error searching ingredients by name",
product_name=product_name, error=str(e), tenant_id=tenant_id)
return None
async def create_ingredient_fast(
self,
ingredient_data: Dict[str, Any],
tenant_id: UUID,
db
) -> Ingredient:
"""Create ingredient without full validation for batch operations"""
try:
repository = IngredientRepository(db)
ingredient_create = IngredientCreate(
name=ingredient_data.get('name'),
product_type=ingredient_data.get('type', 'finished_product'),
unit_of_measure=ingredient_data.get('unit', 'units'),
low_stock_threshold=ingredient_data.get('current_stock', 0),
reorder_point=max(ingredient_data.get('reorder_point', 1),
ingredient_data.get('current_stock', 0) + 1),
average_cost=ingredient_data.get('cost_per_unit', 0.0),
ingredient_category=ingredient_data.get('category') if ingredient_data.get('type') == 'ingredient' else None,
product_category=ingredient_data.get('category') if ingredient_data.get('type') == 'finished_product' else None
)
ingredient = await repository.create_ingredient(ingredient_create, tenant_id)
logger.debug("Created ingredient fast", ingredient_id=ingredient.id, name=ingredient.name)
return ingredient
except Exception as e:
logger.error("Failed to create ingredient fast",
error=str(e), ingredient_data=ingredient_data, tenant_id=tenant_id)
raise
# ===== PRIVATE HELPER METHODS =====
async def _validate_ingredient_data(self, ingredient_data: IngredientCreate, tenant_id: UUID):