""" 物料管理路由模块 提供物料价格历史和供应商关联管理功能,包括: - 物料价格历史记录 - 物料供应商关联管理 - 物料价格趋势分析 路由前缀: /api/materials """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, desc from typing import Optional, List from shared.database.database import get_db_session from shared.services.auth_service import get_current_active_user from shared.models.database import User, Product, MaterialPriceHistory, MaterialSupplier, Supplier from .schemas import ( MaterialPriceHistoryCreate, MaterialPriceHistoryResponse, MaterialSupplierCreate, MaterialSupplierResponse, MaterialPriceTrendResponse ) router = APIRouter(prefix="/materials", tags=["物料管理"]) async def _get_product(db_session: AsyncSession, product_id: int) -> Product: result = await db_session.execute( select(Product).where(Product.id == product_id, Product.is_active == True) ) product = result.scalar_one_or_none() if not product: raise HTTPException(status_code=404, detail="物料不存在") if product.item_type != "material": raise HTTPException(status_code=400, detail="仅物料类型支持价格历史管理") return product @router.post("/{product_id}/price-history", response_model=MaterialPriceHistoryResponse, status_code=201) async def add_material_price_history( product_id: int, price_data: MaterialPriceHistoryCreate, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): product = await _get_product(db_session, product_id) # 检查供应商是否存在 if price_data.supplier_id: supplier_result = await db_session.execute( select(Supplier).where(Supplier.id == price_data.supplier_id, Supplier.is_active == True) ) if not supplier_result.scalar_one_or_none(): raise HTTPException(status_code=400, detail="供应商不存在") price_history = MaterialPriceHistory( product_id=product_id, price=price_data.price, supplier_id=price_data.supplier_id, remark=price_data.remark ) db_session.add(price_history) await db_session.commit() await db_session.refresh(price_history) # 更新产品的成本价格为最新价格 product.cost_price = price_data.price await db_session.commit() return MaterialPriceHistoryResponse( id=price_history.id, product_id=price_history.product_id, product_sku=product.sku, product_name=product.name, price=price_history.price, effective_date=price_history.effective_date, supplier_id=price_history.supplier_id, supplier_name=price_history.supplier.name if price_history.supplier else None, remark=price_history.remark, created_at=price_history.created_at ) @router.get("/{product_id}/price-history", response_model=List[MaterialPriceHistoryResponse]) async def get_material_price_history( product_id: int, limit: int = Query(20, ge=1, le=100), db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): product = await _get_product(db_session, product_id) result = await db_session.execute( select(MaterialPriceHistory) .where(MaterialPriceHistory.product_id == product_id) .order_by(desc(MaterialPriceHistory.effective_date)) .limit(limit) ) price_history_list = result.scalars().all() return [ MaterialPriceHistoryResponse( id=ph.id, product_id=ph.product_id, product_sku=product.sku, product_name=product.name, price=ph.price, effective_date=ph.effective_date, supplier_id=ph.supplier_id, supplier_name=ph.supplier.name if ph.supplier else None, remark=ph.remark, created_at=ph.created_at ) for ph in price_history_list ] @router.get("/{product_id}/price-trend", response_model=MaterialPriceTrendResponse) async def get_material_price_trend( product_id: int, months: int = Query(6, ge=1, le=24), db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): product = await _get_product(db_session, product_id) # 计算价格趋势 result = await db_session.execute( select(MaterialPriceHistory) .where(MaterialPriceHistory.product_id == product_id) .order_by(desc(MaterialPriceHistory.effective_date)) .limit(months) ) price_history_list = result.scalars().all() if not price_history_list: raise HTTPException(status_code=404, detail="无价格历史记录") prices = [ph.price for ph in reversed(price_history_list)] dates = [ph.effective_date for ph in reversed(price_history_list)] # 计算价格变化 current_price = price_history_list[0].price first_price = price_history_list[-1].price price_change = current_price - first_price price_change_percent = (price_change / first_price * 100) if first_price > 0 else 0 return MaterialPriceTrendResponse( product_id=product_id, product_sku=product.sku, product_name=product.name, current_price=current_price, price_change=round(price_change, 2), price_change_percent=round(price_change_percent, 2), price_history=[ { "date": ph.effective_date, "price": ph.price, "supplier_name": ph.supplier.name if ph.supplier else None } for ph in price_history_list ] ) @router.post("/{product_id}/suppliers", response_model=MaterialSupplierResponse, status_code=201) async def add_material_supplier( product_id: int, supplier_data: MaterialSupplierCreate, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): product = await _get_product(db_session, product_id) # 检查供应商是否存在 supplier_result = await db_session.execute( select(Supplier).where(Supplier.id == supplier_data.supplier_id, Supplier.is_active == True) ) supplier = supplier_result.scalar_one_or_none() if not supplier: raise HTTPException(status_code=400, detail="供应商不存在") # 检查是否已存在关联 existing = await db_session.execute( select(MaterialSupplier) .where( MaterialSupplier.product_id == product_id, MaterialSupplier.supplier_id == supplier_data.supplier_id ) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="该供应商已关联到该物料") # 如果设置为主要供应商,将其他供应商设置为非主要 if supplier_data.is_primary: await db_session.execute( MaterialSupplier.__table__.update() .where(MaterialSupplier.product_id == product_id) .values(is_primary=False) ) material_supplier = MaterialSupplier( product_id=product_id, supplier_id=supplier_data.supplier_id, is_primary=supplier_data.is_primary, contact_person=supplier_data.contact_person, contact_phone=supplier_data.contact_phone, lead_time=supplier_data.lead_time, min_order_quantity=supplier_data.min_order_quantity ) db_session.add(material_supplier) await db_session.commit() await db_session.refresh(material_supplier) return MaterialSupplierResponse( id=material_supplier.id, product_id=material_supplier.product_id, product_sku=product.sku, product_name=product.name, supplier_id=material_supplier.supplier_id, supplier_name=supplier.name, is_primary=material_supplier.is_primary, contact_person=material_supplier.contact_person, contact_phone=material_supplier.contact_phone, lead_time=material_supplier.lead_time, min_order_quantity=material_supplier.min_order_quantity, created_at=material_supplier.created_at, updated_at=material_supplier.updated_at ) @router.get("/{product_id}/suppliers", response_model=List[MaterialSupplierResponse]) async def get_material_suppliers( product_id: int, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): product = await _get_product(db_session, product_id) result = await db_session.execute( select(MaterialSupplier) .where(MaterialSupplier.product_id == product_id) .order_by(MaterialSupplier.is_primary.desc(), MaterialSupplier.id.asc()) ) supplier_list = result.scalars().all() return [ MaterialSupplierResponse( id=ms.id, product_id=ms.product_id, product_sku=product.sku, product_name=product.name, supplier_id=ms.supplier_id, supplier_name=ms.supplier.name if ms.supplier else None, is_primary=ms.is_primary, contact_person=ms.contact_person, contact_phone=ms.contact_phone, lead_time=ms.lead_time, min_order_quantity=ms.min_order_quantity, created_at=ms.created_at, updated_at=ms.updated_at ) for ms in supplier_list ] @router.delete("/suppliers/{supplier_id}") async def remove_material_supplier( supplier_id: int, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): result = await db_session.execute( select(MaterialSupplier).where(MaterialSupplier.id == supplier_id) ) material_supplier = result.scalar_one_or_none() if not material_supplier: raise HTTPException(status_code=404, detail="物料供应商关联不存在") await db_session.delete(material_supplier) await db_session.commit() return {"message": "物料供应商关联已删除"} @router.get("/suppliers/{supplier_id}/materials", response_model=List[MaterialSupplierResponse]) async def get_supplier_materials( supplier_id: int, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): # 检查供应商是否存在 supplier_result = await db_session.execute( select(Supplier).where(Supplier.id == supplier_id, Supplier.is_active == True) ) supplier = supplier_result.scalar_one_or_none() if not supplier: raise HTTPException(status_code=404, detail="供应商不存在") result = await db_session.execute( select(MaterialSupplier) .where(MaterialSupplier.supplier_id == supplier_id) .order_by(MaterialSupplier.is_primary.desc(), MaterialSupplier.id.asc()) ) material_list = result.scalars().all() return [ MaterialSupplierResponse( id=ms.id, product_id=ms.product_id, product_sku=ms.product.sku if ms.product else None, product_name=ms.product.name if ms.product else None, supplier_id=ms.supplier_id, supplier_name=supplier.name, is_primary=ms.is_primary, contact_person=ms.contact_person, contact_phone=ms.contact_phone, lead_time=ms.lead_time, min_order_quantity=ms.min_order_quantity, created_at=ms.created_at, updated_at=ms.updated_at ) for ms in material_list ]