init
This commit is contained in:
@@ -40,3 +40,8 @@ RUSTFS_PRESIGNED_URL_EXPIRES=3600
|
|||||||
SECRET_KEY=your-secret-key-change-in-production-min-32-chars
|
SECRET_KEY=your-secret-key-change-in-production-min-32-chars
|
||||||
ALGORITHM=HS256
|
ALGORITHM=HS256
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=1440
|
ACCESS_TOKEN_EXPIRE_MINUTES=1440
|
||||||
|
|
||||||
|
# FreeCAD 验证配置(可选)
|
||||||
|
# 启用后会增加处理时间,默认禁用
|
||||||
|
ENABLE_FREECAD_VERIFICATION=false
|
||||||
|
FREECAD_VERIFICATION_TIMEOUT=120
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ class Settings:
|
|||||||
self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123')
|
self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123')
|
||||||
self.ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@gemold.com')
|
self.ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@gemold.com')
|
||||||
self.ADMIN_FULL_NAME = os.getenv('ADMIN_FULL_NAME', '系统管理员')
|
self.ADMIN_FULL_NAME = os.getenv('ADMIN_FULL_NAME', '系统管理员')
|
||||||
|
|
||||||
|
# FreeCAD 验证配置
|
||||||
|
self.ENABLE_FREECAD_VERIFICATION = os.getenv('ENABLE_FREECAD_VERIFICATION', 'false').lower() == 'true'
|
||||||
|
self.FREECAD_VERIFICATION_TIMEOUT = int(os.getenv('FREECAD_VERIFICATION_TIMEOUT', '120'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DATABASE_URL(self) -> str:
|
def DATABASE_URL(self) -> str:
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
进销存管理模块
|
||||||
|
|
||||||
|
本模块提供完整的进销存管理系统功能,包括:
|
||||||
|
- 产品管理:产品的增删改查
|
||||||
|
- 供应商管理:供应商信息管理
|
||||||
|
- 客户管理:客户信息管理
|
||||||
|
- 仓库管理:仓库信息管理
|
||||||
|
- 库存管理:库存查询和统计
|
||||||
|
- 库存变动:入库、出库、调整等操作
|
||||||
|
- 采购订单:采购订单管理
|
||||||
|
- 销售订单:销售订单管理
|
||||||
|
- 仪表盘:系统统计数据展示
|
||||||
|
|
||||||
|
所有路由统一通过 inventory_router 导出,路由前缀为 /api
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from .product_routes import router as product_router
|
||||||
|
from .supplier_routes import router as supplier_router
|
||||||
|
from .customer_routes import router as customer_router
|
||||||
|
from .warehouse_routes import router as warehouse_router
|
||||||
|
from .inventory_routes import router as inventory_router
|
||||||
|
from .stock_movement_routes import router as stock_movement_router
|
||||||
|
from .purchase_order_routes import router as purchase_order_router
|
||||||
|
from .sales_order_routes import router as sales_order_router
|
||||||
|
from .dashboard_routes import router as dashboard_router
|
||||||
|
|
||||||
|
inventory_router = APIRouter(prefix="/api", tags=["进销存"])
|
||||||
|
|
||||||
|
inventory_router.include_router(product_router)
|
||||||
|
inventory_router.include_router(supplier_router)
|
||||||
|
inventory_router.include_router(customer_router)
|
||||||
|
inventory_router.include_router(warehouse_router)
|
||||||
|
inventory_router.include_router(inventory_router)
|
||||||
|
inventory_router.include_router(stock_movement_router)
|
||||||
|
inventory_router.include_router(purchase_order_router)
|
||||||
|
inventory_router.include_router(sales_order_router)
|
||||||
|
inventory_router.include_router(dashboard_router)
|
||||||
|
|
||||||
|
__all__ = ["inventory_router"]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
客户管理路由模块
|
||||||
|
|
||||||
|
提供客户信息的管理功能,包括:
|
||||||
|
- 客户列表查询(支持分页、搜索)
|
||||||
|
- 创建新客户(自动生成客户编码)
|
||||||
|
|
||||||
|
路由前缀: /api/customers
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import User, Customer
|
||||||
|
from .schemas import CustomerCreate, CustomerResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/customers", tags=["客户管理"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[CustomerResponse])
|
||||||
|
async def list_customers(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
search: Optional[str] = None,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = select(Customer).where(Customer.is_active == True)
|
||||||
|
if search:
|
||||||
|
query = query.where(Customer.name.ilike(f"%{search}%"))
|
||||||
|
query = query.offset(skip).limit(limit).order_by(Customer.created_at.desc())
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
return [CustomerResponse.from_orm(c) for c in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=CustomerResponse, status_code=201)
|
||||||
|
async def create_customer(
|
||||||
|
customer_data: CustomerCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
data = customer_data.dict()
|
||||||
|
if not data.get("code"):
|
||||||
|
data["code"] = f"C{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
|
|
||||||
|
customer = Customer(**data)
|
||||||
|
db_session.add(customer)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(customer)
|
||||||
|
return CustomerResponse.from_orm(customer)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
仪表盘路由模块
|
||||||
|
|
||||||
|
提供进销存系统的仪表盘统计数据,包括:
|
||||||
|
- 基础数据统计(产品数、供应商数、客户数、仓库数)
|
||||||
|
- 库存统计(总库存量、库存总价值)
|
||||||
|
- 订单统计(待处理采购订单、待处理销售订单)
|
||||||
|
- 低库存产品预警(库存量低于最小库存的产品列表)
|
||||||
|
|
||||||
|
路由前缀: /api/dashboard
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import (
|
||||||
|
User, Product, Supplier, Customer, Warehouse,
|
||||||
|
Inventory, PurchaseOrder, SalesOrder
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/dashboard", tags=["仪表盘"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def get_dashboard(
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
product_count = await db_session.scalar(select(func.count(Product.id)).where(Product.is_active == True))
|
||||||
|
supplier_count = await db_session.scalar(select(func.count(Supplier.id)).where(Supplier.is_active == True))
|
||||||
|
customer_count = await db_session.scalar(select(func.count(Customer.id)).where(Customer.is_active == True))
|
||||||
|
warehouse_count = await db_session.scalar(select(func.count(Warehouse.id)).where(Warehouse.is_active == True))
|
||||||
|
|
||||||
|
total_stock = await db_session.scalar(select(func.sum(Inventory.quantity))) or 0
|
||||||
|
total_value = await db_session.scalar(
|
||||||
|
select(func.sum(Inventory.quantity * Product.cost_price))
|
||||||
|
.join(Product, Inventory.product_id == Product.id)
|
||||||
|
) or 0
|
||||||
|
|
||||||
|
pending_purchase = await db_session.scalar(
|
||||||
|
select(func.count(PurchaseOrder.id)).where(PurchaseOrder.status == "pending")
|
||||||
|
)
|
||||||
|
pending_sales = await db_session.scalar(
|
||||||
|
select(func.count(SalesOrder.id)).where(SalesOrder.status == "pending")
|
||||||
|
)
|
||||||
|
|
||||||
|
low_stock_products = await db_session.execute(
|
||||||
|
select(Product, Inventory)
|
||||||
|
.join(Inventory, Product.id == Inventory.product_id)
|
||||||
|
.where(Inventory.quantity <= Product.min_stock)
|
||||||
|
.limit(10)
|
||||||
|
)
|
||||||
|
low_stock = [
|
||||||
|
{"id": p.id, "name": p.name, "sku": p.sku, "quantity": i.quantity, "min_stock": p.min_stock}
|
||||||
|
for p, i in low_stock_products.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"product_count": product_count,
|
||||||
|
"supplier_count": supplier_count,
|
||||||
|
"customer_count": customer_count,
|
||||||
|
"warehouse_count": warehouse_count,
|
||||||
|
"total_stock": total_stock,
|
||||||
|
"total_value": round(total_value, 2),
|
||||||
|
"pending_purchase": pending_purchase,
|
||||||
|
"pending_sales": pending_sales,
|
||||||
|
"low_stock_products": low_stock
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
库存管理路由模块
|
||||||
|
|
||||||
|
提供库存信息的查询功能,包括:
|
||||||
|
- 库存列表查询(支持分页、仓库筛选、产品筛选、低库存筛选)
|
||||||
|
- 显示产品库存数量、锁定数量、可用数量等信息
|
||||||
|
|
||||||
|
路由前缀: /api/inventory
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import User, Product, Warehouse, Inventory
|
||||||
|
from .schemas import InventoryResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/inventory", tags=["库存管理"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[InventoryResponse])
|
||||||
|
async def list_inventory(
|
||||||
|
warehouse_id: Optional[int] = None,
|
||||||
|
product_id: Optional[int] = None,
|
||||||
|
low_stock: bool = False,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(Inventory, Product, Warehouse)
|
||||||
|
.join(Product, Inventory.product_id == Product.id)
|
||||||
|
.join(Warehouse, Inventory.warehouse_id == Warehouse.id)
|
||||||
|
.where(Product.is_active == True)
|
||||||
|
.where(Warehouse.is_active == True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if warehouse_id:
|
||||||
|
query = query.where(Inventory.warehouse_id == warehouse_id)
|
||||||
|
if product_id:
|
||||||
|
query = query.where(Inventory.product_id == product_id)
|
||||||
|
if low_stock:
|
||||||
|
query = query.where(Inventory.quantity <= Product.min_stock)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
inventory_list = []
|
||||||
|
for inv, product, warehouse in result.all():
|
||||||
|
inventory_list.append(InventoryResponse(
|
||||||
|
id=inv.id,
|
||||||
|
product_id=inv.product_id,
|
||||||
|
product_name=product.name,
|
||||||
|
product_sku=product.sku,
|
||||||
|
warehouse_id=inv.warehouse_id,
|
||||||
|
warehouse_name=warehouse.name,
|
||||||
|
quantity=inv.quantity,
|
||||||
|
locked_quantity=inv.locked_quantity,
|
||||||
|
available_quantity=inv.available_quantity
|
||||||
|
))
|
||||||
|
|
||||||
|
return inventory_list
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
产品管理路由模块
|
||||||
|
|
||||||
|
提供产品信息的增删改查功能,包括:
|
||||||
|
- 产品列表查询(支持分页、搜索、分类筛选)
|
||||||
|
- 创建新产品(SKU唯一性校验)
|
||||||
|
- 更新产品信息
|
||||||
|
- 删除产品(软删除,需要管理员权限)
|
||||||
|
|
||||||
|
路由前缀: /api/products
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, or_
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user, get_current_admin_user
|
||||||
|
from models.database import User, Product
|
||||||
|
from .schemas import ProductCreate, ProductResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/products", tags=["产品管理"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[ProductResponse])
|
||||||
|
async def list_products(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
search: Optional[str] = None,
|
||||||
|
category: Optional[str] = None,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = select(Product).where(Product.is_active == True)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
query = query.where(or_(Product.name.ilike(f"%{search}%"), Product.sku.ilike(f"%{search}%")))
|
||||||
|
if category:
|
||||||
|
query = query.where(Product.category == category)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit).order_by(Product.created_at.desc())
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
products = result.scalars().all()
|
||||||
|
return [ProductResponse.from_orm(p) for p in products]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ProductResponse, status_code=201)
|
||||||
|
async def create_product(
|
||||||
|
product_data: ProductCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
existing = await db_session.execute(select(Product).where(Product.sku == product_data.sku))
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise HTTPException(status_code=400, detail="SKU已存在")
|
||||||
|
|
||||||
|
product = Product(**product_data.dict())
|
||||||
|
db_session.add(product)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(product)
|
||||||
|
return ProductResponse.from_orm(product)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{product_id}", response_model=ProductResponse)
|
||||||
|
async def update_product(
|
||||||
|
product_id: int,
|
||||||
|
product_data: ProductCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Product).where(Product.id == product_id))
|
||||||
|
product = result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="产品不存在")
|
||||||
|
|
||||||
|
for key, value in product_data.dict().items():
|
||||||
|
setattr(product, key, value)
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(product)
|
||||||
|
return ProductResponse.from_orm(product)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{product_id}")
|
||||||
|
async def delete_product(
|
||||||
|
product_id: int,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Product).where(Product.id == product_id))
|
||||||
|
product = result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="产品不存在")
|
||||||
|
|
||||||
|
product.is_active = False
|
||||||
|
await db_session.commit()
|
||||||
|
return {"message": "产品已删除"}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
采购订单路由模块
|
||||||
|
|
||||||
|
提供采购订单的管理功能,包括:
|
||||||
|
- 采购订单列表查询(支持分页、状态筛选)
|
||||||
|
- 创建采购订单(自动生成订单号、计算总金额)
|
||||||
|
- 采购订单明细管理
|
||||||
|
|
||||||
|
路由前缀: /api/purchase-orders
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import User, Supplier, Product, PurchaseOrder, PurchaseOrderItem
|
||||||
|
from .schemas import PurchaseOrderCreate, PurchaseOrderResponse
|
||||||
|
from .utils import generate_order_no
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/purchase-orders", tags=["采购订单"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[PurchaseOrderResponse])
|
||||||
|
async def list_purchase_orders(
|
||||||
|
status: Optional[str] = None,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(PurchaseOrder, Supplier)
|
||||||
|
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
|
||||||
|
.order_by(PurchaseOrder.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.where(PurchaseOrder.status == status)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
orders = []
|
||||||
|
for order, supplier in result.all():
|
||||||
|
orders.append(PurchaseOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
supplier_name=supplier.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
expected_date=order.expected_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
paid_amount=order.paid_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
))
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=PurchaseOrderResponse, status_code=201)
|
||||||
|
async def create_purchase_order(
|
||||||
|
order_data: PurchaseOrderCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
order = PurchaseOrder(
|
||||||
|
order_no=generate_order_no("PO"),
|
||||||
|
supplier_id=order_data.supplier_id,
|
||||||
|
expected_date=order_data.expected_date,
|
||||||
|
remark=order_data.remark,
|
||||||
|
operator_id=current_user.id,
|
||||||
|
status="draft"
|
||||||
|
)
|
||||||
|
db_session.add(order)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
total_amount = 0
|
||||||
|
for item_data in order_data.items:
|
||||||
|
item = PurchaseOrderItem(
|
||||||
|
order_id=order.id,
|
||||||
|
product_id=item_data.product_id,
|
||||||
|
quantity=item_data.quantity,
|
||||||
|
unit_price=item_data.unit_price,
|
||||||
|
amount=item_data.quantity * item_data.unit_price,
|
||||||
|
remark=item_data.remark
|
||||||
|
)
|
||||||
|
db_session.add(item)
|
||||||
|
total_amount += item.amount
|
||||||
|
|
||||||
|
order.total_amount = total_amount
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(order)
|
||||||
|
|
||||||
|
supplier = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
|
||||||
|
supplier = supplier.scalar_one()
|
||||||
|
|
||||||
|
return PurchaseOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
supplier_name=supplier.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
expected_date=order.expected_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
paid_amount=order.paid_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
销售订单路由模块
|
||||||
|
|
||||||
|
提供销售订单的管理功能,包括:
|
||||||
|
- 销售订单列表查询(支持分页、状态筛选)
|
||||||
|
- 创建销售订单(自动生成订单号、计算总金额)
|
||||||
|
- 销售订单明细管理
|
||||||
|
|
||||||
|
路由前缀: /api/sales-orders
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import User, Customer, SalesOrder, SalesOrderItem
|
||||||
|
from .schemas import SalesOrderCreate, SalesOrderResponse
|
||||||
|
from .utils import generate_order_no
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/sales-orders", tags=["销售订单"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[SalesOrderResponse])
|
||||||
|
async def list_sales_orders(
|
||||||
|
status: Optional[str] = None,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(SalesOrder, Customer)
|
||||||
|
.join(Customer, SalesOrder.customer_id == Customer.id)
|
||||||
|
.order_by(SalesOrder.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.where(SalesOrder.status == status)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
orders = []
|
||||||
|
for order, customer in result.all():
|
||||||
|
orders.append(SalesOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
customer_name=customer.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
delivery_date=order.delivery_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
received_amount=order.received_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
))
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=SalesOrderResponse, status_code=201)
|
||||||
|
async def create_sales_order(
|
||||||
|
order_data: SalesOrderCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
order = SalesOrder(
|
||||||
|
order_no=generate_order_no("SO"),
|
||||||
|
customer_id=order_data.customer_id,
|
||||||
|
delivery_date=order_data.delivery_date,
|
||||||
|
remark=order_data.remark,
|
||||||
|
operator_id=current_user.id,
|
||||||
|
status="draft"
|
||||||
|
)
|
||||||
|
db_session.add(order)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
total_amount = 0
|
||||||
|
for item_data in order_data.items:
|
||||||
|
item = SalesOrderItem(
|
||||||
|
order_id=order.id,
|
||||||
|
product_id=item_data.product_id,
|
||||||
|
quantity=item_data.quantity,
|
||||||
|
unit_price=item_data.unit_price,
|
||||||
|
amount=item_data.quantity * item_data.unit_price,
|
||||||
|
remark=item_data.remark
|
||||||
|
)
|
||||||
|
db_session.add(item)
|
||||||
|
total_amount += item.amount
|
||||||
|
|
||||||
|
order.total_amount = total_amount
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(order)
|
||||||
|
|
||||||
|
customer = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
|
||||||
|
customer = customer.scalar_one()
|
||||||
|
|
||||||
|
return SalesOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
customer_name=customer.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
delivery_date=order.delivery_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
received_amount=order.received_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from .product_schemas import ProductCreate, ProductResponse
|
||||||
|
from .supplier_schemas import SupplierCreate, SupplierResponse
|
||||||
|
from .customer_schemas import CustomerCreate, CustomerResponse
|
||||||
|
from .warehouse_schemas import WarehouseCreate, WarehouseResponse
|
||||||
|
from .inventory_schemas import InventoryResponse
|
||||||
|
from .stock_movement_schemas import StockMovementCreate, StockMovementResponse
|
||||||
|
from .purchase_order_schemas import (
|
||||||
|
PurchaseOrderCreate,
|
||||||
|
PurchaseOrderResponse,
|
||||||
|
PurchaseOrderItemCreate
|
||||||
|
)
|
||||||
|
from .sales_order_schemas import (
|
||||||
|
SalesOrderCreate,
|
||||||
|
SalesOrderResponse,
|
||||||
|
SalesOrderItemCreate
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ProductCreate", "ProductResponse",
|
||||||
|
"SupplierCreate", "SupplierResponse",
|
||||||
|
"CustomerCreate", "CustomerResponse",
|
||||||
|
"WarehouseCreate", "WarehouseResponse",
|
||||||
|
"InventoryResponse",
|
||||||
|
"StockMovementCreate", "StockMovementResponse",
|
||||||
|
"PurchaseOrderCreate", "PurchaseOrderResponse", "PurchaseOrderItemCreate",
|
||||||
|
"SalesOrderCreate", "SalesOrderResponse", "SalesOrderItemCreate",
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerCreate(BaseModel):
|
||||||
|
code: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
code: Optional[str]
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str]
|
||||||
|
phone: Optional[str]
|
||||||
|
email: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
product_id: int
|
||||||
|
product_name: str
|
||||||
|
product_sku: str
|
||||||
|
warehouse_id: int
|
||||||
|
warehouse_name: str
|
||||||
|
quantity: int
|
||||||
|
locked_quantity: int
|
||||||
|
available_quantity: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCreate(BaseModel):
|
||||||
|
sku: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
category: Optional[str] = None
|
||||||
|
unit: str = "件"
|
||||||
|
cost_price: float = 0
|
||||||
|
sale_price: float = 0
|
||||||
|
min_stock: int = 0
|
||||||
|
max_stock: int = 1000
|
||||||
|
|
||||||
|
|
||||||
|
class ProductResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
sku: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str]
|
||||||
|
category: Optional[str]
|
||||||
|
unit: str
|
||||||
|
cost_price: float
|
||||||
|
sale_price: float
|
||||||
|
min_stock: int
|
||||||
|
max_stock: int
|
||||||
|
is_active: bool
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderItemCreate(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
quantity: int
|
||||||
|
unit_price: float
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderCreate(BaseModel):
|
||||||
|
supplier_id: int
|
||||||
|
expected_date: Optional[datetime] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
items: List[PurchaseOrderItemCreate]
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
order_no: str
|
||||||
|
supplier_name: str
|
||||||
|
order_date: datetime
|
||||||
|
expected_date: Optional[datetime]
|
||||||
|
status: str
|
||||||
|
total_amount: float
|
||||||
|
paid_amount: float
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderItemCreate(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
quantity: int
|
||||||
|
unit_price: float
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderCreate(BaseModel):
|
||||||
|
customer_id: int
|
||||||
|
delivery_date: Optional[datetime] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
items: List[SalesOrderItemCreate]
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
order_no: str
|
||||||
|
customer_name: str
|
||||||
|
order_date: datetime
|
||||||
|
delivery_date: Optional[datetime]
|
||||||
|
status: str
|
||||||
|
total_amount: float
|
||||||
|
received_amount: float
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class StockMovementCreate(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
warehouse_id: int
|
||||||
|
movement_type: str
|
||||||
|
quantity: int
|
||||||
|
unit_price: Optional[float] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class StockMovementResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
product_name: str
|
||||||
|
movement_type: str
|
||||||
|
quantity: int
|
||||||
|
before_quantity: int
|
||||||
|
after_quantity: int
|
||||||
|
reference_no: Optional[str]
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierCreate(BaseModel):
|
||||||
|
code: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
code: Optional[str]
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str]
|
||||||
|
phone: Optional[str]
|
||||||
|
email: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class WarehouseCreate(BaseModel):
|
||||||
|
code: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
address: Optional[str] = None
|
||||||
|
manager: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class WarehouseResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
code: Optional[str]
|
||||||
|
name: str
|
||||||
|
address: Optional[str]
|
||||||
|
manager: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
is_default: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""
|
||||||
|
库存变动路由模块
|
||||||
|
|
||||||
|
提供库存变动的管理功能,包括:
|
||||||
|
- 创建库存变动记录(入库、出库、调整)
|
||||||
|
- 库存变动历史查询(支持分页、产品筛选、变动类型筛选)
|
||||||
|
- 自动更新库存数量
|
||||||
|
- 库存不足校验(出库时)
|
||||||
|
|
||||||
|
路由前缀: /api/stock-movements
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import User, Product, Warehouse, Inventory, StockMovement
|
||||||
|
from .schemas import StockMovementCreate, StockMovementResponse
|
||||||
|
from .utils import generate_order_no
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/stock-movements", tags=["库存变动"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=StockMovementResponse, status_code=201)
|
||||||
|
async def create_stock_movement(
|
||||||
|
movement_data: StockMovementCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
if movement_data.movement_type not in ["in", "out", "adjust"]:
|
||||||
|
raise HTTPException(status_code=400, detail="无效的变动类型")
|
||||||
|
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(Inventory)
|
||||||
|
.where(Inventory.product_id == movement_data.product_id)
|
||||||
|
.where(Inventory.warehouse_id == movement_data.warehouse_id)
|
||||||
|
)
|
||||||
|
inventory = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not inventory:
|
||||||
|
if movement_data.movement_type == "out":
|
||||||
|
raise HTTPException(status_code=400, detail="库存不足")
|
||||||
|
inventory = Inventory(
|
||||||
|
product_id=movement_data.product_id,
|
||||||
|
warehouse_id=movement_data.warehouse_id,
|
||||||
|
quantity=0
|
||||||
|
)
|
||||||
|
db_session.add(inventory)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
before_qty = inventory.quantity
|
||||||
|
|
||||||
|
if movement_data.movement_type == "in":
|
||||||
|
inventory.quantity += movement_data.quantity
|
||||||
|
elif movement_data.movement_type == "out":
|
||||||
|
if inventory.quantity < movement_data.quantity:
|
||||||
|
raise HTTPException(status_code=400, detail="库存不足")
|
||||||
|
inventory.quantity -= movement_data.quantity
|
||||||
|
else:
|
||||||
|
inventory.quantity = movement_data.quantity
|
||||||
|
|
||||||
|
after_qty = inventory.quantity
|
||||||
|
|
||||||
|
movement = StockMovement(
|
||||||
|
product_id=movement_data.product_id,
|
||||||
|
warehouse_id=movement_data.warehouse_id,
|
||||||
|
movement_type=movement_data.movement_type,
|
||||||
|
quantity=movement_data.quantity,
|
||||||
|
before_quantity=before_qty,
|
||||||
|
after_quantity=after_qty,
|
||||||
|
reference_no=generate_order_no("SM"),
|
||||||
|
unit_price=movement_data.unit_price,
|
||||||
|
total_amount=movement_data.unit_price * movement_data.quantity if movement_data.unit_price else None,
|
||||||
|
remark=movement_data.remark,
|
||||||
|
operator_id=current_user.id
|
||||||
|
)
|
||||||
|
db_session.add(movement)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
product = await db_session.execute(select(Product).where(Product.id == movement_data.product_id))
|
||||||
|
product = product.scalar_one()
|
||||||
|
|
||||||
|
return StockMovementResponse(
|
||||||
|
id=movement.id,
|
||||||
|
product_name=product.name,
|
||||||
|
movement_type=movement.movement_type,
|
||||||
|
quantity=movement.quantity,
|
||||||
|
before_quantity=movement.before_quantity,
|
||||||
|
after_quantity=movement.after_quantity,
|
||||||
|
reference_no=movement.reference_no,
|
||||||
|
remark=movement.remark,
|
||||||
|
created_at=movement.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[StockMovementResponse])
|
||||||
|
async def list_stock_movements(
|
||||||
|
product_id: Optional[int] = None,
|
||||||
|
movement_type: Optional[str] = None,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(StockMovement, Product)
|
||||||
|
.join(Product, StockMovement.product_id == Product.id)
|
||||||
|
.order_by(StockMovement.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if product_id:
|
||||||
|
query = query.where(StockMovement.product_id == product_id)
|
||||||
|
if movement_type:
|
||||||
|
query = query.where(StockMovement.movement_type == movement_type)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
movements = []
|
||||||
|
for movement, product in result.all():
|
||||||
|
movements.append(StockMovementResponse(
|
||||||
|
id=movement.id,
|
||||||
|
product_name=product.name,
|
||||||
|
movement_type=movement.movement_type,
|
||||||
|
quantity=movement.quantity,
|
||||||
|
before_quantity=movement.before_quantity,
|
||||||
|
after_quantity=movement.after_quantity,
|
||||||
|
reference_no=movement.reference_no,
|
||||||
|
remark=movement.remark,
|
||||||
|
created_at=movement.created_at
|
||||||
|
))
|
||||||
|
|
||||||
|
return movements
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
供应商管理路由模块
|
||||||
|
|
||||||
|
提供供应商信息的管理功能,包括:
|
||||||
|
- 供应商列表查询(支持分页、搜索)
|
||||||
|
- 创建新供应商(自动生成供应商编码)
|
||||||
|
|
||||||
|
路由前缀: /api/suppliers
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import User, Supplier
|
||||||
|
from .schemas import SupplierCreate, SupplierResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/suppliers", tags=["供应商管理"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[SupplierResponse])
|
||||||
|
async def list_suppliers(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
search: Optional[str] = None,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = select(Supplier).where(Supplier.is_active == True)
|
||||||
|
if search:
|
||||||
|
query = query.where(Supplier.name.ilike(f"%{search}%"))
|
||||||
|
query = query.offset(skip).limit(limit).order_by(Supplier.created_at.desc())
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
return [SupplierResponse.from_orm(s) for s in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=SupplierResponse, status_code=201)
|
||||||
|
async def create_supplier(
|
||||||
|
supplier_data: SupplierCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
data = supplier_data.dict()
|
||||||
|
if not data.get("code"):
|
||||||
|
data["code"] = f"S{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
|
|
||||||
|
supplier = Supplier(**data)
|
||||||
|
db_session.add(supplier)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(supplier)
|
||||||
|
return SupplierResponse.from_orm(supplier)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
库存管理工具函数模块
|
||||||
|
|
||||||
|
提供进销存系统通用的工具函数,包括:
|
||||||
|
- 订单编号生成器(采购订单、销售订单、库存变动等)
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def generate_order_no(prefix: str) -> str:
|
||||||
|
"""生成订单编号
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prefix: 订单类型前缀,如 PO(采购订单)、SO(销售订单)、SM(库存变动)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式为 {prefix}{YYYYMMDDHHMMSS}{4位随机字符} 的订单编号
|
||||||
|
"""
|
||||||
|
date_str = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
random_str = uuid.uuid4().hex[:4].upper()
|
||||||
|
return f"{prefix}{date_str}{random_str}"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
仓库管理路由模块
|
||||||
|
|
||||||
|
提供仓库信息的管理功能,包括:
|
||||||
|
- 仓库列表查询(按默认仓库排序)
|
||||||
|
- 创建新仓库(自动生成仓库编码)
|
||||||
|
|
||||||
|
路由前缀: /api/warehouses
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from typing import List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user
|
||||||
|
from models.database import User, Warehouse
|
||||||
|
from .schemas import WarehouseCreate, WarehouseResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/warehouses", tags=["仓库管理"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[WarehouseResponse])
|
||||||
|
async def list_warehouses(
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(Warehouse).where(Warehouse.is_active == True).order_by(Warehouse.is_default.desc())
|
||||||
|
)
|
||||||
|
return [WarehouseResponse.from_orm(w) for w in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=WarehouseResponse, status_code=201)
|
||||||
|
async def create_warehouse(
|
||||||
|
warehouse_data: WarehouseCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
data = warehouse_data.dict()
|
||||||
|
if not data.get("code"):
|
||||||
|
data["code"] = f"W{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
|
|
||||||
|
warehouse = Warehouse(**data)
|
||||||
|
db_session.add(warehouse)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(warehouse)
|
||||||
|
return WarehouseResponse.from_orm(warehouse)
|
||||||
@@ -1,769 +0,0 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import select, func, and_, or_
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from typing import Optional, List
|
|
||||||
from datetime import datetime
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from database.database import get_db_session
|
|
||||||
from services.auth_service import get_current_active_user, get_current_admin_user
|
|
||||||
from models.database import (
|
|
||||||
User, Product, Supplier, Customer, Warehouse, Inventory,
|
|
||||||
StockMovement, PurchaseOrder, PurchaseOrderItem,
|
|
||||||
SalesOrder, SalesOrderItem
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/inventory", tags=["进销存"])
|
|
||||||
|
|
||||||
|
|
||||||
def generate_order_no(prefix: str) -> str:
|
|
||||||
date_str = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
||||||
random_str = uuid.uuid4().hex[:4].upper()
|
|
||||||
return f"{prefix}{date_str}{random_str}"
|
|
||||||
|
|
||||||
|
|
||||||
class ProductCreate(BaseModel):
|
|
||||||
sku: str
|
|
||||||
name: str
|
|
||||||
description: Optional[str] = None
|
|
||||||
category: Optional[str] = None
|
|
||||||
unit: str = "件"
|
|
||||||
cost_price: float = 0
|
|
||||||
sale_price: float = 0
|
|
||||||
min_stock: int = 0
|
|
||||||
max_stock: int = 1000
|
|
||||||
|
|
||||||
|
|
||||||
class ProductResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
sku: str
|
|
||||||
name: str
|
|
||||||
description: Optional[str]
|
|
||||||
category: Optional[str]
|
|
||||||
unit: str
|
|
||||||
cost_price: float
|
|
||||||
sale_price: float
|
|
||||||
min_stock: int
|
|
||||||
max_stock: int
|
|
||||||
is_active: bool
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierCreate(BaseModel):
|
|
||||||
code: Optional[str] = None
|
|
||||||
name: str
|
|
||||||
contact_person: Optional[str] = None
|
|
||||||
phone: Optional[str] = None
|
|
||||||
email: Optional[str] = None
|
|
||||||
address: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
code: Optional[str]
|
|
||||||
name: str
|
|
||||||
contact_person: Optional[str]
|
|
||||||
phone: Optional[str]
|
|
||||||
email: Optional[str]
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerCreate(BaseModel):
|
|
||||||
code: Optional[str] = None
|
|
||||||
name: str
|
|
||||||
contact_person: Optional[str] = None
|
|
||||||
phone: Optional[str] = None
|
|
||||||
email: Optional[str] = None
|
|
||||||
address: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
code: Optional[str]
|
|
||||||
name: str
|
|
||||||
contact_person: Optional[str]
|
|
||||||
phone: Optional[str]
|
|
||||||
email: Optional[str]
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class WarehouseCreate(BaseModel):
|
|
||||||
code: Optional[str] = None
|
|
||||||
name: str
|
|
||||||
address: Optional[str] = None
|
|
||||||
manager: Optional[str] = None
|
|
||||||
phone: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class WarehouseResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
code: Optional[str]
|
|
||||||
name: str
|
|
||||||
address: Optional[str]
|
|
||||||
manager: Optional[str]
|
|
||||||
is_active: bool
|
|
||||||
is_default: bool
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
product_id: int
|
|
||||||
product_name: str
|
|
||||||
product_sku: str
|
|
||||||
warehouse_id: int
|
|
||||||
warehouse_name: str
|
|
||||||
quantity: int
|
|
||||||
locked_quantity: int
|
|
||||||
available_quantity: int
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class StockMovementCreate(BaseModel):
|
|
||||||
product_id: int
|
|
||||||
warehouse_id: int
|
|
||||||
movement_type: str
|
|
||||||
quantity: int
|
|
||||||
unit_price: Optional[float] = None
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class StockMovementResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
product_name: str
|
|
||||||
movement_type: str
|
|
||||||
quantity: int
|
|
||||||
before_quantity: int
|
|
||||||
after_quantity: int
|
|
||||||
reference_no: Optional[str]
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderItemCreate(BaseModel):
|
|
||||||
product_id: int
|
|
||||||
quantity: int
|
|
||||||
unit_price: float
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderCreate(BaseModel):
|
|
||||||
supplier_id: int
|
|
||||||
expected_date: Optional[datetime] = None
|
|
||||||
remark: Optional[str] = None
|
|
||||||
items: List[PurchaseOrderItemCreate]
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
order_no: str
|
|
||||||
supplier_name: str
|
|
||||||
order_date: datetime
|
|
||||||
expected_date: Optional[datetime]
|
|
||||||
status: str
|
|
||||||
total_amount: float
|
|
||||||
paid_amount: float
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderItemCreate(BaseModel):
|
|
||||||
product_id: int
|
|
||||||
quantity: int
|
|
||||||
unit_price: float
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderCreate(BaseModel):
|
|
||||||
customer_id: int
|
|
||||||
delivery_date: Optional[datetime] = None
|
|
||||||
remark: Optional[str] = None
|
|
||||||
items: List[SalesOrderItemCreate]
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderResponse(BaseModel):
|
|
||||||
id: int
|
|
||||||
order_no: str
|
|
||||||
customer_name: str
|
|
||||||
order_date: datetime
|
|
||||||
delivery_date: Optional[datetime]
|
|
||||||
status: str
|
|
||||||
total_amount: float
|
|
||||||
received_amount: float
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/dashboard")
|
|
||||||
async def get_dashboard(
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
product_count = await db_session.scalar(select(func.count(Product.id)).where(Product.is_active == True))
|
|
||||||
supplier_count = await db_session.scalar(select(func.count(Supplier.id)).where(Supplier.is_active == True))
|
|
||||||
customer_count = await db_session.scalar(select(func.count(Customer.id)).where(Customer.is_active == True))
|
|
||||||
warehouse_count = await db_session.scalar(select(func.count(Warehouse.id)).where(Warehouse.is_active == True))
|
|
||||||
|
|
||||||
total_stock = await db_session.scalar(select(func.sum(Inventory.quantity))) or 0
|
|
||||||
total_value = await db_session.scalar(
|
|
||||||
select(func.sum(Inventory.quantity * Product.cost_price))
|
|
||||||
.join(Product, Inventory.product_id == Product.id)
|
|
||||||
) or 0
|
|
||||||
|
|
||||||
pending_purchase = await db_session.scalar(
|
|
||||||
select(func.count(PurchaseOrder.id)).where(PurchaseOrder.status == "pending")
|
|
||||||
)
|
|
||||||
pending_sales = await db_session.scalar(
|
|
||||||
select(func.count(SalesOrder.id)).where(SalesOrder.status == "pending")
|
|
||||||
)
|
|
||||||
|
|
||||||
low_stock_products = await db_session.execute(
|
|
||||||
select(Product, Inventory)
|
|
||||||
.join(Inventory, Product.id == Inventory.product_id)
|
|
||||||
.where(Inventory.quantity <= Product.min_stock)
|
|
||||||
.limit(10)
|
|
||||||
)
|
|
||||||
low_stock = [
|
|
||||||
{"id": p.id, "name": p.name, "sku": p.sku, "quantity": i.quantity, "min_stock": p.min_stock}
|
|
||||||
for p, i in low_stock_products.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"product_count": product_count,
|
|
||||||
"supplier_count": supplier_count,
|
|
||||||
"customer_count": customer_count,
|
|
||||||
"warehouse_count": warehouse_count,
|
|
||||||
"total_stock": total_stock,
|
|
||||||
"total_value": round(total_value, 2),
|
|
||||||
"pending_purchase": pending_purchase,
|
|
||||||
"pending_sales": pending_sales,
|
|
||||||
"low_stock_products": low_stock
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/products", response_model=List[ProductResponse])
|
|
||||||
async def list_products(
|
|
||||||
skip: int = Query(0, ge=0),
|
|
||||||
limit: int = Query(20, ge=1, le=100),
|
|
||||||
search: Optional[str] = None,
|
|
||||||
category: Optional[str] = None,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
query = select(Product).where(Product.is_active == True)
|
|
||||||
|
|
||||||
if search:
|
|
||||||
query = query.where(or_(Product.name.ilike(f"%{search}%"), Product.sku.ilike(f"%{search}%")))
|
|
||||||
if category:
|
|
||||||
query = query.where(Product.category == category)
|
|
||||||
|
|
||||||
query = query.offset(skip).limit(limit).order_by(Product.created_at.desc())
|
|
||||||
result = await db_session.execute(query)
|
|
||||||
products = result.scalars().all()
|
|
||||||
return [ProductResponse.from_orm(p) for p in products]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/products", response_model=ProductResponse, status_code=201)
|
|
||||||
async def create_product(
|
|
||||||
product_data: ProductCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
existing = await db_session.execute(select(Product).where(Product.sku == product_data.sku))
|
|
||||||
if existing.scalar_one_or_none():
|
|
||||||
raise HTTPException(status_code=400, detail="SKU已存在")
|
|
||||||
|
|
||||||
product = Product(**product_data.dict())
|
|
||||||
db_session.add(product)
|
|
||||||
await db_session.commit()
|
|
||||||
await db_session.refresh(product)
|
|
||||||
return ProductResponse.from_orm(product)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/products/{product_id}", response_model=ProductResponse)
|
|
||||||
async def update_product(
|
|
||||||
product_id: int,
|
|
||||||
product_data: ProductCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
result = await db_session.execute(select(Product).where(Product.id == product_id))
|
|
||||||
product = result.scalar_one_or_none()
|
|
||||||
if not product:
|
|
||||||
raise HTTPException(status_code=404, detail="产品不存在")
|
|
||||||
|
|
||||||
for key, value in product_data.dict().items():
|
|
||||||
setattr(product, key, value)
|
|
||||||
|
|
||||||
await db_session.commit()
|
|
||||||
await db_session.refresh(product)
|
|
||||||
return ProductResponse.from_orm(product)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/products/{product_id}")
|
|
||||||
async def delete_product(
|
|
||||||
product_id: int,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_admin_user)
|
|
||||||
):
|
|
||||||
result = await db_session.execute(select(Product).where(Product.id == product_id))
|
|
||||||
product = result.scalar_one_or_none()
|
|
||||||
if not product:
|
|
||||||
raise HTTPException(status_code=404, detail="产品不存在")
|
|
||||||
|
|
||||||
product.is_active = False
|
|
||||||
await db_session.commit()
|
|
||||||
return {"message": "产品已删除"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/suppliers", response_model=List[SupplierResponse])
|
|
||||||
async def list_suppliers(
|
|
||||||
skip: int = Query(0, ge=0),
|
|
||||||
limit: int = Query(20, ge=1, le=100),
|
|
||||||
search: Optional[str] = None,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
query = select(Supplier).where(Supplier.is_active == True)
|
|
||||||
if search:
|
|
||||||
query = query.where(Supplier.name.ilike(f"%{search}%"))
|
|
||||||
query = query.offset(skip).limit(limit).order_by(Supplier.created_at.desc())
|
|
||||||
result = await db_session.execute(query)
|
|
||||||
return [SupplierResponse.from_orm(s) for s in result.scalars().all()]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/suppliers", response_model=SupplierResponse, status_code=201)
|
|
||||||
async def create_supplier(
|
|
||||||
supplier_data: SupplierCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
data = supplier_data.dict()
|
|
||||||
if not data.get("code"):
|
|
||||||
data["code"] = f"S{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
||||||
|
|
||||||
supplier = Supplier(**data)
|
|
||||||
db_session.add(supplier)
|
|
||||||
await db_session.commit()
|
|
||||||
await db_session.refresh(supplier)
|
|
||||||
return SupplierResponse.from_orm(supplier)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/customers", response_model=List[CustomerResponse])
|
|
||||||
async def list_customers(
|
|
||||||
skip: int = Query(0, ge=0),
|
|
||||||
limit: int = Query(20, ge=1, le=100),
|
|
||||||
search: Optional[str] = None,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
query = select(Customer).where(Customer.is_active == True)
|
|
||||||
if search:
|
|
||||||
query = query.where(Customer.name.ilike(f"%{search}%"))
|
|
||||||
query = query.offset(skip).limit(limit).order_by(Customer.created_at.desc())
|
|
||||||
result = await db_session.execute(query)
|
|
||||||
return [CustomerResponse.from_orm(c) for c in result.scalars().all()]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/customers", response_model=CustomerResponse, status_code=201)
|
|
||||||
async def create_customer(
|
|
||||||
customer_data: CustomerCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
data = customer_data.dict()
|
|
||||||
if not data.get("code"):
|
|
||||||
data["code"] = f"C{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
||||||
|
|
||||||
customer = Customer(**data)
|
|
||||||
db_session.add(customer)
|
|
||||||
await db_session.commit()
|
|
||||||
await db_session.refresh(customer)
|
|
||||||
return CustomerResponse.from_orm(customer)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/warehouses", response_model=List[WarehouseResponse])
|
|
||||||
async def list_warehouses(
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
result = await db_session.execute(
|
|
||||||
select(Warehouse).where(Warehouse.is_active == True).order_by(Warehouse.is_default.desc())
|
|
||||||
)
|
|
||||||
return [WarehouseResponse.from_orm(w) for w in result.scalars().all()]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/warehouses", response_model=WarehouseResponse, status_code=201)
|
|
||||||
async def create_warehouse(
|
|
||||||
warehouse_data: WarehouseCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
data = warehouse_data.dict()
|
|
||||||
if not data.get("code"):
|
|
||||||
data["code"] = f"W{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
||||||
|
|
||||||
warehouse = Warehouse(**data)
|
|
||||||
db_session.add(warehouse)
|
|
||||||
await db_session.commit()
|
|
||||||
await db_session.refresh(warehouse)
|
|
||||||
return WarehouseResponse.from_orm(warehouse)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/inventory", response_model=List[InventoryResponse])
|
|
||||||
async def list_inventory(
|
|
||||||
warehouse_id: Optional[int] = None,
|
|
||||||
product_id: Optional[int] = None,
|
|
||||||
low_stock: bool = False,
|
|
||||||
skip: int = Query(0, ge=0),
|
|
||||||
limit: int = Query(20, ge=1, le=100),
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
query = (
|
|
||||||
select(Inventory, Product, Warehouse)
|
|
||||||
.join(Product, Inventory.product_id == Product.id)
|
|
||||||
.join(Warehouse, Inventory.warehouse_id == Warehouse.id)
|
|
||||||
.where(Product.is_active == True)
|
|
||||||
.where(Warehouse.is_active == True)
|
|
||||||
)
|
|
||||||
|
|
||||||
if warehouse_id:
|
|
||||||
query = query.where(Inventory.warehouse_id == warehouse_id)
|
|
||||||
if product_id:
|
|
||||||
query = query.where(Inventory.product_id == product_id)
|
|
||||||
if low_stock:
|
|
||||||
query = query.where(Inventory.quantity <= Product.min_stock)
|
|
||||||
|
|
||||||
query = query.offset(skip).limit(limit)
|
|
||||||
result = await db_session.execute(query)
|
|
||||||
|
|
||||||
inventory_list = []
|
|
||||||
for inv, product, warehouse in result.all():
|
|
||||||
inventory_list.append(InventoryResponse(
|
|
||||||
id=inv.id,
|
|
||||||
product_id=inv.product_id,
|
|
||||||
product_name=product.name,
|
|
||||||
product_sku=product.sku,
|
|
||||||
warehouse_id=inv.warehouse_id,
|
|
||||||
warehouse_name=warehouse.name,
|
|
||||||
quantity=inv.quantity,
|
|
||||||
locked_quantity=inv.locked_quantity,
|
|
||||||
available_quantity=inv.available_quantity
|
|
||||||
))
|
|
||||||
|
|
||||||
return inventory_list
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/stock-movements", response_model=StockMovementResponse, status_code=201)
|
|
||||||
async def create_stock_movement(
|
|
||||||
movement_data: StockMovementCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
if movement_data.movement_type not in ["in", "out", "adjust"]:
|
|
||||||
raise HTTPException(status_code=400, detail="无效的变动类型")
|
|
||||||
|
|
||||||
result = await db_session.execute(
|
|
||||||
select(Inventory)
|
|
||||||
.where(Inventory.product_id == movement_data.product_id)
|
|
||||||
.where(Inventory.warehouse_id == movement_data.warehouse_id)
|
|
||||||
)
|
|
||||||
inventory = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if not inventory:
|
|
||||||
if movement_data.movement_type == "out":
|
|
||||||
raise HTTPException(status_code=400, detail="库存不足")
|
|
||||||
inventory = Inventory(
|
|
||||||
product_id=movement_data.product_id,
|
|
||||||
warehouse_id=movement_data.warehouse_id,
|
|
||||||
quantity=0
|
|
||||||
)
|
|
||||||
db_session.add(inventory)
|
|
||||||
await db_session.flush()
|
|
||||||
|
|
||||||
before_qty = inventory.quantity
|
|
||||||
|
|
||||||
if movement_data.movement_type == "in":
|
|
||||||
inventory.quantity += movement_data.quantity
|
|
||||||
elif movement_data.movement_type == "out":
|
|
||||||
if inventory.quantity < movement_data.quantity:
|
|
||||||
raise HTTPException(status_code=400, detail="库存不足")
|
|
||||||
inventory.quantity -= movement_data.quantity
|
|
||||||
else:
|
|
||||||
inventory.quantity = movement_data.quantity
|
|
||||||
|
|
||||||
after_qty = inventory.quantity
|
|
||||||
|
|
||||||
movement = StockMovement(
|
|
||||||
product_id=movement_data.product_id,
|
|
||||||
warehouse_id=movement_data.warehouse_id,
|
|
||||||
movement_type=movement_data.movement_type,
|
|
||||||
quantity=movement_data.quantity,
|
|
||||||
before_quantity=before_qty,
|
|
||||||
after_quantity=after_qty,
|
|
||||||
reference_no=generate_order_no("SM"),
|
|
||||||
unit_price=movement_data.unit_price,
|
|
||||||
total_amount=movement_data.unit_price * movement_data.quantity if movement_data.unit_price else None,
|
|
||||||
remark=movement_data.remark,
|
|
||||||
operator_id=current_user.id
|
|
||||||
)
|
|
||||||
db_session.add(movement)
|
|
||||||
await db_session.commit()
|
|
||||||
|
|
||||||
product = await db_session.execute(select(Product).where(Product.id == movement_data.product_id))
|
|
||||||
product = product.scalar_one()
|
|
||||||
|
|
||||||
return StockMovementResponse(
|
|
||||||
id=movement.id,
|
|
||||||
product_name=product.name,
|
|
||||||
movement_type=movement.movement_type,
|
|
||||||
quantity=movement.quantity,
|
|
||||||
before_quantity=movement.before_quantity,
|
|
||||||
after_quantity=movement.after_quantity,
|
|
||||||
reference_no=movement.reference_no,
|
|
||||||
remark=movement.remark,
|
|
||||||
created_at=movement.created_at
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stock-movements", response_model=List[StockMovementResponse])
|
|
||||||
async def list_stock_movements(
|
|
||||||
product_id: Optional[int] = None,
|
|
||||||
movement_type: Optional[str] = None,
|
|
||||||
skip: int = Query(0, ge=0),
|
|
||||||
limit: int = Query(20, ge=1, le=100),
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
query = (
|
|
||||||
select(StockMovement, Product)
|
|
||||||
.join(Product, StockMovement.product_id == Product.id)
|
|
||||||
.order_by(StockMovement.created_at.desc())
|
|
||||||
)
|
|
||||||
|
|
||||||
if product_id:
|
|
||||||
query = query.where(StockMovement.product_id == product_id)
|
|
||||||
if movement_type:
|
|
||||||
query = query.where(StockMovement.movement_type == movement_type)
|
|
||||||
|
|
||||||
query = query.offset(skip).limit(limit)
|
|
||||||
result = await db_session.execute(query)
|
|
||||||
|
|
||||||
movements = []
|
|
||||||
for movement, product in result.all():
|
|
||||||
movements.append(StockMovementResponse(
|
|
||||||
id=movement.id,
|
|
||||||
product_name=product.name,
|
|
||||||
movement_type=movement.movement_type,
|
|
||||||
quantity=movement.quantity,
|
|
||||||
before_quantity=movement.before_quantity,
|
|
||||||
after_quantity=movement.after_quantity,
|
|
||||||
reference_no=movement.reference_no,
|
|
||||||
remark=movement.remark,
|
|
||||||
created_at=movement.created_at
|
|
||||||
))
|
|
||||||
|
|
||||||
return movements
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/purchase-orders", response_model=List[PurchaseOrderResponse])
|
|
||||||
async def list_purchase_orders(
|
|
||||||
status: Optional[str] = None,
|
|
||||||
skip: int = Query(0, ge=0),
|
|
||||||
limit: int = Query(20, ge=1, le=100),
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
query = (
|
|
||||||
select(PurchaseOrder, Supplier)
|
|
||||||
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
|
|
||||||
.order_by(PurchaseOrder.created_at.desc())
|
|
||||||
)
|
|
||||||
|
|
||||||
if status:
|
|
||||||
query = query.where(PurchaseOrder.status == status)
|
|
||||||
|
|
||||||
query = query.offset(skip).limit(limit)
|
|
||||||
result = await db_session.execute(query)
|
|
||||||
|
|
||||||
orders = []
|
|
||||||
for order, supplier in result.all():
|
|
||||||
orders.append(PurchaseOrderResponse(
|
|
||||||
id=order.id,
|
|
||||||
order_no=order.order_no,
|
|
||||||
supplier_name=supplier.name,
|
|
||||||
order_date=order.order_date,
|
|
||||||
expected_date=order.expected_date,
|
|
||||||
status=order.status,
|
|
||||||
total_amount=order.total_amount,
|
|
||||||
paid_amount=order.paid_amount,
|
|
||||||
remark=order.remark,
|
|
||||||
created_at=order.created_at
|
|
||||||
))
|
|
||||||
|
|
||||||
return orders
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/purchase-orders", response_model=PurchaseOrderResponse, status_code=201)
|
|
||||||
async def create_purchase_order(
|
|
||||||
order_data: PurchaseOrderCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
order = PurchaseOrder(
|
|
||||||
order_no=generate_order_no("PO"),
|
|
||||||
supplier_id=order_data.supplier_id,
|
|
||||||
expected_date=order_data.expected_date,
|
|
||||||
remark=order_data.remark,
|
|
||||||
operator_id=current_user.id,
|
|
||||||
status="draft"
|
|
||||||
)
|
|
||||||
db_session.add(order)
|
|
||||||
await db_session.flush()
|
|
||||||
|
|
||||||
total_amount = 0
|
|
||||||
for item_data in order_data.items:
|
|
||||||
item = PurchaseOrderItem(
|
|
||||||
order_id=order.id,
|
|
||||||
product_id=item_data.product_id,
|
|
||||||
quantity=item_data.quantity,
|
|
||||||
unit_price=item_data.unit_price,
|
|
||||||
amount=item_data.quantity * item_data.unit_price,
|
|
||||||
remark=item_data.remark
|
|
||||||
)
|
|
||||||
db_session.add(item)
|
|
||||||
total_amount += item.amount
|
|
||||||
|
|
||||||
order.total_amount = total_amount
|
|
||||||
await db_session.commit()
|
|
||||||
await db_session.refresh(order)
|
|
||||||
|
|
||||||
supplier = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
|
|
||||||
supplier = supplier.scalar_one()
|
|
||||||
|
|
||||||
return PurchaseOrderResponse(
|
|
||||||
id=order.id,
|
|
||||||
order_no=order.order_no,
|
|
||||||
supplier_name=supplier.name,
|
|
||||||
order_date=order.order_date,
|
|
||||||
expected_date=order.expected_date,
|
|
||||||
status=order.status,
|
|
||||||
total_amount=order.total_amount,
|
|
||||||
paid_amount=order.paid_amount,
|
|
||||||
remark=order.remark,
|
|
||||||
created_at=order.created_at
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/sales-orders", response_model=List[SalesOrderResponse])
|
|
||||||
async def list_sales_orders(
|
|
||||||
status: Optional[str] = None,
|
|
||||||
skip: int = Query(0, ge=0),
|
|
||||||
limit: int = Query(20, ge=1, le=100),
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
query = (
|
|
||||||
select(SalesOrder, Customer)
|
|
||||||
.join(Customer, SalesOrder.customer_id == Customer.id)
|
|
||||||
.order_by(SalesOrder.created_at.desc())
|
|
||||||
)
|
|
||||||
|
|
||||||
if status:
|
|
||||||
query = query.where(SalesOrder.status == status)
|
|
||||||
|
|
||||||
query = query.offset(skip).limit(limit)
|
|
||||||
result = await db_session.execute(query)
|
|
||||||
|
|
||||||
orders = []
|
|
||||||
for order, customer in result.all():
|
|
||||||
orders.append(SalesOrderResponse(
|
|
||||||
id=order.id,
|
|
||||||
order_no=order.order_no,
|
|
||||||
customer_name=customer.name,
|
|
||||||
order_date=order.order_date,
|
|
||||||
delivery_date=order.delivery_date,
|
|
||||||
status=order.status,
|
|
||||||
total_amount=order.total_amount,
|
|
||||||
received_amount=order.received_amount,
|
|
||||||
remark=order.remark,
|
|
||||||
created_at=order.created_at
|
|
||||||
))
|
|
||||||
|
|
||||||
return orders
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sales-orders", response_model=SalesOrderResponse, status_code=201)
|
|
||||||
async def create_sales_order(
|
|
||||||
order_data: SalesOrderCreate,
|
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
|
||||||
current_user: User = Depends(get_current_active_user)
|
|
||||||
):
|
|
||||||
order = SalesOrder(
|
|
||||||
order_no=generate_order_no("SO"),
|
|
||||||
customer_id=order_data.customer_id,
|
|
||||||
delivery_date=order_data.delivery_date,
|
|
||||||
remark=order_data.remark,
|
|
||||||
operator_id=current_user.id,
|
|
||||||
status="draft"
|
|
||||||
)
|
|
||||||
db_session.add(order)
|
|
||||||
await db_session.flush()
|
|
||||||
|
|
||||||
total_amount = 0
|
|
||||||
for item_data in order_data.items:
|
|
||||||
item = SalesOrderItem(
|
|
||||||
order_id=order.id,
|
|
||||||
product_id=item_data.product_id,
|
|
||||||
quantity=item_data.quantity,
|
|
||||||
unit_price=item_data.unit_price,
|
|
||||||
amount=item_data.quantity * item_data.unit_price,
|
|
||||||
remark=item_data.remark
|
|
||||||
)
|
|
||||||
db_session.add(item)
|
|
||||||
total_amount += item.amount
|
|
||||||
|
|
||||||
order.total_amount = total_amount
|
|
||||||
await db_session.commit()
|
|
||||||
await db_session.refresh(order)
|
|
||||||
|
|
||||||
customer = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
|
|
||||||
customer = customer.scalar_one()
|
|
||||||
|
|
||||||
return SalesOrderResponse(
|
|
||||||
id=order.id,
|
|
||||||
order_no=order.order_no,
|
|
||||||
customer_name=customer.name,
|
|
||||||
order_date=order.order_date,
|
|
||||||
delivery_date=order.delivery_date,
|
|
||||||
status=order.status,
|
|
||||||
total_amount=order.total_amount,
|
|
||||||
received_amount=order.received_amount,
|
|
||||||
remark=order.remark,
|
|
||||||
created_at=order.created_at
|
|
||||||
)
|
|
||||||
+28
-20
@@ -673,28 +673,36 @@ async def process_file_core(
|
|||||||
product_weight=product_weight_g
|
product_weight=product_weight_g
|
||||||
)
|
)
|
||||||
|
|
||||||
# 9.7 FreeCAD 几何验证
|
# 9.7 FreeCAD 几何验证(可通过配置禁用)
|
||||||
await storage_service.update_task_status(
|
|
||||||
db_session, task_id, "processing", 90, "FreeCAD几何验证"
|
|
||||||
)
|
|
||||||
|
|
||||||
verification_result = None
|
verification_result = None
|
||||||
try:
|
from config.settings import settings
|
||||||
from services.verification_service import verification_service
|
|
||||||
verification_result = await verification_service.verify_stp_file(file_path)
|
if settings.ENABLE_FREECAD_VERIFICATION:
|
||||||
|
await storage_service.update_task_status(
|
||||||
|
db_session, task_id, "processing", 90, "FreeCAD几何验证"
|
||||||
|
)
|
||||||
|
|
||||||
# 保存验证结果到数据库
|
try:
|
||||||
if verification_result and analysis_result:
|
from services.verification_service import GeometryVerificationService
|
||||||
await _save_verification_metrics(
|
# 使用配置的超时时间
|
||||||
db_session,
|
verification_svc = GeometryVerificationService(timeout=settings.FREECAD_VERIFICATION_TIMEOUT)
|
||||||
stp_file_id,
|
verification_result = await verification_svc.verify_stp_file(file_path)
|
||||||
verification_result
|
|
||||||
)
|
# 保存验证结果到数据库
|
||||||
|
if verification_result and analysis_result:
|
||||||
logger.info(f"FreeCAD验证完成: {verification_result.get('status', 'unknown') if verification_result else 'failed'}")
|
await _save_verification_metrics(
|
||||||
except Exception as ve:
|
db_session,
|
||||||
logger.warning(f"FreeCAD验证失败(不影响主流程): {ve}")
|
stp_file_id,
|
||||||
verification_result = {"status": "error", "error": str(ve)}
|
verification_result
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"FreeCAD验证完成: {verification_result.get('status', 'unknown') if verification_result else 'failed'}")
|
||||||
|
except Exception as ve:
|
||||||
|
logger.warning(f"FreeCAD验证失败(不影响主流程): {ve}")
|
||||||
|
verification_result = {"status": "error", "error": str(ve)}
|
||||||
|
else:
|
||||||
|
logger.info("FreeCAD验证已禁用(设置 ENABLE_FREECAD_VERIFICATION=true 启用)")
|
||||||
|
verification_result = {"status": "disabled", "reason": "FreeCAD验证已禁用"}
|
||||||
|
|
||||||
# 10. 完成处理
|
# 10. 完成处理
|
||||||
await storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
await storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ import asyncio
|
|||||||
|
|
||||||
from api.routes import router
|
from api.routes import router
|
||||||
from api.auth_routes import router as auth_router
|
from api.auth_routes import router as auth_router
|
||||||
from api.inventory_routes import router as inventory_router
|
from api.inventory import inventory_router
|
||||||
from utils.logger import setup_logging
|
from utils.logger import setup_logging
|
||||||
from database.init_db import init_database
|
from database.init_db import init_database
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user