9.2 KiB
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来源/提供方forPRODUCTION_SURPLUS. - Replace the single text input for
来源库存批次号withStockLotTagSelectforPRODUCTION_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:
const genericSourceLotOptions = ref([]);
const selectedGenericSourceLots = ref([]);
let genericSourceLotOptionsRequestSeq = 0;
- Step 2: Add computed helpers
Add computed values near genericRequiresLogistics:
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:
genericSourceLotOptionsRequestSeq += 1;
selectedGenericSourceLots.value = [];
genericSourceLotOptions.value = [];
- Step 4: Load source lot options by material
Add this function near loadProductionStockLotOptions:
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:
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:
<div v-if="activeOperation?.direction !== 'out'" class="double-field">
to:
<div v-if="activeOperation?.direction !== 'out' && !isProductionSurplusInbound" class="double-field">
- Step 2: Hide top-level inbound weight for production surplus inbound
Add v-if="!isProductionSurplusInbound" to the 入库/出库 weight label:
<label v-if="!isProductionSurplusInbound" class="form-field">
<span>{{ activeOperation?.direction === "out" ? "出库重量(kg)" : "入库重量(kg)" }}</span>
<input v-model.number="genericForm.weight_kg" type="number" min="0.001" step="0.001" required />
</label>
- Step 3: Replace source-lot text input with multi-select for production surplus inbound
Replace the existing 来源库存批次号 label with two branches:
<label v-if="!isProductionSurplusInbound" class="form-field">
<span>来源库存批次号</span>
<input v-model.trim="genericForm.source_material_sub_batch_no" type="text" placeholder="有来源库存批次时填写" />
</label>
<label v-else class="form-field">
<span>来源库存批次号</span>
<StockLotTagSelect v-model="selectedGenericSourceLots" :options="genericSourceLotOptions" placeholder="搜索并选择归还到的来源库存批次号,可多选" />
</label>
- Step 4: Add return-weight table
Add this block after the production-surplus StockLotTagSelect label:
<div v-if="isProductionSurplusInbound && selectedGenericSourceLots.length" class="table-wrap">
<table class="data-table compact-table">
<thead>
<tr>
<th>来源库存批次号</th>
<th>原材料</th>
<th>剩余重量</th>
<th>归还重量(kg)</th>
</tr>
</thead>
<tbody>
<tr v-for="lot in selectedGenericSourceLots" :key="lot.lot_id">
<td>{{ lot.lot_no }}</td>
<td>{{ lot.item_code }} · {{ lot.item_name }}</td>
<td>{{ formatWeight(lot.remaining_weight_kg) }}</td>
<td>
<input v-model.number="lot.return_weight_kg" type="number" min="0.001" step="0.001" required />
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="isProductionSurplusInbound" class="feedback-banner">
本次归还总重量:{{ formatWeight(genericSourceLotReturnWeight) }}
</div>
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():
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:
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:
cd frontend && npm run build
Expected: build succeeds. Vite chunk size warning is acceptable if present.
- Step 2: Run backend tests
Run:
cd backend && .venv/bin/python -m pytest tests -q
Expected: all existing backend tests pass. No new backend behavior is introduced.