|
|
58fa42bff3
|
feat(scrap): 报废全链路收口到审批流
系统自陈的规则是「报废一律需审批」(SCRAP_ALWAYS_REQUIRES_APPROVAL = True),
但实际有 4 条写 TransScrap 的路径,其中 3 条绕过审批。本提交把三条旁路
全部收口,只保留「申请 → 审批 → 执行」一条写入路径。
【删除】直接报废 POST /api/v1/scrap
同时移除 ScrapService.process_scrap()。该路径的一个连带影响是
「维修件报废」能力随之消失 —— process_scrap 的 trans_repair 分支是
repair_status='报废转出' 的唯一写入点。实测该能力零使用(报废转出 0 条、
trans_scrap 来源 0 条),且早已半死:审批流的扫码校验会拒绝 trans_*
来源。repair_service.py 的误导文案(原文指引操作员「前往报废管理进行
扫码操作」,而那条路根本不通)已改为「维修件报废暂未开放」。
权限元素 scrap_create:operation 保留不删 —— add_scrap_perm.sql 以它为
scrap_apply/scrap_execute 的授权来源。
【改造】借库转报废 POST /borrow/scrap → POST /borrow/scrap-request
TransService.scrap_borrow() 删除,逻辑迁入 BorrowScrapAdapter。
沿用 op_return:operation 权限(零授权变更)。已归还/已报废的记录改为
**直接报错**,不再静默 continue 返回 count=0(原缺陷:用户以为成功)。
【改造】不良品报废 POST /defective/<id>/scrap → POST /defective/<id>/scrap-request
申请时**不预占** remaining_qty(与报废模块「仅锁定意向,不扣库存」的
既有哲学一致)。副作用:同一批坏件可重复提交多张申请单,执行期由适配器
按 Fail-Closed 拒绝超额的那几张。已在该接口注释与前端提示中写明。
【服务层】ScrapApprovalService 接入来源适配层
- submit_approval 改由 get_adapter() 分派,并新增整单 scrap_mode 一致性
校验(混合「需扫码/免扫码」两类来源直接拒绝,让 execute 分流保持简单)
- _build_scanned_index 的来源校验改为 is_scan_source():语义上是**收窄**
而非放宽,扫码通道永远不接纳 trans_* 来源
- execute 按模式分流:scan 项走原有扫码匹配(索引只由 scan 项构建),
auto 项按批准量执行。签名与调用契约不变。
- _match_key 加入来源表:不良品 SKU 是从原库存行复制的,不带来源时
同一张单里的两者会**必然串键**,扫码量算到错误对象上。纯库存单两侧
同源、键仍匹配,对既有流程零行为变更。
【修复】approve() 的 fail-open
原实现 `if user_entries and str(operator_id) not in user_entries` ——
allowed_approvers 为空时条件短路为假,**任何登录用户都能审批**。
这与「报废一律需审批」直接矛盾,留这扇门等于没有审批。改为无名单即拒绝。
已实测存量「无审批人」的在途单为 0 张,不会卡死历史数据。
【扫码通道】scrap/scan 的 trans_repair 分支改为 trans_defective_goods
在管不良品按 SKU 匹配,在管量回填到既有字段形状,前端无需按来源分支。
【清理】scrap_approval_service 的 _stock_models 与 TransScrap 导入已移除
(来源差异全部收敛到适配层);trans_service / inventory_reservation 中
指向已删方法的注释已更新指向 BorrowScrapAdapter。
|
2026-09-16 17:14:17 +08:00 |
|
|
|
077fd2f2cf
|
feat(borrow,scrap): 补齐申请人撤回端点,与出库对齐
背景
----
出库已有独立的申请人撤回端点(仅 @jwt_required + 服务层归属断言),
但借库/报废没有:
· 借库撤回复用 close 端点,权限是 op_borrow_approval(管理路径),
普通员工调用返回 403;
· 报废撤回带 @permission_required('scrap_apply'),同样挡住普通申请人
(scrap_apply 只授予 INBOUND/OUTBOUND/SUPERVISOR/SUPER_ADMIN)。
结果是「我的申请单」页面里,借库/报废的撤回按钮对普通员工点了报错。
借库
----
服务层拆成与管理路径并列的两条入口(与出库同构):
mark_completed —— 管理路径,需 op_borrow_approval,仅 status==1
withdraw_request —— 申请人路径,仅校验单据归属,status 0 或 1
两者共用 _release_and_close(),释放逻辑只有一份实现。
新增 POST /transactions/borrow/request/<id>/withdraw(仅 @jwt_required)。
报废
----
withdraw() 增加 require_owner 参数区分两条路径:
require_owner=True (默认,申请人路径)→ 断言 applicant_id == operator_id
require_owner=False(管理路径) → 由调用方权限装饰器鉴权
WITHDRAWABLE_STATUS 从 (1,) 放宽到 (0, 1),与出库/借库对齐。
移除端点上的 @permission_required('scrap_apply'),改走归属校验。
安全实测
--------
借库 B 撤 A 的单 → 403,库存仍是 27(未被释放)
借库 A 撤自己的单 → 200,30 完全释放
报废 B 撤 A 的单 → 403
报废 A 撤自己的单 → 200
主管代撤员工的单(特权路径)→ 200
|
2026-09-10 15:46:55 +08:00 |
|
|
|
9925bf2b99
|
fix(inventory): 撤回/作废单据时释放预占库存,消除库存泄漏
问题
----
系统原本已有「完结」功能(出库/借库),但它只改状态、不释放预占:
approval.status = 4 # 已完结
db.session.commit() # ← 库存没还回去
申请阶段 reserve_for_items() 扣掉的 available_quantity 就此永久泄漏 ——
货被一张永不执行的作废单锁死,谁也领不走。
核查存量 23 张 status=4 的单,所幸均为预占改造前提交(items_json 无
stock_id),尚未造成实际损失。但缺陷本身是真实的。
改动(三个模块统一)
--------------------
出库 outbound_service.close_request
借库 borrow_service.mark_completed
· 调用 release_reserved(approval.get_items()) 按 items_json 原样归还;
· 明确「仅 status==1(已通过待执行)可撤回」—— 执行成功后
create_outbound_batch / execute_dispatch 会把 status 置为 3,
故该状态判断本身即执行守卫,已执行或已撤回的单都进不来;
· 返回消息带上释放条数,便于操作者确认。
报废 scrap_approval_service.withdraw(新增能力)
· 报废原先只有 approve/reject,没有撤回入口,补齐;
· 复用同一个 release_reserved():报废当前尚未接入预占,调用它会安全
跳过(无 reserved 标记),但将来报废接入预占时该段代码自动生效;
· 新增端点 POST /api/v1/scrap/request/<id>/withdraw(权限 scrap_apply)。
未采用按流水表二次校验:request_no(APR-OUT-…) 与 outbound_no(OUT-…)
格式不同、无关联字段,按单号比对是无效的,状态判断已足够。
实测
----
决定性用例(证明释放真实生效,非账面功夫):
A单预占5 → available=1
B单要5 → 400 拒绝(被A占住)
撤回A → available=6
B单再要5 → 200 成功,available=1 ★ 释放的库存真的可被复用
三模块:
出库 撤回后 4→10 完全恢复,stock 未变(货没动)
借库 撤回后 6→10 完全恢复
报废 撤回成功,重复撤回被正确拒绝
状态码说明:报废复用出库/借库已有的 4=已完结 作为「已撤回」,
而非引入 -1,避免同一系统出现两套编号(其 2 已被「已驳回」占用)。
|
2026-09-10 15:08:32 +08:00 |
|
|
|
e152f16ebc
|
feat(scrap): 报废一律需审批,不再按物料标记区分
业务规则变更:所有报废申请都必须由指定审批人审批通过后才能执行。
原逻辑走 resolve_approval_control 判定是否需审批,而 material_base 表中
仅 1/3012 个物料标记了 is_approval_required,意味着 99.97% 的报废申请会
走免审批分支——status 直接置 1、actual_approver_id 被赋为申请人自己、
审批人参数被静默丢弃。前端即便做了必填也只是摆设。
改动(规则收敛到单一来源):
· 新增 SCRAP_ALWAYS_REQUIRES_APPROVAL = True,作为唯一开关;
· submit_approval() 无审批人一律拒绝;恒置 status=0(待审批);
删除免审批自动通过分支;
· resolve_approval_control 仍调用,但仅用于生成提示文案,
不再参与是否审批的判定;
· /request/check-approval 返回 need_approval=SCRAP_ALWAYS_REQUIRES_APPROVAL,
否则该接口会继续返回 false,导致前端预检结果失真。
实测:不传审批人 → 拒绝;传审批人 → status=0 且 actual_approver_id 为空。
⚠ 注意:此前自动通过的单据今后一律进入审批队列。
|
2026-09-10 11:32:51 +08:00 |
|
|
|
ed351f96ec
|
refactor(scrap): 按单执行校验改用 SKU 主键匹配
架构要求以 SKU 为唯一校验键,原实现按 (source_table, stock_id) 匹配。
已核验数据前提:SKU 在同一库存表内唯一(0 重复),且不存在跨表重名
(stock_buy / stock_semi / stock_product 交叉 0 冲突),故 SKU 可安全
作为跨来源标识。
改动:
· 新增 _match_key():SKU 非空时以 'sku:<SKU>' 为键;个别历史库存行
SKU 为空,回退 'row:<table>#<id>',前缀区分确保绝不串键;
· _build_approved_index() 按 SKU 聚合,累加批准量并保留各行的
source_table + stock_id(rows 列表);
· 校验拆为两步并给出明确文案:SKU 是否在批准明细内、同 SKU 累计量
是否超批准量;
· ★ 扣减改为按批准单配对的 source_table + stock_id 定位库存行加锁,
不再信任前端传来的 stock_id —— 实测前端传伪造 stock_id=99999 时,
仍从批准单指定的库存行扣减,台账同样记录批准单的 stock_id;
· 同一 SKU 在批准单占多行时,按批准顺序依次分配扣减量。
实测 7 项校验用例 + 多行分配 / 批准行已删除 / 可用不足 均符合预期。
|
2026-09-10 10:21:50 +08:00 |
|
|
|
7719943779
|
feat(scrap): 报废执行改为按单扫码校验,废弃盲执行
execute() 原先只吃 request_id,按申请单快照全量扣减,用户反馈「扫码与报废脱节」。
现改为接收实扫明细 scanned_items:
· 强制按单:未提交实扫明细一律拒绝执行;
· 键为 (source_table, stock_id),与申请单 items_json 同口径,
同一物品多次扫码自动累加,兼容「逐件扫」与「输数量」两种操作;
· 校验实扫项必须落在批准明细内,且累计量不得超批准量,否则整单拒绝;
· 允许合法子集(少扫 = 本次不报废该行);
· 扣减以实扫量为准,同时扣 available_quantity 与 stock_quantity
(报废即实物销毁,上一版只扣可用库存会让总库存虚挂)。
接口 POST /scrap/request/<id>/execute 由无 body 改为接收 {items:[...]}。
|
2026-09-10 10:14:40 +08:00 |
|
|
|
8755837fe8
|
fix(timezone): 审批单据时间统一为 naive 北京时间,消除 8 小时偏差
成因:approved_at / executed_at 列是 timestamp without time zone,而赋值用了
带时区的 datetime.now(timezone(timedelta(hours=8)))。psycopg2 对 naive 列不会
剥掉 tzinfo,而是转成 naive UTC 再写入,结果比同为北京时间的 created_at 早 8 小时。
(代码里并无 datetime.utcnow(),真实成因是 aware 值被驱动的隐式 UTC 转换。)
改动:
· outbound_service / borrow_service 的审批分支
datetime.now(beijing_tz).replace(tzinfo=None) → beijing_time()
(免审批分支已在上一轮改为 beijing_time,此处补齐审批分支);
· purchase_service 审批/完结分支同源缺陷一并修复;
· scrap_approval / scrap_approval_service 的 _beijing()、_beijing_now() 由
tz-aware 改为 naive —— 配合上述迁移把列类型改为 naive,
若仍返回 aware 值,timestamptz→timestamp 后会反向早 8 小时。
实测四表(scrap/outbound/borrow/purchase)created_at 与 approved_at 差值
均在同一秒内(-1ms ~ -12ms)。
|
2026-09-10 10:14:31 +08:00 |
|
|
|
ffbb2199f0
|
feat(scrap): 报废申请审批流 + 按单报废(后端)
- ScrapApproval 模型 + ScrapApprovalService:提交/列表/审批/执行(锁库存扣 available_quantity 写 TransScrap)
- 新路由 POST /scrap/request、/request/check-approval、GET /request、PATCH /request/<id>/approve、POST /request/<id>/execute;权限码 scrap_apply/scrap_approval/scrap_execute(无角色硬编码)
- 旧 /scrap 直接报废入口保持不变
- /inbound/stock/list 每项返回 source_table,供按单流程精准选库存
|
2026-09-10 09:46:57 +08:00 |
|