815cbf9d8c
- 更新 .gitignore:全面覆盖环境变量、数据库、日志、缓存、上传文件 - 移除误跟踪的 server/venv/、crm_data.db、.env 文件 - 新增 server/.env.example 模板 - 新增合同管理、利润核算、AI教练等功能模块 - 新增 Playwright e2e 测试套件 - 前后端多项功能升级和 bug 修复
311 lines
9.5 KiB
Python
311 lines
9.5 KiB
Python
"""
|
|
订单管理 Service 层
|
|
REST API 路由 和 MCP 工具 共用此层函数
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import date, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.exceptions import BizException, ForbiddenException, NotFoundException
|
|
from app.models.crm import CrmCustomer
|
|
from app.models.erp import ProductSku
|
|
from app.models.order import ErpOrder, ErpOrderItem
|
|
from app.models.sys import SysUser
|
|
from app.schemas.auth import CurrentUserPayload
|
|
from app.schemas.order import (
|
|
OrderBriefResponse,
|
|
OrderCreate,
|
|
OrderItemResponse,
|
|
OrderListResponse,
|
|
OrderResponse,
|
|
PriceCalculateResponse,
|
|
)
|
|
|
|
|
|
# ── 工具函数 ─────────────────────────────────────────────
|
|
|
|
async def _generate_order_no(db: AsyncSession) -> str:
|
|
today = date.today().strftime("%Y%m%d")
|
|
prefix = f"ORD-{today}-"
|
|
stmt = (
|
|
select(func.count())
|
|
.select_from(ErpOrder)
|
|
.where(ErpOrder.order_no.like(f"{prefix}%"))
|
|
)
|
|
count = (await db.execute(stmt)).scalar() or 0
|
|
return f"{prefix}{count + 1:03d}"
|
|
|
|
|
|
def _item_to_response(item: ErpOrderItem) -> OrderItemResponse:
|
|
return OrderItemResponse(
|
|
id=item.id,
|
|
sku_id=item.sku_id,
|
|
sku_code=item.sku.sku_code if item.sku else None,
|
|
sku_name=item.sku.name if item.sku else None,
|
|
spec=item.sku.spec if item.sku else None,
|
|
qty=float(item.qty),
|
|
unit_price=float(item.unit_price),
|
|
sub_total=float(item.sub_total),
|
|
shipped_qty=float(item.shipped_qty or 0),
|
|
)
|
|
|
|
|
|
def _order_to_response(o: ErpOrder, with_items: bool = True) -> OrderResponse:
|
|
return OrderResponse(
|
|
id=o.id,
|
|
order_no=o.order_no,
|
|
customer_id=o.customer_id,
|
|
customer_name=o.customer.name if o.customer else None,
|
|
salesperson_id=o.salesperson_id,
|
|
salesperson_name=o.salesperson.real_name if o.salesperson else None,
|
|
total_amount=float(o.total_amount or 0),
|
|
shipping_state=o.shipping_state,
|
|
payment_state=o.payment_state,
|
|
paid_amount=float(o.paid_amount or 0),
|
|
remark=o.remark,
|
|
order_date=o.order_date,
|
|
items=[_item_to_response(i) for i in o.items] if with_items else [],
|
|
created_at=o.created_at,
|
|
updated_at=o.updated_at,
|
|
)
|
|
|
|
|
|
def _order_to_brief(o: ErpOrder) -> OrderBriefResponse:
|
|
return OrderBriefResponse(
|
|
id=o.id,
|
|
order_no=o.order_no,
|
|
customer_id=o.customer_id,
|
|
customer_name=o.customer.name if o.customer else None,
|
|
salesperson_name=o.salesperson.real_name if o.salesperson else None,
|
|
total_amount=float(o.total_amount or 0),
|
|
shipping_state=o.shipping_state,
|
|
payment_state=o.payment_state,
|
|
paid_amount=float(o.paid_amount or 0),
|
|
order_date=o.order_date,
|
|
created_at=o.created_at,
|
|
)
|
|
|
|
|
|
def _check_order_access(order: ErpOrder, user: CurrentUserPayload) -> None:
|
|
if user.data_scope == "all":
|
|
return
|
|
if user.data_scope == "self":
|
|
if order.salesperson_id != user.user_id:
|
|
raise ForbiddenException("无权访问该订单(数据权限:仅本人)")
|
|
|
|
|
|
# ── Service Functions ────────────────────────────────────
|
|
|
|
async def calculate_price(
|
|
db: AsyncSession,
|
|
customer_id: uuid.UUID,
|
|
sku_id: uuid.UUID,
|
|
) -> PriceCalculateResponse:
|
|
sku = (
|
|
await db.execute(
|
|
select(ProductSku).where(
|
|
ProductSku.id == sku_id,
|
|
ProductSku.is_deleted.is_(False),
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
if sku is None:
|
|
raise NotFoundException("产品 SKU 不存在")
|
|
|
|
# 历史成交价追溯
|
|
history_stmt = (
|
|
select(ErpOrderItem.unit_price, ErpOrder.order_no, ErpOrder.order_date)
|
|
.join(ErpOrder, ErpOrderItem.order_id == ErpOrder.id)
|
|
.where(
|
|
ErpOrder.customer_id == customer_id,
|
|
ErpOrderItem.sku_id == sku_id,
|
|
ErpOrder.is_deleted.is_(False),
|
|
ErpOrderItem.is_deleted.is_(False),
|
|
)
|
|
.order_by(ErpOrder.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
history = (await db.execute(history_stmt)).first()
|
|
|
|
if history:
|
|
return PriceCalculateResponse(
|
|
sku_id=sku.id,
|
|
sku_code=sku.sku_code,
|
|
sku_name=sku.name,
|
|
unit_price=float(history.unit_price),
|
|
price_source="history",
|
|
last_order_no=history.order_no,
|
|
last_order_date=history.order_date,
|
|
)
|
|
|
|
return PriceCalculateResponse(
|
|
sku_id=sku.id,
|
|
sku_code=sku.sku_code,
|
|
sku_name=sku.name,
|
|
unit_price=float(sku.standard_price or 0),
|
|
price_source="standard",
|
|
)
|
|
|
|
|
|
async def create_order(
|
|
db: AsyncSession,
|
|
user: CurrentUserPayload,
|
|
body: OrderCreate,
|
|
company_id: uuid.UUID,
|
|
) -> OrderResponse:
|
|
# 校验客户存在
|
|
cust = (
|
|
await db.execute(
|
|
select(CrmCustomer).where(
|
|
CrmCustomer.id == body.customer_id,
|
|
CrmCustomer.is_deleted.is_(False),
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
if cust is None:
|
|
raise NotFoundException("客户不存在")
|
|
|
|
# 校验所有 SKU 存在
|
|
sku_ids = [item.sku_id for item in body.items]
|
|
skus = (
|
|
await db.execute(
|
|
select(ProductSku).where(
|
|
ProductSku.id.in_(sku_ids),
|
|
ProductSku.is_deleted.is_(False),
|
|
)
|
|
)
|
|
).scalars().all()
|
|
found_ids = {s.id for s in skus}
|
|
missing = [str(sid) for sid in sku_ids if sid not in found_ids]
|
|
if missing:
|
|
raise BizException(message=f"以下 SKU 不存在: {', '.join(missing)}")
|
|
|
|
try:
|
|
async with db.begin_nested():
|
|
order_no = await _generate_order_no(db)
|
|
total = sum(item.qty * item.unit_price for item in body.items)
|
|
|
|
order = ErpOrder(
|
|
order_no=order_no,
|
|
customer_id=body.customer_id,
|
|
salesperson_id=user.user_id,
|
|
company_id=company_id,
|
|
total_amount=total,
|
|
shipping_state="pending",
|
|
payment_state="unpaid",
|
|
paid_amount=0,
|
|
remark=body.remark,
|
|
order_date=body.order_date or date.today(),
|
|
)
|
|
db.add(order)
|
|
await db.flush()
|
|
|
|
for item in body.items:
|
|
order_item = ErpOrderItem(
|
|
order_id=order.id,
|
|
sku_id=item.sku_id,
|
|
qty=item.qty,
|
|
unit_price=item.unit_price,
|
|
sub_total=round(item.qty * item.unit_price, 2),
|
|
shipped_qty=0,
|
|
)
|
|
db.add(order_item)
|
|
|
|
await db.commit()
|
|
except BizException:
|
|
raise
|
|
except Exception as e:
|
|
await db.rollback()
|
|
raise BizException(code=500, message=f"订单创建事务失败: {e!s}") from e
|
|
|
|
refreshed = (
|
|
await db.execute(select(ErpOrder).where(ErpOrder.id == order.id))
|
|
).scalar_one()
|
|
return _order_to_response(refreshed)
|
|
|
|
|
|
async def list_orders(
|
|
db: AsyncSession,
|
|
user: CurrentUserPayload,
|
|
page: int = 1,
|
|
size: int = 20,
|
|
customer_id: uuid.UUID | None = None,
|
|
shipping_state: str | None = None,
|
|
payment_state: str | None = None,
|
|
keyword: str | None = None,
|
|
company_id: uuid.UUID | None = None,
|
|
) -> OrderListResponse:
|
|
where: list[Any] = [ErpOrder.is_deleted.is_(False)]
|
|
if company_id:
|
|
where.append(ErpOrder.company_id == company_id)
|
|
|
|
if user.data_scope == "self":
|
|
where.append(ErpOrder.salesperson_id == user.user_id)
|
|
elif user.data_scope == "dept_and_sub":
|
|
if user.dept_id is not None:
|
|
from app.models.sys import SysUser
|
|
sub = select(SysUser.id).where(
|
|
SysUser.dept_id == user.dept_id,
|
|
SysUser.is_deleted.is_(False),
|
|
)
|
|
where.append(ErpOrder.salesperson_id.in_(sub))
|
|
|
|
if customer_id:
|
|
where.append(ErpOrder.customer_id == customer_id)
|
|
if shipping_state:
|
|
where.append(ErpOrder.shipping_state == shipping_state)
|
|
if payment_state:
|
|
where.append(ErpOrder.payment_state == payment_state)
|
|
if keyword:
|
|
where.append(ErpOrder.order_no.ilike(f"%{keyword}%"))
|
|
|
|
total = (
|
|
await db.execute(select(func.count()).select_from(ErpOrder).where(*where))
|
|
).scalar() or 0
|
|
|
|
stmt = (
|
|
select(ErpOrder)
|
|
.where(*where)
|
|
.order_by(ErpOrder.created_at.desc())
|
|
.offset((page - 1) * size)
|
|
.limit(size)
|
|
)
|
|
orders = (await db.execute(stmt)).scalars().all()
|
|
|
|
return OrderListResponse(
|
|
total=total,
|
|
items=[_order_to_brief(o) for o in orders],
|
|
page=page,
|
|
size=size,
|
|
)
|
|
|
|
|
|
async def get_order(
|
|
db: AsyncSession,
|
|
user: CurrentUserPayload,
|
|
order_id: uuid.UUID,
|
|
company_id: uuid.UUID | None = None,
|
|
) -> OrderResponse:
|
|
where_clause = [
|
|
ErpOrder.id == order_id,
|
|
ErpOrder.is_deleted.is_(False),
|
|
]
|
|
if company_id:
|
|
where_clause.append(ErpOrder.company_id == company_id)
|
|
order = (
|
|
await db.execute(
|
|
select(ErpOrder).where(*where_clause)
|
|
)
|
|
).scalar_one_or_none()
|
|
if order is None:
|
|
raise NotFoundException("订单不存在或已被删除")
|
|
|
|
_check_order_access(order, user)
|
|
return _order_to_response(order)
|