# 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.
