# Warehouse Transaction Ledger 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:** Add a warehouse-level “流水” entry in 百华仓库 so each of the six warehouses can view paginated, searchable, sortable inbound/outbound inventory transactions. **Architecture:** Keep `wh_inventory_txn` as the single source of truth. Add a new backend pagination endpoint for warehouse-level transaction ledger queries, then add a right-side drawer in `InventoryLedgerView.vue` that follows the active warehouse tab and displays summary cards, filters, and transaction rows. Keep the existing material-level stock detail drawer unchanged. **Tech Stack:** FastAPI, SQLAlchemy ORM, Pydantic schemas, SQLite backend unit tests, Vue 3 SFC, Vite, existing `FormDrawer`, `PaginationBar`, `TableControls`, and notification conventions. --- ## Confirmed Requirements - Add a “流水” icon/text button in 百华仓库, positioned on the right side of the existing inbound/outbound operation bar. - The button follows the active warehouse tab: 原材料库、半成品库、成品库、辅料库、废料库、退货库. - Clicking opens a right-side drawer titled `{当前仓库名} · 出入库流水`. - The drawer shows summary cards, filters, and a paginated table. - Data comes from `wh_inventory_txn`; do not create a second ledger table. - Warehouse-level ledger does not replace the existing material-level stock detail drawer. - 原材料库、辅料库、废料库 use weight-first display and must not revive raw material quantity concepts. ## File Structure - Create `backend/tests/test_warehouse_transaction_ledger.py` - Owns backend regression coverage for warehouse filtering, direction, keyword, pagination, summary, and raw-material quantity display. - Modify `backend/app/schemas/operations.py` - Add response schemas for ledger rows, summary, and paginated response. - Modify `backend/app/services/operations.py` - Add reusable ledger query helpers and summary calculation. - Modify `backend/app/api/routes/inventory.py` - Add `GET /inventory/transaction-ledger`. - Create `frontend/scripts/test-inventory-ledger-transaction-ledger.mjs` - Static checks for the new drawer, endpoint, and preservation of existing material-level flow. - Modify `frontend/src/views/InventoryLedgerView.vue` - Add the “流水” action button, drawer, filters, server-side loading state, and table rendering. --- ### Task 1: Add Backend Failing Tests For Warehouse Transaction Ledger **Files:** - Create: `backend/tests/test_warehouse_transaction_ledger.py` - [ ] **Step 1: Create the backend test file scaffold** Create `backend/tests/test_warehouse_transaction_ledger.py` with this content: ```python from __future__ import annotations import unittest from datetime import UTC, datetime, timedelta from decimal import Decimal from sqlalchemy import BigInteger, create_engine from sqlalchemy.ext.compiler import compiles from sqlalchemy.orm import Session, sessionmaker @compiles(BigInteger, "sqlite") def _compile_bigint_for_sqlite(type_, compiler, **kw) -> str: _ = type_, compiler, kw return "INTEGER" import app.models.master_data # noqa: E402,F401 import app.models.operations # noqa: E402,F401 import app.models.org # noqa: E402,F401 from app.models.base import Base # noqa: E402 class WarehouseTransactionLedgerTest(unittest.TestCase): def setUp(self) -> None: engine = create_engine("sqlite+pysqlite:///:memory:", future=True) Base.metadata.create_all(engine) self.SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True) self.db: Session = self.SessionLocal() def tearDown(self) -> None: self.db.close() def seed_ledger_data(self) -> None: from app.models.master_data import Item, Warehouse from app.models.operations import InventoryTxn, StockLot, WarehouseLocation from app.models.org import Department, Employee, User now = datetime(2026, 6, 1, 9, 0, tzinfo=UTC) department = Department( id=1, dept_code="D001", dept_name="仓储部", dept_type="WAREHOUSE", status="ACTIVE", created_at=now, updated_at=now, ) employee = Employee( id=1, employee_code="EMP001", employee_name="张仓管", dept_id=1, status="ACTIVE", created_at=now, updated_at=now, ) user = User( id=1, username="keeper", password_hash="x", employee_id=1, dept_id=1, nickname="张仓管", status="ACTIVE", created_at=now, updated_at=now, ) raw_wh = Warehouse( id=1, warehouse_code="WH-RAW-01", warehouse_name="原材料库", warehouse_type="RAW", status="ACTIVE", created_at=now, updated_at=now, ) finished_wh = Warehouse( id=2, warehouse_code="WH-FG-01", warehouse_name="成品库", warehouse_type="FINISHED", status="ACTIVE", created_at=now, updated_at=now, ) raw_location = WarehouseLocation( id=1, warehouse_id=1, location_code="RAW-A", location_name="原料主库位", is_default=1, is_locked=0, status="ACTIVE", created_at=now, updated_at=now, ) finished_location = WarehouseLocation( id=2, warehouse_id=2, location_code="FG-A", location_name="成品主库位", is_default=1, is_locked=0, status="ACTIVE", created_at=now, updated_at=now, ) raw_item = Item( id=1, item_code="原材料00001", item_name="304不锈钢", item_type="RAW_MATERIAL", unit_weight_kg=Decimal("1"), status="ACTIVE", created_at=now, updated_at=now, ) finished_item = Item( id=2, item_code="产品需规00001", item_name="冲压支架", item_type="FINISHED_GOOD", unit_weight_kg=Decimal("2.5"), status="ACTIVE", created_at=now, updated_at=now, ) raw_lot = StockLot( id=1, lot_no="JH_原材料00001_0001", lot_role="INBOUND_RAW", item_id=1, warehouse_id=1, location_id=1, source_doc_type="OPENING_STOCK", source_doc_id=1, inbound_qty=0, inbound_weight_kg=1000, remaining_qty=0, remaining_weight_kg=850, locked_qty=0, locked_weight_kg=0, unit_cost=Decimal("10"), quality_status="PASS", status="AVAILABLE", created_at=now, updated_at=now, ) finished_lot = StockLot( id=2, lot_no="FG-20260601-001", lot_role="INBOUND_FINISHED", item_id=2, warehouse_id=2, location_id=2, source_doc_type="FG_RECEIPT", source_doc_id=9, inbound_qty=100, inbound_weight_kg=250, remaining_qty=80, remaining_weight_kg=200, locked_qty=0, locked_weight_kg=0, unit_cost=Decimal("18"), quality_status="PASS", status="AVAILABLE", created_at=now, updated_at=now, ) transactions = [ InventoryTxn( id=1, txn_no="TXN-RAW-IN", txn_type="OPENING_IN", item_id=1, warehouse_id=1, location_id=1, lot_id=1, qty_change=0, weight_change_kg=1000, unit_cost=10, amount=10000, source_doc_type="OPENING_STOCK", source_doc_id=1, biz_time=now, operator_user_id=1, remark="期初入库 304", created_at=now, updated_at=now, ), InventoryTxn( id=2, txn_no="TXN-RAW-OUT", txn_type="MATERIAL_ISSUE", item_id=1, warehouse_id=1, location_id=1, lot_id=1, qty_change=0, weight_change_kg=-150, unit_cost=10, amount=1500, source_doc_type="WORK_ORDER", source_doc_id=2, biz_time=now + timedelta(hours=1), operator_user_id=1, remark="生产出库", created_at=now, updated_at=now, ), InventoryTxn( id=3, txn_no="TXN-FG-IN", txn_type="FG_IN", item_id=2, warehouse_id=2, location_id=2, lot_id=2, qty_change=100, weight_change_kg=250, unit_cost=18, amount=1800, source_doc_type="FG_RECEIPT", source_doc_id=9, biz_time=now + timedelta(hours=2), operator_user_id=1, remark="成品入库", created_at=now, updated_at=now, ), ] self.db.add_all([department, employee, user, raw_wh, finished_wh, raw_location, finished_location, raw_item, finished_item, raw_lot, finished_lot, *transactions]) self.db.commit() ``` - [ ] **Step 2: Add the failing warehouse filter and summary test** Append this test to `WarehouseTransactionLedgerTest`: ```python def test_transaction_ledger_filters_by_warehouse_type_and_returns_summary(self) -> None: from app.api.routes.inventory import list_inventory_transaction_ledger self.seed_ledger_data() result = list_inventory_transaction_ledger( warehouse_type="RAW", page=1, page_size=10, db=self.db, ) self.assertEqual(result.total, 2) self.assertEqual([row.warehouse_type for row in result.items], ["RAW", "RAW"]) self.assertEqual(result.summary.in_count, 1) self.assertEqual(result.summary.out_count, 1) self.assertEqual(Decimal(str(result.summary.in_weight_kg)), Decimal("1000.0")) self.assertEqual(Decimal(str(result.summary.out_weight_kg)), Decimal("150.0")) self.assertEqual(result.items[0].txn_type, "MATERIAL_ISSUE") self.assertEqual(result.items[0].direction, "OUT") ``` - [ ] **Step 3: Add the failing keyword, direction, pagination, and raw quantity test** Append this test to `WarehouseTransactionLedgerTest`: ```python def test_transaction_ledger_supports_keyword_direction_pagination_and_raw_qty_zero(self) -> None: from app.api.routes.inventory import list_inventory_transaction_ledger self.seed_ledger_data() result = list_inventory_transaction_ledger( warehouse_type="RAW", keyword="304", direction="OUT", page=1, page_size=1, db=self.db, ) self.assertEqual(result.total, 1) self.assertEqual(len(result.items), 1) row = result.items[0] self.assertEqual(row.txn_no, "TXN-RAW-OUT") self.assertEqual(row.item_code, "原材料00001") self.assertEqual(row.lot_no, "JH_原材料00001_0001") self.assertEqual(row.qty_change, 0) self.assertEqual(row.operator_name, "张仓管") self.assertEqual(result.summary.out_count, 1) ``` - [ ] **Step 4: Add the failing invalid warehouse type test** Append this test to `WarehouseTransactionLedgerTest`: ```python def test_transaction_ledger_rejects_unknown_warehouse_type(self) -> None: from app.api.routes.inventory import list_inventory_transaction_ledger from fastapi import HTTPException self.seed_ledger_data() with self.assertRaises(HTTPException) as exc: list_inventory_transaction_ledger( warehouse_type="UNKNOWN", page=1, page_size=10, db=self.db, ) self.assertEqual(exc.exception.status_code, 400) self.assertIn("未知仓库类型", exc.exception.detail) ``` - [ ] **Step 5: Run tests and verify they fail for missing API** Run: ```bash cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend .venv/bin/python -m pytest tests/test_warehouse_transaction_ledger.py -q ``` Expected: FAIL with an import or attribute error for `list_inventory_transaction_ledger`. --- ### Task 2: Add Backend Schemas, Query Helper, And API Route **Files:** - Modify: `backend/app/schemas/operations.py` - Modify: `backend/app/services/operations.py` - Modify: `backend/app/api/routes/inventory.py` - Test: `backend/tests/test_warehouse_transaction_ledger.py` - [ ] **Step 1: Add response schemas** In `backend/app/schemas/operations.py`, after `InventoryTxnRead`, add: ```python class InventoryTxnLedgerRowRead(InventoryTxnRead): direction: str source_line_id: int | None = None operator_user_id: int | None = None operator_name: str | None = None class InventoryTxnLedgerSummaryRead(BaseModel): in_count: int = 0 out_count: int = 0 adjust_count: int = 0 in_qty: float = 0 out_qty: float = 0 in_weight_kg: float = 0 out_weight_kg: float = 0 latest_biz_time: datetime | None = None class InventoryTxnLedgerResponse(BaseModel): items: list[InventoryTxnLedgerRowRead] total: int summary: InventoryTxnLedgerSummaryRead ``` - [ ] **Step 2: Import the schemas in inventory route** In `backend/app/api/routes/inventory.py`, extend the schema import block: ```python from app.schemas.operations import ( ... InventoryTxnLedgerResponse, ) ``` Keep existing imports and add only the new name. - [ ] **Step 3: Add service constants and helpers** In `backend/app/services/operations.py`, near `get_inventory_txn_query`, add: ```python WAREHOUSE_LEDGER_TYPES = {"RAW", "SEMI", "FINISHED", "AUX", "SCRAP", "RETURN"} WAREHOUSE_LEDGER_SORT_COLUMNS = { "biz_time": InventoryTxn.biz_time, "txn_type": InventoryTxn.txn_type, "item_code": Item.item_code, "item_name": Item.item_name, "lot_no": StockLot.lot_no, "qty_change": InventoryTxn.qty_change, "weight_change_kg": InventoryTxn.weight_change_kg, "amount": InventoryTxn.amount, } def inventory_txn_direction_expression(): return case( (or_(InventoryTxn.txn_type.like("STOCKTAKE_%"), InventoryTxn.source_doc_type == "STOCKTAKE"), "ADJUST"), (or_(InventoryTxn.weight_change_kg > 0, InventoryTxn.qty_change > 0), "IN"), (or_(InventoryTxn.weight_change_kg < 0, InventoryTxn.qty_change < 0), "OUT"), else_="ADJUST", ) ``` - [ ] **Step 4: Add the ledger row query helper** In `backend/app/services/operations.py`, below the helper above, add: ```python def get_inventory_txn_ledger_query( *, warehouse_type: str, warehouse_id: int | None = None, keyword: str | None = None, direction: str = "ALL", txn_type: str | None = None, item_id: int | None = None, lot_no: str | None = None, start_date: date | None = None, end_date: date | None = None, sort_key: str = "biz_time", sort_direction: str = "desc", ) -> Select: normalized_warehouse_type = str(warehouse_type or "").upper() direction_expr = inventory_txn_direction_expression().label("direction") operator_employee = aliased(Employee) stmt = ( select( InventoryTxn.id.label("inventory_txn_id"), InventoryTxn.txn_no.label("txn_no"), InventoryTxn.txn_type.label("txn_type"), InventoryTxn.item_id.label("item_id"), Item.item_code.label("item_code"), Item.item_name.label("item_name"), InventoryTxn.warehouse_id.label("warehouse_id"), Warehouse.warehouse_name.label("warehouse_name"), Warehouse.warehouse_type.label("warehouse_type"), InventoryTxn.location_id.label("location_id"), WarehouseLocation.location_name.label("location_name"), InventoryTxn.lot_id.label("lot_id"), StockLot.lot_no.label("lot_no"), case((Item.item_type == "RAW_MATERIAL", literal(0)), else_=InventoryTxn.qty_change).label("qty_change"), InventoryTxn.weight_change_kg.label("weight_change_kg"), InventoryTxn.unit_cost.label("unit_cost"), InventoryTxn.amount.label("amount"), InventoryTxn.source_doc_type.label("source_doc_type"), InventoryTxn.source_doc_id.label("source_doc_id"), InventoryTxn.source_line_id.label("source_line_id"), InventoryTxn.biz_time.label("biz_time"), InventoryTxn.operator_user_id.label("operator_user_id"), func.coalesce(operator_employee.employee_name, User.nickname, User.username).label("operator_name"), InventoryTxn.logistics_waybill_no.label("logistics_waybill_no"), InventoryTxn.logistics_freight_amount.label("logistics_freight_amount"), InventoryTxn.logistics_photo_url.label("logistics_photo_url"), InventoryTxn.remark.label("remark"), direction_expr, ) .join(Item, Item.id == InventoryTxn.item_id) .join(Warehouse, Warehouse.id == InventoryTxn.warehouse_id) .outerjoin(WarehouseLocation, WarehouseLocation.id == InventoryTxn.location_id) .outerjoin(StockLot, StockLot.id == InventoryTxn.lot_id) .outerjoin(User, User.id == InventoryTxn.operator_user_id) .outerjoin(operator_employee, operator_employee.id == User.employee_id) .where(Warehouse.warehouse_type == normalized_warehouse_type) ) if warehouse_id: stmt = stmt.where(InventoryTxn.warehouse_id == warehouse_id) if txn_type: stmt = stmt.where(InventoryTxn.txn_type == str(txn_type).upper()) if item_id: stmt = stmt.where(InventoryTxn.item_id == item_id) if lot_no: stmt = stmt.where(StockLot.lot_no.like(f"%{lot_no.strip()}%")) if start_date: stmt = stmt.where(InventoryTxn.biz_time >= start_of_day(start_date)) if end_date: stmt = stmt.where(InventoryTxn.biz_time <= end_of_day(end_date)) if keyword and keyword.strip(): pattern = f"%{keyword.strip()}%" stmt = stmt.where( or_( Item.item_code.like(pattern), Item.item_name.like(pattern), InventoryTxn.txn_type.like(pattern), InventoryTxn.source_doc_type.like(pattern), InventoryTxn.txn_no.like(pattern), InventoryTxn.logistics_waybill_no.like(pattern), InventoryTxn.remark.like(pattern), StockLot.lot_no.like(pattern), ) ) normalized_direction = str(direction or "ALL").upper() if normalized_direction in {"IN", "OUT", "ADJUST"}: stmt = stmt.where(direction_expr == normalized_direction) sort_column = WAREHOUSE_LEDGER_SORT_COLUMNS.get(sort_key, InventoryTxn.biz_time) if str(sort_direction or "desc").lower() == "asc": stmt = stmt.order_by(sort_column.asc(), InventoryTxn.id.asc()) else: stmt = stmt.order_by(sort_column.desc(), InventoryTxn.id.desc()) return stmt ``` - [ ] **Step 5: Add the summary helper** In `backend/app/services/operations.py`, below `get_inventory_txn_ledger_query`, add: ```python def summarize_inventory_txn_ledger(db: Session, ledger_stmt: Select) -> dict[str, object]: rows = db.execute(ledger_stmt).mappings().all() summary = { "in_count": 0, "out_count": 0, "adjust_count": 0, "in_qty": Decimal("0"), "out_qty": Decimal("0"), "in_weight_kg": Decimal("0"), "out_weight_kg": Decimal("0"), "latest_biz_time": None, } for row in rows: direction = str(row["direction"] or "ADJUST").upper() qty = abs(to_decimal(row["qty_change"])) weight = abs(to_decimal(row["weight_change_kg"])) if direction == "IN": summary["in_count"] += 1 summary["in_qty"] += qty summary["in_weight_kg"] += weight elif direction == "OUT": summary["out_count"] += 1 summary["out_qty"] += qty summary["out_weight_kg"] += weight else: summary["adjust_count"] += 1 biz_time = row["biz_time"] if biz_time and (summary["latest_biz_time"] is None or biz_time > summary["latest_biz_time"]): summary["latest_biz_time"] = biz_time return summary ``` - [ ] **Step 6: Import helpers into inventory route** In `backend/app/api/routes/inventory.py`, add these service imports to the existing import list from `app.services.operations`: ```python WAREHOUSE_LEDGER_TYPES, get_inventory_txn_ledger_query, summarize_inventory_txn_ledger, ``` - [ ] **Step 7: Add the API endpoint** In `backend/app/api/routes/inventory.py`, after `list_inventory_transactions`, add: ```python @router.get("/transaction-ledger", response_model=InventoryTxnLedgerResponse) def list_inventory_transaction_ledger( warehouse_type: str = Query(...), warehouse_id: int | None = Query(default=None, ge=1), keyword: str | None = Query(default=None), direction: str = Query(default="ALL"), txn_type: str | None = Query(default=None), item_id: int | None = Query(default=None, ge=1), lot_no: str | None = Query(default=None), start_date: date | None = Query(default=None), end_date: date | None = Query(default=None), page: int = Query(default=1, ge=1), page_size: int = Query(default=10, ge=1, le=100), sort_key: str = Query(default="biz_time"), sort_direction: str = Query(default="desc"), db: Session = Depends(get_db), ) -> InventoryTxnLedgerResponse: normalized_warehouse_type = str(warehouse_type or "").upper() if normalized_warehouse_type not in WAREHOUSE_LEDGER_TYPES: raise HTTPException(status_code=400, detail="未知仓库类型,无法查询出入库流水") normalized_direction = str(direction or "ALL").upper() if normalized_direction not in {"ALL", "IN", "OUT", "ADJUST"}: raise HTTPException(status_code=400, detail="流水方向只能是 ALL、IN、OUT、ADJUST") stmt = get_inventory_txn_ledger_query( warehouse_type=normalized_warehouse_type, warehouse_id=warehouse_id, keyword=keyword, direction=normalized_direction, txn_type=txn_type, item_id=item_id, lot_no=lot_no, start_date=start_date, end_date=end_date, sort_key=sort_key, sort_direction=sort_direction, ) total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0 summary = summarize_inventory_txn_ledger(db, stmt) offset = (page - 1) * page_size rows = db.execute(stmt.offset(offset).limit(page_size)).mappings().all() return InventoryTxnLedgerResponse( items=[dict(row) for row in rows], total=total, summary=summary, ) ``` - [ ] **Step 8: Run backend tests** Run: ```bash cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend .venv/bin/python -m pytest tests/test_warehouse_transaction_ledger.py -q ``` Expected: PASS. - [ ] **Step 9: Run related backend regression tests** Run: ```bash cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend .venv/bin/python -m pytest tests/test_return_warehouse_flow.py tests/test_opening_inventory_import.py tests/test_scrap_warehouse_flow.py::ScrapWarehouseFlowTest::test_scrap_stocktake_keeps_product_scrap_weight_only_after_confirm -q ``` Expected: PASS. - [ ] **Step 10: Compile changed backend files** Run: ```bash cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend .venv/bin/python -m py_compile app/api/routes/inventory.py app/services/operations.py app/schemas/operations.py tests/test_warehouse_transaction_ledger.py ``` Expected: exit code 0. --- ### Task 3: Add Frontend Static Checks For Warehouse-Level Ledger **Files:** - Create: `frontend/scripts/test-inventory-ledger-transaction-ledger.mjs` - [ ] **Step 1: Create the failing static test script** Create `frontend/scripts/test-inventory-ledger-transaction-ledger.mjs`: ```javascript import fs from "node:fs"; import path from "node:path"; const root = process.cwd(); const viewPath = path.join(root, "src/views/InventoryLedgerView.vue"); const view = fs.readFileSync(viewPath, "utf8"); const checks = [ ["ledger button exists", view.includes("openWarehouseLedgerDrawer") && view.includes("查看当前仓库出入库流水")], ["ledger drawer exists", view.includes("warehouseLedgerDrawerOpen") && view.includes("出入库流水")], ["ledger endpoint used", view.includes("/inventory/transaction-ledger")], ["warehouse tab drives ledger", view.includes("activeWarehouseType") && view.includes("activeInventoryLabel")], ["server pagination state exists", view.includes("warehouseLedgerPage") && view.includes("warehouseLedgerPageSize") && view.includes("warehouseLedgerTotal")], ["existing material stock detail remains", view.includes("openStockDetail(balance)") && view.includes("selectedTransactions")], ["six warehouse tabs still present", ["raw", "semi", "finished", "auxiliary", "scrap", "return"].every((key) => view.includes(`key: "${key}"`))] ]; const failed = checks.filter(([, passed]) => !passed); if (failed.length) { console.error("Warehouse transaction ledger UI checks failed:"); failed.forEach(([name]) => console.error(`- ${name}`)); process.exit(1); } console.log("Warehouse transaction ledger UI checks passed"); ``` - [ ] **Step 2: Run static test and verify it fails** Run: ```bash cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend node scripts/test-inventory-ledger-transaction-ledger.mjs ``` Expected: FAIL and list missing ledger UI checks. --- ### Task 4: Add Warehouse Ledger Drawer UI In InventoryLedgerView **Files:** - Modify: `frontend/src/views/InventoryLedgerView.vue` - Test: `frontend/scripts/test-inventory-ledger-transaction-ledger.mjs` - [ ] **Step 1: Add the ledger button to the operation bar** In `frontend/src/views/InventoryLedgerView.vue`, inside `
`, after the two existing `.inventory-io-row` blocks, add: ```vue ``` - [ ] **Step 2: Add the ledger drawer template** In the same Vue file, before `
入库笔数 {{ warehouseLedgerSummary.in_count || 0 }}
出库笔数 {{ warehouseLedgerSummary.out_count || 0 }}
入库重量 {{ formatWeight(warehouseLedgerSummary.in_weight_kg || 0) }}
出库重量 {{ formatWeight(warehouseLedgerSummary.out_weight_kg || 0) }}
最近流水 {{ warehouseLedgerSummary.latest_biz_time ? formatDateTime(warehouseLedgerSummary.latest_biz_time) : "-" }}
时间 方向 类型 物料 批次号 变动数量 变动重量(kg) 单价 金额 来源 经办人 运单/照片 备注
{{ formatDateTime(row.biz_time) }} {{ formatWarehouseLedgerDirection(row.direction) }} {{ formatInventoryTxnTypeLabel(row.txn_type) }} {{ row.item_code }} · {{ row.item_name }} {{ row.lot_no || "-" }} {{ formatQty(row.qty_change) }} {{ formatWeight(row.weight_change_kg) }} ¥{{ formatAmount(row.unit_cost) }} ¥{{ formatAmount(row.amount) }} {{ formatSourceDocTypeLabel(row.source_doc_type) }} #{{ row.source_doc_id }} {{ row.operator_name || "-" }} {{ row.logistics_waybill_no || "-" }} {{ row.remark || "-" }}
{{ warehouseLedgerLoading ? "流水加载中..." : `暂无${activeInventoryLabel}出入库流水` }}
``` - [ ] **Step 3: Add reactive state** In `