feat(purchase): 新增待采购池接口(防重复采购)

GET /api/v1/purchase/pending-pool:找出「有效供给仍低于预警线」的物料。

  有效供给 = 物理总库存 + 在途量
  过滤规则 = 有效供给 <= 红/黄阈值 才进池
  建议采购量 = max(1, 目标阈值 - 有效供给)

★ 为什么不用「有活跃单就排除」:红线 10、库存 0、某采购员只建了一张
  数量 5 的单时,该物料会立刻从池中消失,剩下 5 个缺口永远无人认领。
  改为按量计算后它继续留池,suggested_qty 自动降到 5。

★ 用 <= 而非 < 是与业务方确认后刻意维持的口径(与物料列表预警、预警邮件
  同源),勿擅自改成严格小于 —— 只改本接口会造成「列表亮黄灯、池子却排除」
  的撕裂,真要改必须三处一起动。代价是恰好等于阈值时建议量落到保底的 1,
  前端 tooltip 已单独说明,不展示算不成立的错误算式。

权限:新增 inbound_purchase:pending_pool(注册为 sys_element 而非新建
SysMenu)。因 ensure_default_permissions 在角色已有权限时整段跳过,另写了
存量角色自动迁移,并按源行镜像 company_name 避免跨公司作用域泄漏。
This commit is contained in:
yueli
2026-09-18 14:30:53 +08:00
parent fc5e62c848
commit dec33b685c
3 changed files with 327 additions and 1 deletions

View File

@ -673,6 +673,12 @@ class PermissionService:
('inbound_purchase:unit_price', '采购单价', 'column'),
('inbound_purchase:total_price', '采购总价', 'column'),
('inbound_purchase:tax_rate', '税率', 'column'),
# ★ 待采购清单(防重复采购)菜单权限。
# 注册成 element 而非新建 SysMenu侧边栏与路由守卫只读前端
# meta.permissions + sys_role_permission从不读 sys_menu
# 且 init_all_menus 的「冗余子菜单清理」会按 target_code 删权限行,
# 新增 SysMenu 等于多一个被该清理逻辑波及的面。
('inbound_purchase:pending_pool', '待采购清单', 'operation'),
]
for code, name, etype in purchase_elements:
existing = SysElement.query.filter_by(
@ -833,6 +839,48 @@ class PermissionService:
print(f"[权限迁移] {role_code}: inbound_buy → inbound_purchase 已自动迁移")
total_inserted += 1
# ★ 自动迁移:已有「采购申请」菜单权限的角色 → 自动获得「待采购清单」权限
#
# 为什么必须独立成段:上面的默认分配逻辑在角色**已有任何权限**时
# 就整段跳过(见上方 existing_count > 0 的分支),存量角色根本走不到
# 那里。不写这一段,除了超管之外没有任何角色能拿到新权限。
#
# 本段依赖 autoflush上一个循环刚 add 的 inbound_purchase 行尚未
# commit这里的查询必须能看见它们扩展默认 autoflush=True 保证了这点)。
for role_code in known_roles:
if role_code.upper() == 'SUPER_ADMIN':
continue
# 以「采购申请菜单权限」为门控 —— 能进采购页的角色就该看到待采购清单。
# ★ 同时镜像它的 company_name若某角色的采购菜单是公司定制的
# company_name='A'),补出的权限必须落在同一作用域;硬写 NULL
# 会让其他公司的同角色用户凭空多出这个菜单。
src_perm = SysRolePermission.query.filter_by(
role_code=role_code, target_code='inbound_purchase', type='menu'
).first()
if not src_perm:
continue
# 幂等检查也必须带 company_name否则 A 公司已有行会跳过 B 公司那条
already = SysRolePermission.query.filter_by(
role_code=role_code,
target_code='inbound_purchase:pending_pool',
type='element',
company_name=src_perm.company_name,
).first()
if already:
continue
db.session.add(SysRolePermission(
role_code=role_code,
target_code='inbound_purchase:pending_pool',
type='element',
company_name=src_perm.company_name,
))
total_inserted += 1
print(f"[权限迁移] {role_code}: 已补充「待采购清单」权限"
f"(作用域={src_perm.company_name or '全局'}")
# ★ SUPERVISOR 主管默认获得采购申请子菜单权限
for role_code in known_roles:
if role_code.upper() == 'SUPERVISOR':