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:
hankin
2026-05-11 07:24:19 +00:00
parent 0f4c6b7924
commit 815cbf9d8c
2526 changed files with 11875 additions and 804148 deletions
+49 -15
View File
@@ -10,9 +10,11 @@ from typing import Any
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import BizException, ForbiddenException, NotFoundException
from app.models.erp import InventoryFlow, ProductSku
from app.models.erp import ErpSkuInventory, InventoryFlow, ProductSku
from app.models.order import ErpOrder, ErpOrderItem
from app.models.shipping import ErpShippingItem, ErpShippingRecord
from app.models.sys import SysUser
from app.models.crm import CrmCustomer
from app.schemas.auth import CurrentUserPayload
from app.schemas.shipping import (
ShippingBriefResponse, ShippingCreate, ShippingItemResponse,
@@ -75,10 +77,15 @@ def _check_shipping_access(order: ErpOrder, user: CurrentUserPayload) -> None:
async def create_shipping(
db: AsyncSession, user: CurrentUserPayload, body: ShippingCreate,
company_id: uuid.UUID,
) -> tuple[ShippingResponse, str]:
"""返回 (response, new_shipping_state)"""
"""返回 (response, new_shipping_state)。库存从 erp_sku_inventory 扣减"""
order = (await db.execute(
select(ErpOrder).where(ErpOrder.id == body.order_id, ErpOrder.is_deleted.is_(False))
select(ErpOrder).where(
ErpOrder.id == body.order_id,
ErpOrder.is_deleted.is_(False),
ErpOrder.company_id == company_id,
)
)).scalar_one_or_none()
if order is None:
raise NotFoundException("订单不存在")
@@ -114,6 +121,7 @@ async def create_shipping(
carrier=body.carrier, tracking_no=body.tracking_no,
status="transit", ship_date=body.ship_date or date.today(),
remark=body.remark, operator_id=user.user_id,
company_id=company_id,
)
db.add(record)
await db.flush()
@@ -125,22 +133,41 @@ async def create_shipping(
)
db.add(si)
result = await db.execute(
update(ProductSku).where(
ProductSku.id == item.sku_id,
ProductSku.stock_qty >= item.shipped_qty,
).values(
stock_qty=ProductSku.stock_qty - Decimal(str(item.shipped_qty)),
# ── 从 erp_sku_inventory 扣减库存(行锁) ──
inv = (
await db.execute(
select(ErpSkuInventory)
.where(
ErpSkuInventory.sku_id == item.sku_id,
ErpSkuInventory.company_id == company_id,
)
.with_for_update()
)
).scalar_one_or_none()
current_stock = float(inv.stock_qty) if inv else 0
if current_stock < item.shipped_qty:
raise BizException(
message=f"库存不足无法发货: SKU {item.sku_id}"
f"当前库存 {current_stock},请求出库 {item.shipped_qty}"
)
if inv is None:
# 不应出现此情况,但防御性处理
raise BizException(message=f"SKU {item.sku_id} 在当前公司无库存记录")
await db.execute(
update(ErpSkuInventory)
.where(ErpSkuInventory.id == inv.id)
.values(
stock_qty=ErpSkuInventory.stock_qty - Decimal(str(item.shipped_qty)),
updated_at=now,
)
)
if result.rowcount == 0:
sku = (await db.execute(select(ProductSku).where(ProductSku.id == item.sku_id))).scalar_one_or_none()
current_stock = float(sku.stock_qty) if sku else 0
raise BizException(message=f"库存不足无法发货: SKU {item.sku_id},当前库存 {current_stock},请求出库 {item.shipped_qty}")
db.add(InventoryFlow(
sku_id=item.sku_id, change_qty=-item.shipped_qty,
sku_id=item.sku_id, company_id=company_id,
change_qty=-item.shipped_qty,
reason="shipment", remark=f"订单发货出库 - 发货单 {shipping_no}",
operator_id=user.user_id,
))
@@ -178,8 +205,11 @@ async def list_shipping(
db: AsyncSession, user: CurrentUserPayload,
page: int = 1, size: int = 20,
order_no: str | None = None, tracking_no: str | None = None,
company_id: uuid.UUID | None = None,
) -> ShippingListResponse:
where: list[Any] = [ErpShippingRecord.is_deleted.is_(False)]
if company_id:
where.append(ErpShippingRecord.company_id == company_id)
if user.data_scope == "self":
my_orders = select(ErpOrder.id).where(ErpOrder.salesperson_id == user.user_id, ErpOrder.is_deleted.is_(False))
where.append(ErpShippingRecord.order_id.in_(my_orders))
@@ -203,9 +233,13 @@ async def list_shipping(
async def get_shipping_by_order(
db: AsyncSession, user: CurrentUserPayload, order_id: uuid.UUID,
company_id: uuid.UUID | None = None,
) -> dict[str, Any]:
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(ErpOrder.id == order_id, ErpOrder.is_deleted.is_(False))
select(ErpOrder).where(*where_clause)
)).scalar_one_or_none()
if order is None:
raise NotFoundException("订单不存在")