# Single Warehouse Stocktake Flow Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Change 百华仓库盘库 into a current-selected-warehouse-only flow: select a warehouse bar first, click 盘库, confirm/export one locked warehouse sheet, import only that same stocktake workbook, show a modal diff immediately, and confirm to reset that warehouse inventory and unlock it.
**Architecture:** Backend becomes the source of truth for single-warehouse stocktake rules, workbook validation token checks, import overwrite behavior, and current-warehouse history filtering. Frontend removes multi-warehouse selection from the stocktake dialog and renders two blocks: a stocktake flow block for the currently selected warehouse and a stocktake records block filtered to that warehouse. The diff review is opened as a modal immediately after each import; cancel closes the modal and leaves the warehouse locked for re-import.
**Tech Stack:** FastAPI, SQLAlchemy ORM, openpyxl, Python unittest, Vue 3 Composition API, Node built-in `assert`, Vite.
---
## Scope Rules
- Keep existing stocktake tables: `wh_stocktake`, `wh_stocktake_warehouse`, `wh_stocktake_line`, and `wh_stocktake_adjustment`.
- Do not add a migration unless tests prove existing columns cannot represent the requested flow.
- Backend must reject multi-warehouse stocktake creation even if a caller bypasses the UI.
- Backend must reject import files that are not for the current stocktake number and current warehouse.
- Re-importing a corrected workbook must replace the previous imported diff preview for that same stocktake before confirmation.
- Frontend must not let the user choose warehouses inside the stocktake dialog. The active warehouse comes from the warehouse bar selection.
- The workspace root `/Users/souplearn/Gitlab/py/ForgeFlow-ERP` is not a git repository in this environment, so plan steps do not include commits.
## File Structure
- Modify: `backend/app/services/stocktake.py`
- Enforce one warehouse per stocktake.
- Build and validate workbook check tokens.
- Export one warehouse workbook with a validation marker.
- Import only matching current-stocktake/current-warehouse workbooks.
- Reset previous imported preview rows before re-import.
- Modify: `backend/app/api/routes/inventory.py`
- Filter stocktake history by current warehouse with `warehouse_id`.
- Keep import/export route shapes stable.
- Modify: `backend/app/schemas/operations.py`
- Tighten `StocktakeStartCreate` to one warehouse in request validation if supported by current Pydantic.
- Modify: `backend/tests/test_stocktake_excel_diff_confirm.py`
- Add backend regression tests for single-warehouse start, validation marker export, wrong stocktake import rejection, wrong warehouse import rejection, and re-import overwrite.
- Modify: `frontend/src/components/StocktakeDialog.vue`
- Replace multi-warehouse overview cards with current warehouse flow + current warehouse records.
- Start and export in one user action.
- Open diff as modal immediately after import.
- Cancel diff modal without unlocking; allow re-import.
- Modify: `frontend/src/utils/stocktakeWorkflow.js`
- Add current-warehouse-only helpers for active stocktake lookup and panel routing.
- Modify: `frontend/scripts/test-stocktake-workflow.mjs`
- Update workflow tests to cover current-selected-warehouse behavior and no global fallback.
- Modify: `frontend/src/styles/main.css`
- Style the two-block stocktake dialog, current warehouse badge, and diff modal.
---
## Task 1: Backend Failing Tests for Single-Warehouse Rules
**Files:**
- Modify: `backend/tests/test_stocktake_excel_diff_confirm.py`
- [ ] **Step 1: Add imports used by new tests**
Ensure this import line includes `Workbook`:
```python
from openpyxl import Workbook, load_workbook
```
If the file currently imports `Workbook` inside a test method, leave local imports alone unless duplicate imports become confusing.
- [ ] **Step 2: Add a test that multi-warehouse stocktake creation is rejected**
Append this method to `StocktakeExcelDiffConfirmTest`:
```python
def test_start_stocktake_rejects_multiple_warehouses_for_current_bar_flow(self) -> None:
from fastapi import HTTPException
now = datetime.now(UTC)
aux = Warehouse(
id=2,
warehouse_code="WH-AUX-01",
warehouse_name="辅料库",
warehouse_type="AUX",
status="ACTIVE",
created_at=now,
updated_at=now,
)
self.db.add(aux)
self.db.commit()
with self.assertRaises(HTTPException) as exc:
start_stocktake(self.db, warehouse_ids=[self.raw.id, aux.id], user_id=1, remark="多库盘库")
self.assertEqual(exc.exception.status_code, 400)
self.assertIn("一次只能盘一个仓库", exc.exception.detail)
```
- [ ] **Step 3: Add a test that export includes a validation marker and one warehouse sheet**
Append this method:
```python
def test_single_warehouse_export_contains_validation_marker(self) -> None:
stocktake = start_stocktake(self.db, warehouse_ids=[self.raw.id], user_id=1, remark="原材料盘库")
workbook = build_stocktake_workbook(self.db, stocktake.id)
output = BytesIO()
workbook.save(output)
loaded = load_workbook(BytesIO(output.getvalue()), read_only=True, data_only=True)
self.assertEqual(loaded.sheetnames, ["原材料库"])
rows = list(loaded["原材料库"].iter_rows(values_only=True))
headers = rows[0]
self.assertIn("盘库校验码", headers)
self.assertIn("仓库ID", headers)
self.assertEqual(rows[1][headers.index("盘库单号")], stocktake.stocktake_no)
self.assertEqual(rows[1][headers.index("仓库ID")], self.raw.id)
self.assertEqual(
rows[1][headers.index("盘库校验码")],
f"{stocktake.stocktake_no}|{self.raw.id}|RAW",
)
```
- [ ] **Step 4: Add a helper for workbook mutation in tests**
Append this private helper method to the test class:
```python
def _exported_workbook_bytes(self, stocktake) -> bytes:
workbook = build_stocktake_workbook(self.db, stocktake.id)
output = BytesIO()
workbook.save(output)
return output.getvalue()
```
- [ ] **Step 5: Add a test that wrong stocktake number is rejected**
Append this method:
```python
def test_import_rejects_workbook_from_other_stocktake_number(self) -> None:
from app.services.stocktake import import_stocktake_workbook
stocktake = start_stocktake(self.db, warehouse_ids=[self.raw.id], user_id=1, remark="原材料盘库")
workbook = load_workbook(BytesIO(self._exported_workbook_bytes(stocktake)))
worksheet = workbook["原材料库"]
headers = [cell.value for cell in worksheet[1]]
worksheet.cell(row=2, column=headers.index("盘库单号") + 1).value = "PK-OTHER-001"
worksheet.cell(row=2, column=headers.index("盘库校验码") + 1).value = "PK-OTHER-001|1|RAW"
output = BytesIO()
workbook.save(output)
with self.assertRaises(ValueError) as exc:
import_stocktake_workbook(self.db, stocktake.id, output.getvalue(), user_id=1)
self.assertIn("不属于当前盘库单", str(exc.exception))
```
- [ ] **Step 6: Add a test that wrong warehouse marker is rejected**
Append this method:
```python
def test_import_rejects_workbook_from_other_warehouse(self) -> None:
from app.services.stocktake import import_stocktake_workbook
stocktake = start_stocktake(self.db, warehouse_ids=[self.raw.id], user_id=1, remark="原材料盘库")
workbook = load_workbook(BytesIO(self._exported_workbook_bytes(stocktake)))
worksheet = workbook["原材料库"]
headers = [cell.value for cell in worksheet[1]]
worksheet.cell(row=2, column=headers.index("仓库") + 1).value = "成品库"
worksheet.cell(row=2, column=headers.index("仓库ID") + 1).value = 999
worksheet.cell(row=2, column=headers.index("盘库校验码") + 1).value = f"{stocktake.stocktake_no}|999|FINISHED"
output = BytesIO()
workbook.save(output)
with self.assertRaises(ValueError) as exc:
import_stocktake_workbook(self.db, stocktake.id, output.getvalue(), user_id=1)
self.assertIn("不属于当前仓库", str(exc.exception))
```
- [ ] **Step 7: Add a test that re-import overwrites previous diff preview**
Append this method:
```python
def test_reimport_overwrites_previous_stocktake_preview(self) -> None:
from app.services.stocktake import import_stocktake_workbook
stocktake = start_stocktake(self.db, warehouse_ids=[self.raw.id], user_id=1, remark="原材料盘库")
workbook = load_workbook(BytesIO(self._exported_workbook_bytes(stocktake)))
worksheet = workbook["原材料库"]
headers = [cell.value for cell in worksheet[1]]
worksheet.cell(row=2, column=headers.index("盘点重量(kg)") + 1).value = 90
output = BytesIO()
workbook.save(output)
first_preview = import_stocktake_workbook(self.db, stocktake.id, output.getvalue(), user_id=1)
self.assertEqual(first_preview["summary"]["loss_count"], 1)
workbook = load_workbook(BytesIO(self._exported_workbook_bytes(stocktake)))
worksheet = workbook["原材料库"]
headers = [cell.value for cell in worksheet[1]]
worksheet.cell(row=2, column=headers.index("盘点重量(kg)") + 1).value = 100
output = BytesIO()
workbook.save(output)
second_preview = import_stocktake_workbook(self.db, stocktake.id, output.getvalue(), user_id=1)
self.assertEqual(second_preview["summary"]["loss_count"], 0)
self.assertEqual(second_preview["summary"]["match_count"], 1)
line = self.db.scalar(select(StocktakeLine).where(StocktakeLine.stocktake_id == stocktake.id))
self.assertEqual(line.diff_type, "MATCH")
self.assertEqual(float(line.counted_weight_kg), 100)
```
- [ ] **Step 8: Run tests and confirm they fail for missing implementation**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
.venv/bin/python -m unittest tests.test_stocktake_excel_diff_confirm
```
Expected: fail because current backend still allows multi-warehouse stocktakes and does not export/validate `盘库校验码` and `仓库ID`.
---
## Task 2: Backend Single-Warehouse Service Implementation
**Files:**
- Modify: `backend/app/services/stocktake.py`
- Modify: `backend/app/schemas/operations.py`
- [ ] **Step 1: Add workbook validation headers**
In `backend/app/services/stocktake.py`, extend `STOCKTAKE_EXCEL_HEADERS` so the first columns are:
```python
STOCKTAKE_EXCEL_HEADERS = [
"盘库单号",
"盘库校验码",
"仓库ID",
"快照行ID",
"仓库",
"库位",
"物料编码",
"物料名称",
"批次号",
"来源材料分批次号",
"系统数量",
"系统重量(kg)",
"系统单价",
"盘点数量",
"盘点重量(kg)",
"盘点备注",
]
```
- [ ] **Step 2: Add validation token helper**
Add below `stocktake_warehouse_type_label`:
```python
def stocktake_validation_token(stocktake_no: str, warehouse_id: int, warehouse_type: str | None) -> str:
return f"{stocktake_no}|{int(warehouse_id)}|{str(warehouse_type or '').upper()}"
```
- [ ] **Step 3: Enforce one warehouse in service**
In `start_stocktake`, after `ids = ...`, add:
```python
if len(set(ids)) != 1:
raise HTTPException(status_code=400, detail="一次只能盘一个仓库,请先在仓库栏选中单个仓库")
```
Keep the existing empty and invalid warehouse checks.
- [ ] **Step 4: Tighten schema request validation**
In `backend/app/schemas/operations.py`, change:
```python
class StocktakeStartCreate(BaseModel):
warehouse_ids: list[int] = Field(min_length=1)
remark: str | None = None
```
to:
```python
class StocktakeStartCreate(BaseModel):
warehouse_ids: list[int] = Field(min_length=1, max_length=1)
remark: str | None = None
```
- [ ] **Step 5: Update export rows to include token and warehouse ID**
In `build_stocktake_workbook`, compute token per stocktake warehouse. Replace worksheet append rows so empty marker rows start with:
```python
token = stocktake_validation_token(stocktake.stocktake_no, warehouse.id, warehouse.warehouse_type)
worksheet.append(
[
stocktake.stocktake_no,
token,
warehouse.id,
"",
sheet_name,
"",
"",
"",
"",
"",
0,
0,
0,
0,
0,
EMPTY_STOCKTAKE_MARKER_REMARK,
]
)
```
For normal stocktake lines, row values must start with:
```python
[
stocktake.stocktake_no,
stocktake_validation_token(stocktake.stocktake_no, line.warehouse_id, line.warehouse_type),
line.warehouse_id,
line.id,
line.warehouse_name,
...
]
```
Update column widths list to 16 columns:
```python
[18, 30, 10, 12, 14, 14, 18, 28, 22, 24, 14, 16, 12, 14, 16, 24]
```
- [ ] **Step 6: Add helper to get the single stocktake warehouse**
Add below `_stocktake_lines`:
```python
def _single_stocktake_warehouse(db: Session, stocktake_id: int) -> StocktakeWarehouse:
rows = db.scalars(
select(StocktakeWarehouse).where(StocktakeWarehouse.stocktake_id == stocktake_id).order_by(StocktakeWarehouse.id.asc())
).all()
if len(rows) != 1:
raise HTTPException(status_code=400, detail="当前盘库单不是单仓库盘库,无法继续处理")
return rows[0]
```
- [ ] **Step 7: Update empty marker detection for shifted columns**
No logic change is needed if `_is_empty_stocktake_marker_row` uses header labels. Verify it still checks `快照行ID`, material fields, and numeric columns by name.
- [ ] **Step 8: Add workbook validation helper**
Add below `_is_empty_stocktake_marker_row`:
```python
def _validate_stocktake_row_marker(row: dict[str, object], stocktake: Stocktake, warehouse: StocktakeWarehouse) -> None:
row_stocktake_no = _clean_text(row.get("盘库单号"))
if row_stocktake_no != stocktake.stocktake_no:
raise ValueError(f"存在不属于当前盘库单的行:{row_stocktake_no or '空盘库单号'}")
row_warehouse_id = _clean_text(row.get("仓库ID"))
if row_warehouse_id and int(float(row_warehouse_id)) != int(warehouse.warehouse_id):
raise ValueError("导入清单不属于当前仓库,请确认仓库栏选中项和盘库清单一致")
row_warehouse_name = _clean_text(row.get("仓库"))
if row_warehouse_name and row_warehouse_name != warehouse.warehouse_name:
raise ValueError("导入清单不属于当前仓库,请确认仓库栏选中项和盘库清单一致")
expected_token = stocktake_validation_token(stocktake.stocktake_no, warehouse.warehouse_id, warehouse.warehouse_type)
row_token = _clean_text(row.get("盘库校验码"))
if row_token != expected_token:
raise ValueError("盘库校验码不匹配,请导入本次盘库导出的仓库清单")
```
- [ ] **Step 9: Add re-import reset helper**
Add below `_validate_stocktake_row_marker`:
```python
def _reset_stocktake_preview_for_reimport(db: Session, stocktake_id: int, warehouse_id: int) -> dict[int, StocktakeLine]:
lines = db.scalars(
select(StocktakeLine)
.where(StocktakeLine.stocktake_id == stocktake_id, StocktakeLine.warehouse_id == warehouse_id)
.order_by(StocktakeLine.line_no.asc())
).all()
existing_lines: dict[int, StocktakeLine] = {}
for line in lines:
if line.lot_id is None and line.diff_type == "NEW_LOT":
db.delete(line)
continue
line.counted_qty = None
line.counted_weight_kg = None
line.diff_qty = Decimal("0")
line.diff_weight_kg = Decimal("0")
line.diff_amount = Decimal("0")
line.diff_type = "MATCH"
line.row_status = "SNAPSHOT"
line.error_message = None
line.remark = None
db.add(line)
existing_lines[line.id] = line
db.flush()
return existing_lines
```
- [ ] **Step 10: Rewrite import to validate current warehouse and overwrite previous preview**
In `import_stocktake_workbook`, after stocktake status check, add:
```python
stocktake_warehouse = _single_stocktake_warehouse(db, stocktake_id)
```
Then replace `existing_lines = {...}` with:
```python
existing_lines = _reset_stocktake_preview_for_reimport(db, stocktake_id, stocktake_warehouse.warehouse_id)
```
Inside `for row in rows`, replace the current stocktake number validation with:
```python
_validate_stocktake_row_marker(row, stocktake, stocktake_warehouse)
```
For new rows without `快照行ID`, remove lookup by arbitrary warehouse name and force the stocktake warehouse:
```python
warehouse = db.get(Warehouse, stocktake_warehouse.warehouse_id)
if not item or not warehouse:
raise ValueError(f"新增批次行物料或仓库不存在:{stocktake_warehouse.warehouse_name} / {item_code}")
```
When creating `StocktakeLine` for new rows, set:
```python
stocktake_warehouse_id=stocktake_warehouse.id,
warehouse_id=warehouse.id,
warehouse_name=warehouse.warehouse_name,
warehouse_type=str(warehouse.warehouse_type or "").upper(),
```
In the missing-line loop, only iterate current `existing_lines`.
- [ ] **Step 11: Return only current warehouse lines after import**
At the end of `import_stocktake_workbook`, replace final `lines = ...` query with:
```python
lines = db.scalars(
select(StocktakeLine)
.where(StocktakeLine.stocktake_id == stocktake_id, StocktakeLine.warehouse_id == stocktake_warehouse.warehouse_id)
.order_by(StocktakeLine.line_no.asc())
).all()
```
- [ ] **Step 12: Run backend stocktake tests**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
.venv/bin/python -m unittest tests.test_stocktake_excel_diff_confirm
.venv/bin/python -m unittest tests.test_stocktake_locking tests.test_stocktake_excel_diff_confirm
```
Expected: both commands pass with `OK`.
---
## Task 3: Backend Routes for Current-Warehouse History and Lines
**Files:**
- Modify: `backend/app/api/routes/inventory.py`
- [ ] **Step 1: Add warehouse filter to stocktake read summary**
Modify `_stocktake_read` signature:
```python
def _stocktake_read(db: Session, stocktake_id: int, warehouse_id: int | None = None) -> StocktakeRead:
```
Replace:
```python
lines = stocktake_lines_for_read(db, stocktake_id)
```
with:
```python
lines = stocktake_lines_for_read(db, stocktake_id)
if warehouse_id:
lines = [line for line in lines if int(line.warehouse_id) == int(warehouse_id)]
```
- [ ] **Step 2: Filter `/inventory/stocktakes` by warehouse ID**
Change route signature:
```python
def list_inventory_stocktakes(
limit: int = Query(default=100, ge=1, le=500),
warehouse_id: int | None = Query(default=None),
db: Session = Depends(get_db),
) -> list[StocktakeRead]:
```
Replace the query with:
```python
stmt = select(Stocktake).order_by(Stocktake.started_at.desc(), Stocktake.id.desc()).limit(limit)
if warehouse_id:
stmt = (
select(Stocktake)
.join(StocktakeWarehouse, StocktakeWarehouse.stocktake_id == Stocktake.id)
.where(StocktakeWarehouse.warehouse_id == warehouse_id)
.order_by(Stocktake.started_at.desc(), Stocktake.id.desc())
.limit(limit)
)
rows = db.scalars(stmt).all()
return [_stocktake_read(db, row.id, warehouse_id=warehouse_id) for row in rows]
```
- [ ] **Step 3: Filter lines endpoint by warehouse ID**
Change route signature:
```python
def list_inventory_stocktake_lines(
stocktake_id: int,
warehouse_id: int | None = Query(default=None),
db: Session = Depends(get_db),
) -> list[StocktakeLineRead]:
```
Replace return with:
```python
lines = stocktake_lines_for_read(db, stocktake_id)
if warehouse_id:
lines = [line for line in lines if int(line.warehouse_id) == int(warehouse_id)]
return [StocktakeLineRead.model_validate(_stocktake_line_read(line)) for line in lines]
```
- [ ] **Step 4: Preserve import preview response**
In `_stocktake_preview_read`, do not filter because backend import is now single-warehouse. Confirm it still returns `result["lines"]`.
- [ ] **Step 5: Run route-adjacent tests**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
.venv/bin/python -m unittest tests.test_stocktake_locking tests.test_stocktake_excel_diff_confirm
```
Expected: pass with `OK`.
---
## Task 4: Frontend Workflow Helper for Current Warehouse Only
**Files:**
- Modify: `frontend/src/utils/stocktakeWorkflow.js`
- Modify: `frontend/scripts/test-stocktake-workflow.mjs`
- [ ] **Step 1: Add helper for current warehouse history filtering**
In `frontend/src/utils/stocktakeWorkflow.js`, add:
```js
export function stocktakeBelongsToWarehouse(row, warehouseId) {
const id = Number(warehouseId || 0);
if (!id || !Array.isArray(row?.warehouses)) return false;
return row.warehouses.some((warehouse) => Number(warehouse.warehouse_id) === id);
}
export function filterStocktakesForWarehouse(historyRows = [], warehouseId) {
return historyRows.filter((row) => stocktakeBelongsToWarehouse(row, warehouseId));
}
```
- [ ] **Step 2: Replace global fallback helper usage policy in tests**
In `frontend/scripts/test-stocktake-workflow.mjs`, add imports:
```js
filterStocktakesForWarehouse,
stocktakeBelongsToWarehouse,
```
Add assertions:
```js
assert.equal(stocktakeBelongsToWarehouse(lockedRaw, 1), true);
assert.equal(stocktakeBelongsToWarehouse(lockedRaw, 4), false);
assert.deepEqual(filterStocktakesForWarehouse([lockedRaw, emptyExported], 1).map((row) => row.id), [12]);
```
- [ ] **Step 3: Add source assertions that global fallback is not used by dialog**
Add after existing component source assertions:
```js
assert.doesNotMatch(componentSource, /findOnlyActiveStocktake\\(historyRows\\.value\\)/);
assert.match(componentSource, /findActiveStocktakeForWarehouse\\(historyRows\\.value, currentWarehouseId\\.value\\)/);
assert.match(componentSource, /\\/inventory\\/stocktakes\\?warehouse_id=\\$\\{currentWarehouseId\\.value\\}/);
```
- [ ] **Step 4: Run frontend helper test and confirm it fails before dialog implementation**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-stocktake-workflow.mjs
```
Expected: fail because `StocktakeDialog.vue` still uses `findOnlyActiveStocktake(historyRows.value)` and does not request `warehouse_id`.
---
## Task 5: Frontend Single-Warehouse Dialog Flow
**Files:**
- Modify: `frontend/src/components/StocktakeDialog.vue`
- [ ] **Step 1: Update imports**
Remove unused imports if no longer needed:
```js
findOnlyActiveStocktake,
warehouseStocktakeStatus
```
Add:
```js
filterStocktakesForWarehouse,
```
- [ ] **Step 2: Replace multi-step labels**
Change `flowSteps` to:
```js
const flowSteps = [
{ key: "export", no: "01", label: "确认盘库并导出" },
{ key: "import", no: "02", label: "导入清单" }
];
```
- [ ] **Step 3: Replace selection state with current warehouse state**
Remove:
```js
const selectedWarehouseIds = ref([]);
const activeWarehouseStatuses = computed(...);
const selectedStartableWarehouseIds = computed(...);
const activeWarehouseNames = computed(...);
```
Add:
```js
const currentWarehouseId = computed(() => Number(props.defaultWarehouseId || 0));
const currentWarehouse = computed(
() => props.warehouses.find((warehouse) => Number(warehouse.id) === currentWarehouseId.value) || null
);
const currentWarehouseName = computed(() => currentWarehouse.value?.warehouse_name || "未选择仓库");
const currentWarehouseHistoryRows = computed(() => filterStocktakesForWarehouse(historyRows.value, currentWarehouseId.value));
```
- [ ] **Step 4: Replace open watcher**
Replace watcher body with:
```js
watch(
() => props.open,
async (open) => {
if (!open) return;
activePanel.value = "export";
activeStocktake.value = null;
previewLines.value = [];
previewSummary.value = {};
diffFilter.value = "ALL";
confirmDialogOpen.value = false;
diffDialogOpen.value = false;
dialogMessage.value = "";
dialogError.value = "";
if (!currentWarehouseId.value) {
setDialogError("请先在仓库栏选中一个仓库,再进行盘库");
return;
}
await loadHistory();
const activeRow = findActiveStocktakeForWarehouse(historyRows.value, currentWarehouseId.value);
if (activeRow) {
await continueStocktake(activeRow, { silent: true });
}
}
);
```
- [ ] **Step 5: Add diff modal state**
Add near `confirmDialogOpen`:
```js
const diffDialogOpen = ref(false);
```
In `applyLoadedStocktake`, set `diffDialogOpen.value = false` before loading state.
- [ ] **Step 6: Replace `loadHistory` with warehouse-filtered API call**
Replace `loadHistory` with:
```js
async function loadHistory() {
try {
if (!currentWarehouseId.value) {
historyRows.value = [];
return;
}
historyRows.value = await fetchResource(`/inventory/stocktakes?warehouse_id=${currentWarehouseId.value}&limit=100`, []);
} catch (error) {
setDialogError(error.message || "读取盘库记录失败");
}
}
```
- [ ] **Step 7: Replace start flow with start-and-export action**
Replace `startStocktake` with:
```js
async function startAndExportStocktake() {
clearDialogFeedback();
if (!currentWarehouseId.value) {
setDialogError("请先在仓库栏选中一个仓库");
return;
}
busy.value = true;
try {
const stocktake = await postResource("/inventory/stocktakes", {
warehouse_ids: [currentWarehouseId.value],
remark: `${currentWarehouseName.value}盘库`
});
activeStocktake.value = stocktake;
await downloadResource(`/inventory/stocktakes/${stocktake.id}/export`, `${stocktake.stocktake_no}_${currentWarehouseName.value}_盘库清单.xlsx`);
const exportedStocktake = await fetchResource(`/inventory/stocktakes/${stocktake.id}`, null);
if (!exportedStocktake) {
throw new Error("导出后读取盘库单失败,请重新打开盘库");
}
await loadHistory();
await applyLoadedStocktake(exportedStocktake, { silent: true });
setDialogMessage(`盘库单 ${exportedStocktake.stocktake_no} 已导出,${currentWarehouseName.value} 已锁库`);
} catch (error) {
setDialogError(error.message || "开始盘库并导出失败");
} finally {
busy.value = false;
}
}
```
Keep `exportStocktake` only if used for continuing a `LOCKED` stocktake that has not exported. If kept, make it use `currentWarehouseName`.
- [ ] **Step 8: Update import stocktake line loading to current warehouse**
In `applyLoadedStocktake`, change line fetch:
```js
const lineRows = await fetchResource(`/inventory/stocktakes/${stocktake.id}/lines?warehouse_id=${currentWarehouseId.value}`, null);
```
- [ ] **Step 9: Replace import fallback**
In `importStocktake`, replace fallback lookup with:
```js
const fallbackRow = findActiveStocktakeForWarehouse(historyRows.value, currentWarehouseId.value);
```
- [ ] **Step 10: Open diff modal immediately after import success**
In `importStocktake`, after setting `activePanel.value = "diff";`, add:
```js
diffDialogOpen.value = true;
activePanel.value = "import";
```
This keeps the flow block on the import step while the modal shows differences.
- [ ] **Step 11: Replace template overview/export/import/history sections**
Replace the current overview/export/import blocks with this two-block flow:
```vue
点击后系统会锁定当前仓库,并导出本次盘库 Excel 清单。盘库结束前该仓库不能出入库。 只允许导入当前仓库、本次盘库单导出的清单。导入成功后会立刻弹出差异对比。
盘库差异对比