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

9.2 KiB
Raw Blame History

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:

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.