v0.2.0: CRM/ERP 系统升级 - 清理 .gitignore 并移除误提交的 venv/env/db 文件
- 更新 .gitignore:全面覆盖环境变量、数据库、日志、缓存、上传文件 - 移除误跟踪的 server/venv/、crm_data.db、.env 文件 - 新增 server/.env.example 模板 - 新增合同管理、利润核算、AI教练等功能模块 - 新增 Playwright e2e 测试套件 - 前后端多项功能升级和 bug 修复
This commit is contained in:
+338
-4
@@ -6,7 +6,7 @@ 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
|
||||
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
|
||||
@@ -32,8 +32,9 @@ 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)
|
||||
result = await svc.create_order(db, current_user, body, company_id)
|
||||
return ok(data=result.model_dump(mode="json"), message=f"订单 {result.order_no} 创建成功")
|
||||
|
||||
|
||||
@@ -47,16 +48,349 @@ async def list_orders(
|
||||
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)
|
||||
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)
|
||||
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="发票创建并关联成功")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user