328 lines
12 KiB
Python
328 lines
12 KiB
Python
"""
|
|
物料管理路由模块
|
|
|
|
提供物料价格历史和供应商关联管理功能,包括:
|
|
- 物料价格历史记录
|
|
- 物料供应商关联管理
|
|
- 物料价格趋势分析
|
|
|
|
路由前缀: /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.flush()
|
|
await db_session.refresh(price_history)
|
|
|
|
# 更新产品的成本价格为最新价格
|
|
product.cost_price = price_data.price
|
|
await db_session.flush()
|
|
|
|
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.flush()
|
|
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.flush()
|
|
|
|
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
|
|
]
|