From 1bdc75cee28c9743cd43acbc158824bf1c9f4b68 Mon Sep 17 00:00:00 2001 From: SZCJW <792430652@qq.com> Date: Sun, 8 Mar 2026 22:58:25 +0800 Subject: [PATCH] init --- .env.example | 5 + config/settings.py | 4 + src/api/inventory/__init__.py | 40 + src/api/inventory/customer_routes.py | 54 ++ src/api/inventory/dashboard_routes.py | 70 ++ src/api/inventory/inventory_routes.py | 65 ++ src/api/inventory/product_routes.py | 97 +++ src/api/inventory/purchase_order_routes.py | 111 +++ src/api/inventory/sales_order_routes.py | 111 +++ src/api/inventory/schemas/__init__.py | 27 + src/api/inventory/schemas/customer_schemas.py | 24 + .../inventory/schemas/inventory_schemas.py | 16 + src/api/inventory/schemas/product_schemas.py | 33 + .../schemas/purchase_order_schemas.py | 33 + .../inventory/schemas/sales_order_schemas.py | 33 + .../schemas/stock_movement_schemas.py | 27 + src/api/inventory/schemas/supplier_schemas.py | 24 + .../inventory/schemas/warehouse_schemas.py | 23 + src/api/inventory/stock_movement_routes.py | 135 +++ src/api/inventory/supplier_routes.py | 54 ++ src/api/inventory/utils.py | 22 + src/api/inventory/warehouse_routes.py | 49 ++ src/api/inventory_routes.py | 769 ------------------ src/api/routes.py | 48 +- src/main.py | 2 +- 25 files changed, 1086 insertions(+), 790 deletions(-) create mode 100644 src/api/inventory/__init__.py create mode 100644 src/api/inventory/customer_routes.py create mode 100644 src/api/inventory/dashboard_routes.py create mode 100644 src/api/inventory/inventory_routes.py create mode 100644 src/api/inventory/product_routes.py create mode 100644 src/api/inventory/purchase_order_routes.py create mode 100644 src/api/inventory/sales_order_routes.py create mode 100644 src/api/inventory/schemas/__init__.py create mode 100644 src/api/inventory/schemas/customer_schemas.py create mode 100644 src/api/inventory/schemas/inventory_schemas.py create mode 100644 src/api/inventory/schemas/product_schemas.py create mode 100644 src/api/inventory/schemas/purchase_order_schemas.py create mode 100644 src/api/inventory/schemas/sales_order_schemas.py create mode 100644 src/api/inventory/schemas/stock_movement_schemas.py create mode 100644 src/api/inventory/schemas/supplier_schemas.py create mode 100644 src/api/inventory/schemas/warehouse_schemas.py create mode 100644 src/api/inventory/stock_movement_routes.py create mode 100644 src/api/inventory/supplier_routes.py create mode 100644 src/api/inventory/utils.py create mode 100644 src/api/inventory/warehouse_routes.py delete mode 100644 src/api/inventory_routes.py diff --git a/.env.example b/.env.example index d5d5ec0..c0779c1 100644 --- a/.env.example +++ b/.env.example @@ -40,3 +40,8 @@ RUSTFS_PRESIGNED_URL_EXPIRES=3600 SECRET_KEY=your-secret-key-change-in-production-min-32-chars ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +# FreeCAD 验证配置(可选) +# 启用后会增加处理时间,默认禁用 +ENABLE_FREECAD_VERIFICATION=false +FREECAD_VERIFICATION_TIMEOUT=120 diff --git a/config/settings.py b/config/settings.py index fb7dda7..bc140f0 100644 --- a/config/settings.py +++ b/config/settings.py @@ -76,6 +76,10 @@ class Settings: self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123') self.ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@gemold.com') 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 def DATABASE_URL(self) -> str: diff --git a/src/api/inventory/__init__.py b/src/api/inventory/__init__.py new file mode 100644 index 0000000..c4517b0 --- /dev/null +++ b/src/api/inventory/__init__.py @@ -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"] diff --git a/src/api/inventory/customer_routes.py b/src/api/inventory/customer_routes.py new file mode 100644 index 0000000..084562c --- /dev/null +++ b/src/api/inventory/customer_routes.py @@ -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) diff --git a/src/api/inventory/dashboard_routes.py b/src/api/inventory/dashboard_routes.py new file mode 100644 index 0000000..2c5ed61 --- /dev/null +++ b/src/api/inventory/dashboard_routes.py @@ -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 + } diff --git a/src/api/inventory/inventory_routes.py b/src/api/inventory/inventory_routes.py new file mode 100644 index 0000000..881cede --- /dev/null +++ b/src/api/inventory/inventory_routes.py @@ -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 diff --git a/src/api/inventory/product_routes.py b/src/api/inventory/product_routes.py new file mode 100644 index 0000000..8e993cc --- /dev/null +++ b/src/api/inventory/product_routes.py @@ -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": "产品已删除"} diff --git a/src/api/inventory/purchase_order_routes.py b/src/api/inventory/purchase_order_routes.py new file mode 100644 index 0000000..7eb41f8 --- /dev/null +++ b/src/api/inventory/purchase_order_routes.py @@ -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 + ) diff --git a/src/api/inventory/sales_order_routes.py b/src/api/inventory/sales_order_routes.py new file mode 100644 index 0000000..e91bc58 --- /dev/null +++ b/src/api/inventory/sales_order_routes.py @@ -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 + ) diff --git a/src/api/inventory/schemas/__init__.py b/src/api/inventory/schemas/__init__.py new file mode 100644 index 0000000..1ea672e --- /dev/null +++ b/src/api/inventory/schemas/__init__.py @@ -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", +] diff --git a/src/api/inventory/schemas/customer_schemas.py b/src/api/inventory/schemas/customer_schemas.py new file mode 100644 index 0000000..3fa4624 --- /dev/null +++ b/src/api/inventory/schemas/customer_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/inventory_schemas.py b/src/api/inventory/schemas/inventory_schemas.py new file mode 100644 index 0000000..fde63ba --- /dev/null +++ b/src/api/inventory/schemas/inventory_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/product_schemas.py b/src/api/inventory/schemas/product_schemas.py new file mode 100644 index 0000000..496ec21 --- /dev/null +++ b/src/api/inventory/schemas/product_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/purchase_order_schemas.py b/src/api/inventory/schemas/purchase_order_schemas.py new file mode 100644 index 0000000..b19e5b7 --- /dev/null +++ b/src/api/inventory/schemas/purchase_order_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/sales_order_schemas.py b/src/api/inventory/schemas/sales_order_schemas.py new file mode 100644 index 0000000..6527b6c --- /dev/null +++ b/src/api/inventory/schemas/sales_order_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/stock_movement_schemas.py b/src/api/inventory/schemas/stock_movement_schemas.py new file mode 100644 index 0000000..881d127 --- /dev/null +++ b/src/api/inventory/schemas/stock_movement_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/supplier_schemas.py b/src/api/inventory/schemas/supplier_schemas.py new file mode 100644 index 0000000..8b4a917 --- /dev/null +++ b/src/api/inventory/schemas/supplier_schemas.py @@ -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 diff --git a/src/api/inventory/schemas/warehouse_schemas.py b/src/api/inventory/schemas/warehouse_schemas.py new file mode 100644 index 0000000..90db9d7 --- /dev/null +++ b/src/api/inventory/schemas/warehouse_schemas.py @@ -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 diff --git a/src/api/inventory/stock_movement_routes.py b/src/api/inventory/stock_movement_routes.py new file mode 100644 index 0000000..b1b294c --- /dev/null +++ b/src/api/inventory/stock_movement_routes.py @@ -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 diff --git a/src/api/inventory/supplier_routes.py b/src/api/inventory/supplier_routes.py new file mode 100644 index 0000000..60c0f14 --- /dev/null +++ b/src/api/inventory/supplier_routes.py @@ -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) diff --git a/src/api/inventory/utils.py b/src/api/inventory/utils.py new file mode 100644 index 0000000..f1e654b --- /dev/null +++ b/src/api/inventory/utils.py @@ -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}" diff --git a/src/api/inventory/warehouse_routes.py b/src/api/inventory/warehouse_routes.py new file mode 100644 index 0000000..91c6b53 --- /dev/null +++ b/src/api/inventory/warehouse_routes.py @@ -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) diff --git a/src/api/inventory_routes.py b/src/api/inventory_routes.py deleted file mode 100644 index 2b89183..0000000 --- a/src/api/inventory_routes.py +++ /dev/null @@ -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 - ) diff --git a/src/api/routes.py b/src/api/routes.py index ddaf3be..d9fe501 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -673,28 +673,36 @@ async def process_file_core( product_weight=product_weight_g ) - # 9.7 FreeCAD 几何验证 - await storage_service.update_task_status( - db_session, task_id, "processing", 90, "FreeCAD几何验证" - ) - + # 9.7 FreeCAD 几何验证(可通过配置禁用) verification_result = None - try: - from services.verification_service import verification_service - verification_result = await verification_service.verify_stp_file(file_path) + from config.settings import settings + + if settings.ENABLE_FREECAD_VERIFICATION: + await storage_service.update_task_status( + db_session, task_id, "processing", 90, "FreeCAD几何验证" + ) - # 保存验证结果到数据库 - if verification_result and analysis_result: - await _save_verification_metrics( - db_session, - stp_file_id, - 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)} + try: + from services.verification_service import GeometryVerificationService + # 使用配置的超时时间 + verification_svc = GeometryVerificationService(timeout=settings.FREECAD_VERIFICATION_TIMEOUT) + verification_result = await verification_svc.verify_stp_file(file_path) + + # 保存验证结果到数据库 + if verification_result and analysis_result: + await _save_verification_metrics( + db_session, + stp_file_id, + 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. 完成处理 await storage_service.update_stp_file_status(db_session, stp_file_id, "completed") diff --git a/src/main.py b/src/main.py index fa597a7..d5ada29 100644 --- a/src/main.py +++ b/src/main.py @@ -41,7 +41,7 @@ import asyncio from api.routes import 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 database.init_db import init_database