fix(borrow): 恢复选中审批单后的「一键带出」,并补上领用人自动回填
现象
----
借库执行页选中一张已通过的审批单后,下方的「预计归还日期」「备注说明」
不再自动带出;「领用人」在改为用户下拉后也恒为空,库管每次都得手工补填。
根因(不在前端下拉框,而在后端持久化)
----
borrow_service.submit_approval 会把 reserve_for_items() 的返回值直接
set_items() 落库 —— 即审批单的 items_json **就是预占结果**。
而 reserve_for_items 内部是 `reserved.append({...})` **重建**字典,只保留
库存定位与数量,把申请端按明细传上来的 expected_return_time / is_indefinite
全部丢弃。执行页读的正是 firstItem.expected_return_time,读不到就落到
「清空」分支。
这是 2026-09-10 库存预占改造引入的回退:
b57c21a fix 前: approval.set_items(items) # 原始申请明细
b57c21a fix 后: approval.set_items(reserved_items) # 重建字典,字段丢失
数据完全吻合该提交的上线时刻(09-10 14:59):
最后一张「有日期」的审批单 id=40 创建于 09-10 09:30
第一张「丢失日期」的审批单 id=41 创建于 09-10 16:20
修复
----
1) inventory_reservation.reserve_for_items:透传申请「意图字段」
(expected_return_time / is_indefinite / remark)到预占结果。
· 一条申请明细会被拆到多个批次行(同物料多批次),故按 base_id 建索引,
把同一份意图回填到它拆分出的每一行;
· **只透传意图,不透传库存定位与数量** —— 实扫可能换批次,
这里写什么执行页就按什么释放,绝不能覆盖分配结果;
· 跳过 None:无限期借用提交的 expected_return_time 本就是 null。
2) borrow.vue handleApprovalChange:实现领用人自动回填
· 口径差异:审批单上的 borrower_name 是**完整 username**
(「杜邢宸/duxingchen」),人员名单返回的是展示名(「杜邢宸」)。
两边归一化到「斜杠前段」再比对,否则永远匹配不上;
· borrower_name 优先,匹配不到才回退 applicant_id —— 库管代建时申请人是
库管本人,回退到它会选错人;
· 名单加载改为可重复 await:选中单据时要拿它反查,名单没回来就比对会
误判为「找不到」而清空;
· 匹配不到时保持未选,不回退到自由文本 —— 借用人 ID 是转交/归还责任链的
唯一锚点,宁可让库管手选也不能猜。
可编辑性
----
三个字段均保持可改:领用人下拉、备注文本框无 disabled;日期选择器仅在勾选
「无限期/长期借用」时置灰(原有语义,取消勾选即可重新填写)。
影响与验证
----
· 存量:仅 1 张待执行审批单(id=46)受影响,其日期在库中已无任何留存,
无法回填,需库管手工补一次;已完结单据不受影响。
· 回归:reserve_for_items 透传 9 项断言、submit_approval 端到端 5 项断言
全部通过;库存精确还原、无残留数据。
This commit is contained in:
@ -233,6 +233,40 @@ def reserve_for_items(items, company_limit=None, strict=True):
|
||||
|
||||
# ---- 立即扣减 available_quantity(软锁入池)----
|
||||
models = stock_model_map()
|
||||
|
||||
# ==================================================================
|
||||
# ★ 透传申请明细的「意图字段」到预占结果
|
||||
#
|
||||
# 背景:本函数产出的 reserved 会被调用方**直接持久化** ——
|
||||
# borrow_service.submit_approval 中 `approval.set_items(reserved_items)`,
|
||||
# 即审批单的 items_json 就是下面 reserved.append({...}) 的内容。
|
||||
# 而该 append 是**重建**字典,只保留库存定位与数量,会把调用方传入的
|
||||
# expected_return_time / is_indefinite / remark 全部丢弃 ——
|
||||
# 执行页(扫码发货)选中审批单后因此带不出「预计归还日期」与「长期借用」,
|
||||
# 库管只能每次手工补填。
|
||||
#
|
||||
# 实测证据:2026-09-10 预占改造(b57c21a)上线后新建的审批单,items_json
|
||||
# 中这两个字段全部消失;此前的老单据(id ≤ 40)仍保留。分界点与该提交的
|
||||
# 上线时刻(09-10 14:59)完全吻合。
|
||||
#
|
||||
# 一条申请明细会被拆到多个批次行(同物料多批次),故按 base_id 建索引,
|
||||
# 把同一份「意图」回填到它拆分出的每一行上。
|
||||
# ==================================================================
|
||||
intent_by_base = {}
|
||||
intent_by_name = {}
|
||||
for it in items:
|
||||
if it.get('base_id') is not None:
|
||||
intent_by_base.setdefault(it.get('base_id'), it)
|
||||
intent_by_name.setdefault(
|
||||
(str(it.get('name') or '').strip(),
|
||||
str(it.get('spec_model') or it.get('standard') or '').strip()),
|
||||
it,
|
||||
)
|
||||
|
||||
# 仅透传「申请意图」。库存定位(source_table/stock_id)与数量由分配结果决定,
|
||||
# 绝不能被原明细覆盖 —— 实扫可能换批次,这里写什么,执行页就按什么释放。
|
||||
INTENT_FIELDS = ('expected_return_time', 'is_indefinite', 'remark')
|
||||
|
||||
reserved = []
|
||||
for a in alloc_items:
|
||||
take = float(a.get('allocated_qty') or 0)
|
||||
@ -256,7 +290,7 @@ def reserve_for_items(items, company_limit=None, strict=True):
|
||||
)
|
||||
row.available_quantity = avail - take
|
||||
|
||||
reserved.append({
|
||||
item = {
|
||||
'base_id': a.get('base_id'),
|
||||
'name': a.get('name') or '',
|
||||
'spec_model': a.get('standard') or a.get('spec_model') or '',
|
||||
@ -267,7 +301,21 @@ def reserve_for_items(items, company_limit=None, strict=True):
|
||||
'allocated_qty': take,
|
||||
'quantity': take, # 兼容既有字段名(申请量=预占量)
|
||||
'reserved': True, # ★ 标记该行已预占,释放时据此还原
|
||||
})
|
||||
}
|
||||
|
||||
# ★ 回填申请意图(见上方 INTENT_FIELDS 说明)。
|
||||
# 跳过 None:无限期借用提交的 expected_return_time 就是 null,
|
||||
# 写入 null 无意义,前端读到的仍是 undefined,行为一致。
|
||||
origin = intent_by_base.get(a.get('base_id')) or intent_by_name.get(
|
||||
(str(a.get('name') or '').strip(),
|
||||
str(a.get('standard') or a.get('spec_model') or '').strip())
|
||||
)
|
||||
if origin is not None:
|
||||
for f in INTENT_FIELDS:
|
||||
if origin.get(f) is not None:
|
||||
item[f] = origin.get(f)
|
||||
|
||||
reserved.append(item)
|
||||
|
||||
return reserved, shortages
|
||||
|
||||
|
||||
Reference in New Issue
Block a user