# Production Surplus Lightweight Source-Lot Return Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make 原材料库「生产余料入库」hide unnecessary free-text fields and return surplus weight against one or more selected source stock lots. **Architecture:** Keep the existing generic `/inventory/inbound` API and existing `PRODUCTION_SURPLUS` business type. The frontend special-cases only the generic drawer when `activeOperation.bizType === "PRODUCTION_SURPLUS"`: hide batch/provider fields, replace free-text source lot input with a searchable multi-select, show per-source-lot return weight rows, and submit one inbound record per selected source lot. **Tech Stack:** Vue 3 Composition API, existing `StockLotTagSelect.vue`, existing `fetchResource`/`postResource`, FastAPI existing generic inbound endpoint. --- ## File Structure - Modify: `frontend/src/views/InventoryLedgerView.vue` - Add source-lot option state for generic production surplus inbound. - Hide `批次号` and `来源/提供方` for `PRODUCTION_SURPLUS`. - Replace the single text input for `来源库存批次号` with `StockLotTagSelect` for `PRODUCTION_SURPLUS`. - Add a per-source-lot return weight table and submit one inbound request per source lot. - No backend model/table changes. - No new SQL. ## Task 1: Frontend State And Option Loading **Files:** - Modify: `frontend/src/views/InventoryLedgerView.vue` - [ ] **Step 1: Add generic source lot state** Add refs near the existing `productionStockLotOptions` and `selectedProductionStockLots` declarations: ```js const genericSourceLotOptions = ref([]); const selectedGenericSourceLots = ref([]); let genericSourceLotOptionsRequestSeq = 0; ``` - [ ] **Step 2: Add computed helpers** Add computed values near `genericRequiresLogistics`: ```js const isProductionSurplusInbound = computed(() => activeOperation.value?.direction === "in" && activeOperation.value?.bizType === "PRODUCTION_SURPLUS" ); const genericSourceLotReturnWeight = computed(() => selectedGenericSourceLots.value.reduce((total, lot) => total + Number(lot.return_weight_kg || 0), 0) ); ``` - [ ] **Step 3: Reset selected source lots when generic form resets** Update `resetGenericForm()` so it clears the selected lots and option list: ```js genericSourceLotOptionsRequestSeq += 1; selectedGenericSourceLots.value = []; genericSourceLotOptions.value = []; ``` - [ ] **Step 4: Load source lot options by material** Add this function near `loadProductionStockLotOptions`: ```js async function loadGenericSourceLotOptions(materialItemId) { const requestSeq = ++genericSourceLotOptionsRequestSeq; const id = Number(materialItemId || 0); if (!id || !isProductionSurplusInbound.value) { genericSourceLotOptions.value = []; selectedGenericSourceLots.value = []; return; } try { const rows = await fetchResource(`/inventory/stock-lot-options?material_item_id=${id}&limit=1000`, []); if (requestSeq !== genericSourceLotOptionsRequestSeq) { return; } genericSourceLotOptions.value = Array.isArray(rows) ? rows : []; const optionMap = new Map(genericSourceLotOptions.value.map((lot) => [Number(lot.lot_id), lot])); selectedGenericSourceLots.value = selectedGenericSourceLots.value .filter((lot) => optionMap.has(Number(lot.lot_id))) .map((lot) => ({ ...optionMap.get(Number(lot.lot_id)), return_weight_kg: Number(lot.return_weight_kg || 0) })); } catch (error) { if (requestSeq === genericSourceLotOptionsRequestSeq) { genericSourceLotOptions.value = []; selectedGenericSourceLots.value = []; errorMessage.value = error.message || "来源库存批次号加载失败"; } } } ``` - [ ] **Step 5: Watch material and operation changes** Add watchers near the existing Vue watchers: ```js watch( () => [genericForm.item_id, activeOperation.value?.bizType, genericDrawerOpen.value], async () => { if (!genericDrawerOpen.value || !isProductionSurplusInbound.value) { return; } selectedGenericSourceLots.value = []; await loadGenericSourceLotOptions(genericForm.item_id); } ); watch( selectedGenericSourceLots, () => { if (isProductionSurplusInbound.value) { genericForm.weight_kg = Number(genericSourceLotReturnWeight.value.toFixed(3)); } }, { deep: true } ); ``` ## Task 2: Frontend Template **Files:** - Modify: `frontend/src/views/InventoryLedgerView.vue` - [ ] **Step 1: Hide batch/provider fields for production surplus inbound** Change this condition: ```vue
``` to: ```vue
``` - [ ] **Step 2: Hide top-level inbound weight for production surplus inbound** Add `v-if="!isProductionSurplusInbound"` to the 入库/出库 weight label: ```vue ``` - [ ] **Step 3: Replace source-lot text input with multi-select for production surplus inbound** Replace the existing `来源库存批次号` label with two branches: ```vue ``` - [ ] **Step 4: Add return-weight table** Add this block after the production-surplus `StockLotTagSelect` label: ```vue
来源库存批次号 原材料 剩余重量 归还重量(kg)
{{ lot.lot_no }} {{ lot.item_code }} · {{ lot.item_name }} {{ formatWeight(lot.remaining_weight_kg) }}
``` ## Task 3: Submission Logic **Files:** - Modify: `frontend/src/views/InventoryLedgerView.vue` - [ ] **Step 1: Add validation for production surplus source lots** Add this function before `submitGenericOperation()`: ```js function validateProductionSurplusSourceLots() { if (!isProductionSurplusInbound.value) { return ""; } if (!selectedGenericSourceLots.value.length) { return "请选择至少一个来源库存批次号"; } for (const lot of selectedGenericSourceLots.value) { const weight = Number(lot.return_weight_kg || 0); if (!Number.isFinite(weight) || weight <= 0) { return `来源库存批次号 ${lot.lot_no || "-"} 的归还重量必须大于0`; } } return ""; } ``` - [ ] **Step 2: Submit one inbound record per selected source lot** At the start of the inbound branch inside `submitGenericOperation()`, add a special case: ```js if (isProductionSurplusInbound.value) { const validationError = validateProductionSurplusSourceLots(); if (validationError) { errorMessage.value = validationError; submittingOperation.value = false; return; } for (const lot of selectedGenericSourceLots.value) { await postResource("/inventory/inbound", { biz_type: activeOperation.value.bizType, item_id: Number(genericForm.item_id), warehouse_id: Number(genericForm.warehouse_id), location_id: genericForm.location_id ? Number(genericForm.location_id) : null, inbound_weight_kg: Number(lot.return_weight_kg || 0), unit_cost: Number(genericForm.unit_cost || 0), lot_no: null, source_material_sub_batch_no: lot.lot_no || null, source_material_summary: lot.lot_no || null, provider_name: null, waybill_no: null, order_photo_url: null, freight_amount: null, remark: genericForm.remark || null }); } feedbackMessage.value = `${activeOperation.value.label}已登记`; genericDrawerOpen.value = false; await loadAll(); return; } ``` ## Task 4: Verification **Files:** - Verify existing frontend/backend. - [ ] **Step 1: Build frontend** Run: ```bash cd frontend && npm run build ``` Expected: build succeeds. Vite chunk size warning is acceptable if present. - [ ] **Step 2: Run backend tests** Run: ```bash cd backend && .venv/bin/python -m pytest tests -q ``` Expected: all existing backend tests pass. No new backend behavior is introduced.