""" ERP 订单管理路由 —— /api/orders 薄路由层:参数解析 + 调用 Service + 包装响应 """ from __future__ import annotations import uuid from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, get_current_company_id from app.db.database import get_db from app.schemas.auth import CurrentUserPayload from app.schemas.order import OrderCreate from app.schemas.response import ok from app.services import order_service as svc router = APIRouter(prefix="/orders", tags=["订单管理"]) @router.get("/price/calculate", summary="B2B 动态定价:历史成交价追溯 → 标准价兜底") async def calculate_price( customer_id: uuid.UUID = Query(..., description="客户 ID"), sku_id: uuid.UUID = Query(..., description="产品 SKU ID"), db: AsyncSession = Depends(get_db), _: CurrentUserPayload = Depends(get_current_user), ) -> dict: result = await svc.calculate_price(db, customer_id, sku_id) return ok(data=result.model_dump(mode="json")) @router.post("", summary="创建订单(主子表事务)") async def create_order( body: OrderCreate, db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: result = await svc.create_order(db, current_user, body, company_id) return ok(data=result.model_dump(mode="json"), message=f"订单 {result.order_no} 创建成功") @router.get("", summary="订单大盘列表(含数据权限隔离)") async def list_orders( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), customer_id: uuid.UUID | None = Query(None), shipping_state: str | None = Query(None, pattern=r"^(pending|partial|shipped)$"), payment_state: str | None = Query(None, pattern=r"^(unpaid|partial|cleared)$"), keyword: str | None = Query(None, description="模糊搜索订单号"), db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: result = await svc.list_orders(db, current_user, page, size, customer_id, shipping_state, payment_state, keyword, company_id) return ok(data=result.model_dump(mode="json")) @router.get("/unlinked-invoices", summary="查询未关联订单的发票列表") async def list_unlinked_invoices( keyword: str | None = Query(None, description="发票号模糊搜索"), db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: from sqlalchemy import select from app.models.finance import FinSalesInvoice conditions = [ FinSalesInvoice.company_id == company_id, FinSalesInvoice.is_deleted.is_(False), FinSalesInvoice.order_id.is_(None), ] if keyword: conditions.append(FinSalesInvoice.invoice_number.ilike(f"%{keyword}%")) stmt = ( select(FinSalesInvoice) .where(*conditions) .order_by(FinSalesInvoice.created_at.desc()) .limit(50) ) invoices = (await db.execute(stmt)).scalars().all() return ok(data=[ { "id": str(inv.id), "invoice_number": inv.invoice_number, "issuer": inv.issuer, "receiver_name": inv.receiver_customer.name if inv.receiver_customer else None, "amount": float(inv.amount), "billing_date": str(inv.billing_date), } for inv in invoices ]) @router.get("/{order_id}", summary="订单全景详情(关系预加载 items + customer)") async def get_order( order_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: result = await svc.get_order(db, current_user, order_id, company_id) return ok(data=result.model_dump(mode="json")) @router.get("/{order_id}/invoices", summary="获取订单关联的销项发票") async def get_order_invoices( order_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: from sqlalchemy import select from app.models.finance import FinSalesInvoice stmt = ( select(FinSalesInvoice) .where( FinSalesInvoice.order_id == order_id, FinSalesInvoice.company_id == company_id, FinSalesInvoice.is_deleted.is_(False), ) .order_by(FinSalesInvoice.created_at.desc()) ) invoices = (await db.execute(stmt)).scalars().all() return ok(data=[ { "id": str(inv.id), "invoice_number": inv.invoice_number, "issuer": inv.issuer, "receiver_name": inv.receiver_customer.name if inv.receiver_customer else None, "amount": float(inv.amount), "billing_date": str(inv.billing_date), "payment_status": inv.payment_status, "payment_date": str(inv.payment_date) if inv.payment_date else None, "payment_amount": float(inv.payment_amount or 0), "payment_due_date": str(inv.payment_due_date) if inv.payment_due_date else None, } for inv in invoices ]) @router.put("/{order_id}/payment", summary="更新订单收款状态") async def update_order_payment( order_id: uuid.UUID, body: dict, db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: from sqlalchemy import select, update as sa_update from app.models.order import ErpOrder from datetime import datetime order = (await db.execute( select(ErpOrder).where( ErpOrder.id == order_id, ErpOrder.company_id == company_id, ErpOrder.is_deleted.is_(False), ) )).scalar_one_or_none() if order is None: from app.core.exceptions import NotFoundException raise NotFoundException("订单不存在") values = {} if "paid_amount" in body: paid = float(body["paid_amount"]) values["paid_amount"] = paid total = float(order.total_amount) if paid >= total: values["payment_state"] = "cleared" elif paid > 0: values["payment_state"] = "partial" else: values["payment_state"] = "unpaid" if "payment_state" in body: values["payment_state"] = body["payment_state"] if values: values["updated_at"] = datetime.utcnow() await db.execute( sa_update(ErpOrder).where(ErpOrder.id == order_id).values(**values) ) await db.commit() return ok(message="收款状态已更新") @router.get("/{order_id}/invoice-detail-preview", summary="生成开票明细预览") async def invoice_detail_preview( order_id: uuid.UUID, mode: str = Query("full", pattern=r"^(full|batch)$", description="full=整体开票, batch=按发货批次"), shipping_id: uuid.UUID | None = Query(None, description="batch模式下必传发货单ID"), db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: """根据模式生成开票明细: 整体=订单全部商品, 批次=指定发货单商品""" from sqlalchemy import select from sqlalchemy.orm import selectinload from app.models.order import ErpOrder, ErpOrderItem from app.models.shipping import ErpShippingRecord, ErpShippingItem from app.models.crm import CrmCustomer from app.models.sys import SysCompany from app.core.exceptions import NotFoundException, BizException # 查订单 order = (await db.execute( select(ErpOrder) .where(ErpOrder.id == order_id, ErpOrder.company_id == company_id, ErpOrder.is_deleted.is_(False)) .options( selectinload(ErpOrder.items), selectinload(ErpOrder.customer), selectinload(ErpOrder.salesperson), ) )).scalar_one_or_none() if not order: raise NotFoundException("订单不存在") # 买方名称 buyer_name = order.customer.name if order.customer else "" # 卖方名称 company = (await db.execute( select(SysCompany).where(SysCompany.id == company_id) )).scalar_one_or_none() seller_name = company.name if company else "" items_data = [] total_amount = 0.0 if mode == "full": # 整体开票: 聚合全部订单明细 for oi in (order.items or []): sub = float(oi.sub_total or 0) items_data.append({ "sku_code": oi.sku.sku_code if oi.sku else "", "sku_name": oi.sku.name if oi.sku else "", "spec": oi.sku.spec if oi.sku else "", "unit": oi.sku.unit if oi.sku else "", "qty": float(oi.qty), "unit_price": float(oi.unit_price), "sub_total": sub, }) total_amount += sub else: # 按发货批次 if not shipping_id: raise BizException(message="batch模式需指定shipping_id") ship = (await db.execute( select(ErpShippingRecord) .where( ErpShippingRecord.id == shipping_id, ErpShippingRecord.order_id == order_id, ErpShippingRecord.is_deleted.is_(False), ) .options(selectinload(ErpShippingRecord.items).selectinload(ErpShippingItem.sku)) )).scalar_one_or_none() if not ship: raise NotFoundException("发货单不存在") # 查对应的订单明细来获取单价 order_item_map = {str(oi.id): oi for oi in (order.items or [])} for si in (ship.items or []): oi = order_item_map.get(str(si.order_item_id)) unit_price = float(oi.unit_price) if oi else 0 qty = float(si.shipped_qty) sub = round(qty * unit_price, 2) items_data.append({ "sku_code": si.sku.sku_code if si.sku else "", "sku_name": si.sku.name if si.sku else "", "spec": si.sku.spec if si.sku else "", "unit": si.sku.unit if si.sku else "", "qty": qty, "unit_price": unit_price, "sub_total": sub, }) total_amount += sub return ok(data={ "order_no": order.order_no, "buyer_name": buyer_name, "seller_name": seller_name, "customer_id": str(order.customer_id), "items": items_data, "total_amount": round(total_amount, 2), "shipping_id": str(shipping_id) if shipping_id else None, }) @router.post("/{order_id}/invoices/link", summary="关联已有发票到订单") async def link_existing_invoice( order_id: uuid.UUID, body: dict, db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: """将已存在的销项发票关联到该订单""" from sqlalchemy import select, update as sa_update from app.models.finance import FinSalesInvoice from app.core.exceptions import NotFoundException, BizException from datetime import datetime invoice_id = body.get("invoice_id") shipping_record_id = body.get("shipping_record_id") if not invoice_id: raise BizException(message="请提供 invoice_id") inv = (await db.execute( select(FinSalesInvoice).where( FinSalesInvoice.id == uuid.UUID(invoice_id), FinSalesInvoice.company_id == company_id, FinSalesInvoice.is_deleted.is_(False), ) )).scalar_one_or_none() if not inv: raise NotFoundException("发票不存在") values = {"order_id": order_id, "updated_at": datetime.utcnow()} if shipping_record_id: values["shipping_record_id"] = uuid.UUID(shipping_record_id) await db.execute( sa_update(FinSalesInvoice) .where(FinSalesInvoice.id == uuid.UUID(invoice_id)) .values(**values) ) await db.commit() return ok(message="发票已关联到订单") @router.post("/{order_id}/invoices/create", summary="直接创建发票并关联到订单") async def create_and_link_invoice( order_id: uuid.UUID, body: dict, db: AsyncSession = Depends(get_db), current_user: CurrentUserPayload = Depends(get_current_user), company_id: uuid.UUID = Depends(get_current_company_id), ) -> dict: """创建新的销项发票,同时关联到当前订单""" from sqlalchemy import select from app.models.finance import FinSalesInvoice from app.models.order import ErpOrder from app.core.exceptions import NotFoundException, BizException from datetime import date as dt_date order = (await db.execute( select(ErpOrder).where( ErpOrder.id == order_id, ErpOrder.company_id == company_id, ErpOrder.is_deleted.is_(False), ) )).scalar_one_or_none() if not order: raise NotFoundException("订单不存在") invoice_number = body.get("invoice_number", "").strip() amount = float(body.get("amount", 0)) issuer = body.get("issuer", "").strip() receiver_customer_id = body.get("receiver_customer_id") or str(order.customer_id) billing_date_str = body.get("billing_date") shipping_record_id = body.get("shipping_record_id") remark = body.get("remark") if not invoice_number: raise BizException(message="请填写发票号") if amount <= 0: raise BizException(message="开票金额需大于0") if not issuer: raise BizException(message="请填写开票方名称") # 检查唯一性 from sqlalchemy import func as sa_func existing = (await db.execute( select(sa_func.count()).select_from(FinSalesInvoice).where( FinSalesInvoice.invoice_number == invoice_number, FinSalesInvoice.is_deleted.is_(False), ) )).scalar() if existing: raise BizException(message=f"发票号 {invoice_number} 已存在") inv = FinSalesInvoice( issuer=issuer, receiver_customer_id=uuid.UUID(receiver_customer_id), invoice_number=invoice_number, amount=amount, billing_date=dt_date.fromisoformat(billing_date_str) if billing_date_str else dt_date.today(), remark=remark, order_id=order_id, shipping_record_id=uuid.UUID(shipping_record_id) if shipping_record_id else None, created_by=current_user.user_id, company_id=company_id, ) db.add(inv) await db.commit() return ok(data={"id": str(inv.id), "invoice_number": invoice_number}, message="发票创建并关联成功")