feat(business): 业务管理接口(编号生成/明细/状态机/归属留痕/毛利润视图)

- 编号 YW{日期}-{序号}:MAX+FOR UPDATE 并发安全 + 1062 兜底重试(no_generator 三单据共用)
- 明细全量替换重算五项汇总冗余;销售额服务层按数量×单价固化不信任前端
- 状态机终态拦截、终止必填原因、创建写首条留痕;归属变更同事务留痕防旁路
- 毛利润视图按行实时计算(不含运费 #2),阈值按 asOf 取 M1 快照,率输出
  Fraction 分数字符串同 M1 预览口径;成本缺失行标注无法计算
- 入参契约统一为蛇形直出(撤回 customer/business 的驼峰 alias,与前端
  businessRoles.ts 模式对齐)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-13 12:01:48 +08:00
1 parent 1625907380
commit 0cdc6246c4
7 files changed
+1565 -18

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
"""单据编号生成器(业务 YW / 发货 SH / 回款 HK 共用)。
规则:``{前缀}{YYYYMMDD}-{当日4位序号}``,序号按自然日重置。
并发安全:``SELECT MAX(no) ... FOR UPDATE`` 锁住当日序号区间 —— 后到的
事务会等待先到者提交后再读到新 MAX,序号单调不重;残余竞态(锁等待超时
等极端场景)由服务层捕唯一键冲突(1062)重试一次兜底。
"""
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.orm import Session
def today_prefix(prefix: str) -> str:
"""当日编号前缀(如 YW20261013-),LIKE 匹配与拼号共用同一来源防漂移。"""
return f"{prefix}{datetime.now().strftime('%Y%m%d')}-"
def next_no(
session: Session,
*,
model: type,
column_name: str,
prefix: str,
seq_width: int = 4,
) -> str:
"""生成下一个编号。
:param model: 编号所在的 ORM 模型(businesses/shipments/receipts)
:param column_name: 编号列名(business_no/shipment_no/receipt_batch_no)
:param prefix: 单据前缀(NO_PREFIX_* 常量)
:param seq_width: 序号位数(默认 4);超出宽度的序号按实际位数输出,
VARCHAR(32) 余量充足、唯一性不受影响
"""
like_prefix = today_prefix(prefix)
column = getattr(model, column_name)
max_no = session.execute(
select(column)
.where(column.like(f"{like_prefix}%"))
.order_by(column.desc())
.limit(1)
.with_for_update()
).scalar()
seq = int(max_no.rsplit("-", 1)[1]) + 1 if max_no else 1
return f"{like_prefix}{seq:0{seq_width}d}"