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
|
||||
|
||||
|
||||
@ -532,6 +532,37 @@ const handleApprovalChange = async (val: number | null) => {
|
||||
}
|
||||
// ★ 自动关联:把该借库申请单的“申请原因(remark)”自动带入执行备注,库管无需重复填写(可手动改)
|
||||
form.remark = (req && req.remark) || ''
|
||||
|
||||
// ★ 领用人自动回填
|
||||
//
|
||||
// 口径差异:审批单上的 borrower_name 存的是**完整 username**
|
||||
// (形如「杜邢宸/duxingchen」),而人员名单返回的是展示名(「杜邢宸」,
|
||||
// 后端已按斜杠前段截断)。两边都归一化到「斜杠前段」再比对,
|
||||
// 否则永远匹配不上、下拉框恒为空。
|
||||
//
|
||||
// 优先级:borrower_name 优先 —— 它才是「谁要借」的意图;库管代建时
|
||||
// applicant_id 是库管本人,回退到它会选错人。仅当审批单没记借用人姓名时
|
||||
// 才回退到申请人。
|
||||
//
|
||||
// 匹配不到时**保持未选**,不回退到自由文本:借用人 ID 是转交/归还责任链的
|
||||
// 唯一锚点,宁可让库管手动选,也不能猜一个。
|
||||
await loadBorrowerOptions()
|
||||
form.borrower_id = null
|
||||
form.borrower_name = ''
|
||||
if (req) {
|
||||
const wantName = String(req.borrower_name || '').split('/')[0].trim()
|
||||
let hit = wantName
|
||||
? borrowerOptions.value.find(u => String(u.name || '').split('/')[0].trim() === wantName)
|
||||
: undefined
|
||||
if (!hit && req.applicant_id) {
|
||||
hit = borrowerOptions.value.find(u => u.id === req.applicant_id)
|
||||
}
|
||||
if (hit) {
|
||||
form.borrower_id = hit.id
|
||||
form.borrower_name = hit.name
|
||||
}
|
||||
}
|
||||
|
||||
cartItems.value = []
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
@ -734,11 +765,17 @@ const form = reactive({
|
||||
|
||||
// ★ 借用人名单:与转交接收人、归还人共用同一数据源,按公司隔离
|
||||
const borrowerOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
let borrowerOptionsLoaded = false
|
||||
|
||||
// 可重复调用:已加载则直接返回;必须可 await ——
|
||||
// ★ 选中审批单时要拿这份名单反查借用人,若名单尚未返回就比对,
|
||||
// 会误判为「找不到」而清空选择。
|
||||
const loadBorrowerOptions = async () => {
|
||||
if (borrowerOptionsLoaded) return
|
||||
try {
|
||||
const res: any = await getBorrowUsers()
|
||||
borrowerOptions.value = res?.data || []
|
||||
borrowerOptionsLoaded = true
|
||||
} catch {
|
||||
borrowerOptions.value = []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user