- 编号 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>
47 lines
1.6 KiB
Python
47 lines
1.6 KiB
Python
"""单据编号生成器(业务 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}"
|