fix(business): 编辑/流转/归属加锁与终态编辑门槛,has_docs 单点收口
- get_with_names 加 for_update 参数:update/change_status/change_ownership 以锁定读取行,与发货/回款写路径同一锁口径——「有发货禁换客户」的 快照读校验不再被并发新增发货旁路(此前 delete 有锁、唯独编辑无锁) - update_business 补终态门槛(明细/汇总是 M3 核算基线,与发货/回款 服务禁写终态同口径);前端编辑按钮按终态禁用并加 tooltip - 新增 business_repository.has_docs(单条 OR exists):替换 update 换客户与 delete 两处内联双查询,M3 新增挂靠单据类型时只改一处 - 出参组装复用首查 row:change_ownership 的姓名侧按「变更项取校验 函数返回的新人名、未变项沿用首查名」处理(首查名是变更前快照) - 新增终态编辑 400 且明细未动的集成用例 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
a0086554b1
commit
6d612daf30
4 files changed
+108
-33
No files matched your search
@@ -7,7 +7,7 @@ models/customer.py 定义)。
|
||||
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import Row, Select, delete, func, or_, select
|
||||
from sqlalchemy import Row, Select, delete, exists, func, or_, select
|
||||
from sqlalchemy.orm import Session, aliased
|
||||
|
||||
from app.models import (
|
||||
@@ -17,18 +17,35 @@ from app.models import (
|
||||
Customer,
|
||||
OwnershipChangeLog,
|
||||
Personnel,
|
||||
Receipt,
|
||||
Shipment,
|
||||
)
|
||||
from app.models.customer import OWNER_TYPE_BUSINESS
|
||||
|
||||
|
||||
def get_by_id(session: Session, business_id: int, *, for_update: bool = False) -> Business | None:
|
||||
"""按主键取业务行;for_update 供删除保护与状态机使用。"""
|
||||
"""按主键取业务行;for_update 供删除保护、编辑门槛与状态机使用。"""
|
||||
stmt = select(Business).where(Business.id == business_id)
|
||||
if for_update:
|
||||
stmt = stmt.with_for_update()
|
||||
return session.execute(stmt).scalars().first()
|
||||
|
||||
|
||||
def has_docs(session: Session, business_id: int) -> bool:
|
||||
"""业务名下是否已有发货单或回款(换客户/删除保护的引用检查)。
|
||||
|
||||
单条 OR exists 一次往返;新增挂靠业务的单据类型时在此补充条件,
|
||||
编辑与删除两处共用同一判定,防「delete 补了检查、update 漏补」的分叉。
|
||||
"""
|
||||
stmt = select(
|
||||
or_(
|
||||
exists().where(Shipment.business_id == business_id),
|
||||
exists().where(Receipt.business_id == business_id),
|
||||
)
|
||||
)
|
||||
return bool(session.execute(stmt).scalar())
|
||||
|
||||
|
||||
def create(session: Session, business: Business) -> None:
|
||||
"""新增业务行(不 commit,flush 让自增 id 就位)。"""
|
||||
session.add(business)
|
||||
@@ -36,19 +53,26 @@ def create(session: Session, business: Business) -> None:
|
||||
|
||||
|
||||
def get_with_names(
|
||||
session: Session, business_id: int
|
||||
session: Session, business_id: int, *, for_update: bool = False
|
||||
) -> tuple[Business, str, str | None, str | None] | None:
|
||||
"""业务行 + 客户名 + 负责人名 + 协助人名(DTO 与审计摘要共用)。"""
|
||||
"""业务行 + 客户名 + 负责人名 + 协助人名(DTO 与审计摘要共用)。
|
||||
|
||||
for_update 供编辑/状态流转/归属变更使用:与发货/回款写路径的
|
||||
get_by_id(for_update=True) 同一锁口径,编辑事务与单据写事务互斥,
|
||||
「有发货禁换客户」的快照读校验不再被并发新增旁路。
|
||||
"""
|
||||
owner = aliased(Personnel)
|
||||
assistant = aliased(Personnel)
|
||||
row = session.execute(
|
||||
stmt = (
|
||||
select(Business, Customer.company_name, owner.personnel_name, assistant.personnel_name)
|
||||
.join(Customer, Customer.id == Business.customer_id)
|
||||
.outerjoin(owner, owner.id == Business.owner_personnel_id)
|
||||
.outerjoin(assistant, assistant.id == Business.assistant_personnel_id)
|
||||
.where(Business.id == business_id)
|
||||
).first()
|
||||
return row
|
||||
)
|
||||
if for_update:
|
||||
stmt = stmt.with_for_update()
|
||||
return session.execute(stmt).first()
|
||||
|
||||
|
||||
def _apply_filters(
|
||||
|
||||
Reference in new issue
Block a user