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:
@@ -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("订单不存在")
|
||||
|
||||
Reference in New Issue
Block a user