ForgeFlow-ERP/docs/superpowers/plans/2026-06-01-production-surplus-light-source-lots.md
2026-06-12 16:00:56 +08:00

291 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
<div v-if="activeOperation?.direction !== 'out'" class="double-field">
```
to:
```vue
<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:
```vue
<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:
```vue
<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:
```vue
<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()`:
```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.