diff --git a/inventory-backend/app/services/inventory_reservation.py b/inventory-backend/app/services/inventory_reservation.py index 2ef7d73..754c7a5 100644 --- a/inventory-backend/app/services/inventory_reservation.py +++ b/inventory-backend/app/services/inventory_reservation.py @@ -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 diff --git a/inventory-web/src/views/transaction/borrow.vue b/inventory-web/src/views/transaction/borrow.vue index 1791641..7daa100 100644 --- a/inventory-web/src/views/transaction/borrow.vue +++ b/inventory-web/src/views/transaction/borrow.vue @@ -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>([]) +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 = [] }