29 KiB
Production Inbound Archive Entrypoints 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: Expose preview/download/regenerate entrypoints for 生产入库结算单 in the places users naturally look after production ledger inbound creates multiple warehouse transactions.
Architecture: Keep 生产台账入库 as one business settlement action and keep all generated warehouse transactions linked to the same 生产入库结算单 archive. The backend warehouse transaction ledger will return archive metadata for production settlement transactions; the frontend will reuse DocumentArchiveActions in the warehouse ledger and show a post-save shortcut when 生产台账入库 succeeds.
Tech Stack: FastAPI, SQLAlchemy, Pydantic, existing document_archives service/routes, Vue 3, existing DocumentArchiveActions, existing openResource/downloadResource/postResource.
File Structure
Backend
- Modify:
backend/app/schemas/operations.py- Add archive metadata fields to
InventoryTxnRead, inherited byInventoryTxnLedgerRowRead.
- Add archive metadata fields to
- Modify:
backend/app/services/operations.py- Add
DocumentArchiveand production archive constants imports if missing. - Add helper functions for production settlement archive grouping on warehouse inventory transactions.
- Extend
get_inventory_txn_ledger_queryto select archive metadata for production settlement rows.
- Add
- Test:
backend/tests/test_inventory_transaction_ledger_archive_fields.py- Add a focused test that proves finished, surplus, and scrap rows from the same production settlement batch all expose the same archive business ID and status.
Frontend
- Modify:
frontend/src/views/InventoryLedgerView.vue- Import
DocumentArchiveActions. - Import
downloadResourceandopenResourceif not already imported. - Add a
单据留档column to the warehouse流水 drawer. - Render
DocumentArchiveActionsfor rows witharchive_business_id. - Add archive preview/download/regenerate handlers.
- Add a post-save shortcut panel after
生产台账入库succeeds.
- Import
- Modify:
frontend/src/views/InventoryLedgerView.test.js- Add static assertions for the new warehouse ledger archive column, document type key, and post-save shortcut state.
- Modify:
frontend/src/styles/main.css- Add compact archive action styling for table cells and feedback shortcut panels.
Current Reality To Preserve
生产台账入库already callsPOST /production/production-ledger-inbounds.- That endpoint already returns:
archive_statusarchive_business_idarchive_document_typearchive_error_message
ProductionLedgerView.vuealready usesDocumentArchiveActionsforproduction-material-outandproduction-inbound-settlement.InventoryLedgerView.vuewarehouse ledger currently has no archive column.GET /inventory/transaction-ledgercurrently returnsInventoryTxnLedgerRowRead, which does not yet include archive metadata.
Task 1: Backend Test For Warehouse Ledger Archive Metadata
Files:
-
Create:
backend/tests/test_inventory_transaction_ledger_archive_fields.py -
Step 1: Write the failing test
Create backend/tests/test_inventory_transaction_ledger_archive_fields.py:
from __future__ import annotations
import unittest
from datetime import datetime
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_big_integer_for_sqlite(type_, compiler, **kw) -> str:
_ = type_, compiler, kw
return "INTEGER"
import app.models.document_archive # noqa: E402,F401
import app.models.master_data # noqa: E402,F401
import app.models.miniapp # noqa: E402,F401
import app.models.operations # noqa: E402,F401
import app.models.org # noqa: E402,F401
import app.models.planning # noqa: E402,F401
import app.models.sales # noqa: E402,F401
from app.models.base import Base # noqa: E402
from app.models.document_archive import DocumentArchive # noqa: E402
from app.models.master_data import Item, Warehouse # noqa: E402
from app.models.operations import InventoryTxn, StockLot # noqa: E402
from app.services.document_archives import DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT # noqa: E402
from app.services.operations import get_inventory_txn_ledger_query # noqa: E402
class InventoryTransactionLedgerArchiveFieldsTest(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()
self.now = datetime(2026, 6, 11, 14, 0, 0)
self._seed_data()
def tearDown(self) -> None:
self.db.close()
def _seed_data(self) -> None:
finished_wh = Warehouse(
id=1,
warehouse_code="WH-FIN",
warehouse_name="成品库",
warehouse_type="FINISHED",
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
raw_wh = Warehouse(
id=2,
warehouse_code="WH-RAW",
warehouse_name="原材料库",
warehouse_type="RAW",
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
scrap_wh = Warehouse(
id=3,
warehouse_code="WH-SCRAP",
warehouse_name="废料库",
warehouse_type="SCRAP",
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
material = Item(
id=10,
item_code="RM-010",
item_name="测试原材料",
item_type="RAW_MATERIAL",
unit_weight_kg=Decimal("0"),
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
product = Item(
id=20,
item_code="FG-020",
item_name="测试成品",
item_type="FINISHED",
unit_weight_kg=Decimal("0.5"),
status="ACTIVE",
created_at=self.now,
updated_at=self.now,
)
source_lot = StockLot(
id=100,
lot_no="YL0001",
item_id=material.id,
warehouse_id=raw_wh.id,
location_id=None,
inbound_qty=0,
inbound_weight_kg=500,
remaining_qty=0,
remaining_weight_kg=200,
locked_qty=0,
locked_weight_kg=0,
unit_cost=Decimal("6.5"),
quality_status="PASS",
status="AVAILABLE",
created_at=self.now,
updated_at=self.now,
)
finished_lot = StockLot(
id=101,
lot_no="FGL0001",
parent_lot_id=source_lot.id,
item_id=product.id,
warehouse_id=finished_wh.id,
location_id=None,
source_material_lot_id=source_lot.id,
source_material_sub_batch_no="YL0001",
inbound_qty=100,
inbound_weight_kg=50,
remaining_qty=100,
remaining_weight_kg=50,
locked_qty=0,
locked_weight_kg=0,
unit_cost=Decimal("3.25"),
quality_status="PASS",
status="AVAILABLE",
created_at=self.now,
updated_at=self.now,
)
scrap_lot = StockLot(
id=102,
lot_no="SCRAPI0001",
parent_lot_id=source_lot.id,
item_id=material.id,
warehouse_id=scrap_wh.id,
location_id=None,
source_material_lot_id=source_lot.id,
source_material_sub_batch_no="YL0001",
inbound_qty=0,
inbound_weight_kg=5,
remaining_qty=0,
remaining_weight_kg=5,
locked_qty=0,
locked_weight_kg=0,
unit_cost=Decimal("6.5"),
quality_status="SCRAP",
status="AVAILABLE",
created_at=self.now,
updated_at=self.now,
)
batch_no = "生产结算20260611140000000001"
self.finished_txn = InventoryTxn(
id=1000,
txn_no="TXN-FIN-001",
txn_type="FG_IN",
item_id=product.id,
warehouse_id=finished_wh.id,
location_id=None,
lot_id=finished_lot.id,
qty_change=Decimal("100"),
weight_change_kg=Decimal("50"),
unit_cost=Decimal("3.25"),
amount=Decimal("325"),
source_doc_type="生产台账",
source_doc_id=2000,
source_line_id=finished_lot.id,
biz_time=self.now,
remark=f"生产台账入库;成品入库;单据批次号:{batch_no}",
created_at=self.now,
updated_at=self.now,
)
self.surplus_txn = InventoryTxn(
id=1001,
txn_no="TXN-SURPLUS-001",
txn_type="PRODUCTION_SURPLUS_IN",
item_id=material.id,
warehouse_id=raw_wh.id,
location_id=None,
lot_id=source_lot.id,
qty_change=Decimal("0"),
weight_change_kg=Decimal("20"),
unit_cost=Decimal("6.5"),
amount=Decimal("130"),
source_doc_type="生产台账",
source_doc_id=2000,
source_line_id=source_lot.id,
biz_time=self.now,
remark=f"生产台账入库;生产余料入库;单据批次号:{batch_no}",
created_at=self.now,
updated_at=self.now,
)
self.scrap_txn = InventoryTxn(
id=1002,
txn_no="TXN-SCRAP-001",
txn_type="PRODUCTION_SCRAP_IN",
item_id=material.id,
warehouse_id=scrap_wh.id,
location_id=None,
lot_id=scrap_lot.id,
qty_change=Decimal("0"),
weight_change_kg=Decimal("5"),
unit_cost=Decimal("6.5"),
amount=Decimal("32.5"),
source_doc_type="生产台账",
source_doc_id=2000,
source_line_id=scrap_lot.id,
biz_time=self.now,
remark=f"生产台账入库;生产废料入库;单据批次号:{batch_no}",
created_at=self.now,
updated_at=self.now,
)
archive = DocumentArchive(
document_type=DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT,
business_id=self.finished_txn.id,
document_no="生产入库结算-TXN-FIN-001",
archive_version=1,
file_format="PDF",
file_name="生产入库结算-TXN-FIN-001_V1.pdf",
file_path="/tmp/生产入库结算-TXN-FIN-001_V1.pdf",
status="已归档",
error_message=None,
created_by=None,
created_at=self.now,
)
self.db.add_all([
finished_wh,
raw_wh,
scrap_wh,
material,
product,
source_lot,
finished_lot,
scrap_lot,
self.finished_txn,
self.surplus_txn,
self.scrap_txn,
archive,
])
self.db.commit()
def test_finished_warehouse_row_exposes_own_settlement_archive(self) -> None:
rows = self.db.execute(get_inventory_txn_ledger_query(warehouse_type="FINISHED")).mappings().all()
self.assertEqual(len(rows), 1)
row = dict(rows[0])
self.assertEqual(row["inventory_txn_id"], self.finished_txn.id)
self.assertEqual(row["archive_business_id"], self.finished_txn.id)
self.assertEqual(row["archive_document_type"], DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT)
self.assertEqual(row["archive_status"], "已归档")
def test_surplus_warehouse_row_exposes_same_settlement_archive(self) -> None:
rows = self.db.execute(get_inventory_txn_ledger_query(warehouse_type="RAW")).mappings().all()
self.assertEqual(len(rows), 1)
row = dict(rows[0])
self.assertEqual(row["inventory_txn_id"], self.surplus_txn.id)
self.assertEqual(row["archive_business_id"], self.finished_txn.id)
self.assertEqual(row["archive_document_type"], DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT)
self.assertEqual(row["archive_status"], "已归档")
def test_scrap_warehouse_row_exposes_same_settlement_archive(self) -> None:
rows = self.db.execute(get_inventory_txn_ledger_query(warehouse_type="SCRAP")).mappings().all()
self.assertEqual(len(rows), 1)
row = dict(rows[0])
self.assertEqual(row["inventory_txn_id"], self.scrap_txn.id)
self.assertEqual(row["archive_business_id"], self.finished_txn.id)
self.assertEqual(row["archive_document_type"], DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT)
self.assertEqual(row["archive_status"], "已归档")
if __name__ == "__main__":
unittest.main()
- Step 2: Run the test and verify it fails
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
python -m unittest backend.tests.test_inventory_transaction_ledger_archive_fields -v
Expected: fail with missing archive_business_id/archive_document_type/archive_status columns on the warehouse transaction ledger query.
Task 2: Add Archive Metadata To Warehouse Transaction Ledger
Files:
-
Modify:
backend/app/schemas/operations.py -
Modify:
backend/app/services/operations.py -
Step 1: Extend the response schema
In backend/app/schemas/operations.py, add these fields to InventoryTxnRead after remark:
archive_status: str | None = None
archive_business_id: int | None = None
archive_document_type: str | None = None
archive_error_message: str | None = None
- Step 2: Import production archive constants
In backend/app/services/operations.py, add this import near the other service imports:
from app.services.document_archives import DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT, production_document_batch_no_from_text
- Step 3: Add production settlement helper constants
In backend/app/services/operations.py, near WAREHOUSE_LEDGER_SORT_COLUMNS, add:
PRODUCTION_SETTLEMENT_INVENTORY_TXN_TYPES = {
"FG_IN",
"PRODUCTION_SURPLUS_IN",
"PRODUCTION_SCRAP_IN",
}
PRODUCTION_SETTLEMENT_MAIN_TXN_PRIORITY = {
"FG_IN": 1,
"PRODUCTION_SURPLUS_IN": 2,
"PRODUCTION_SCRAP_IN": 3,
}
- Step 4: Add a settlement main transaction subquery helper
In backend/app/services/operations.py, add this helper before get_inventory_txn_ledger_query:
def _production_settlement_archive_subquery():
settlement_txn = aliased(InventoryTxn)
settlement_batch_no = func.coalesce(
production_document_batch_no_from_text(settlement_txn.remark),
func.date_format(settlement_txn.biz_time, "%Y-%m-%d %H:%i:%s.%f"),
).label("settlement_group_key")
priority_expr = case(
*[
(settlement_txn.txn_type == txn_type, priority)
for txn_type, priority in PRODUCTION_SETTLEMENT_MAIN_TXN_PRIORITY.items()
],
else_=99,
)
grouped = (
select(
settlement_txn.source_doc_id.label("production_ledger_id"),
settlement_batch_no,
func.min(
case(
*[
(settlement_txn.txn_type == txn_type, priority)
for txn_type, priority in PRODUCTION_SETTLEMENT_MAIN_TXN_PRIORITY.items()
],
else_=99,
)
* 1_000_000_000
+ settlement_txn.id
).label("main_rank"),
)
.where(
settlement_txn.source_doc_type == "生产台账",
settlement_txn.source_doc_id.is_not(None),
settlement_txn.txn_type.in_(PRODUCTION_SETTLEMENT_INVENTORY_TXN_TYPES),
)
.group_by(settlement_txn.source_doc_id, settlement_batch_no)
.subquery()
)
return grouped
If MySQL-specific date_format breaks SQLite tests, replace the fallback group key with func.coalesce(production_document_batch_no_from_text(settlement_txn.remark), settlement_txn.biz_time). The seed data uses 单据批次号 in all remarks, so the SQLite tests still exercise the intended path.
- Step 5: Join archive metadata in
get_inventory_txn_ledger_query
In get_inventory_txn_ledger_query, add aliases before building stmt:
settlement_grouped = _production_settlement_archive_subquery()
settlement_main_txn = aliased(InventoryTxn)
settlement_archive = aliased(DocumentArchive)
current_batch_no = func.coalesce(
production_document_batch_no_from_text(InventoryTxn.remark),
func.date_format(InventoryTxn.biz_time, "%Y-%m-%d %H:%i:%s.%f"),
)
Add these selected columns after InventoryTxn.remark.label("remark"):
settlement_archive.status.label("archive_status"),
settlement_main_txn.id.label("archive_business_id"),
literal(DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT).label("archive_document_type"),
settlement_archive.error_message.label("archive_error_message"),
Add these joins after the operator join:
.outerjoin(
settlement_grouped,
and_(
InventoryTxn.source_doc_type == "生产台账",
InventoryTxn.source_doc_id == settlement_grouped.c.production_ledger_id,
current_batch_no == settlement_grouped.c.settlement_group_key,
InventoryTxn.txn_type.in_(PRODUCTION_SETTLEMENT_INVENTORY_TXN_TYPES),
),
)
.outerjoin(
settlement_main_txn,
settlement_main_txn.id == settlement_grouped.c.main_rank % 1_000_000_000,
)
.outerjoin(
settlement_archive,
and_(
settlement_archive.document_type == DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT,
settlement_archive.business_id == settlement_main_txn.id,
settlement_archive.file_format == "PDF",
settlement_archive.archive_version
== select(func.max(DocumentArchive.archive_version))
.where(
DocumentArchive.document_type == DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT,
DocumentArchive.business_id == settlement_main_txn.id,
DocumentArchive.file_format == "PDF",
)
.scalar_subquery(),
),
)
- Step 6: Run the backend test
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
python -m unittest backend.tests.test_inventory_transaction_ledger_archive_fields -v
Expected: all 3 tests pass.
Task 3: Frontend Static Tests For Warehouse Ledger Archive Entrypoints
Files:
-
Modify:
frontend/src/views/InventoryLedgerView.test.js -
Step 1: Add static assertions
Append this describe block to frontend/src/views/InventoryLedgerView.test.js:
describe("InventoryLedgerView document archive entrypoints", () => {
it("renders a warehouse ledger archive column using production settlement archive actions", () => {
assert.match(source, /<th>单据留档<\/th>/);
assert.match(source, /<DocumentArchiveActions/);
assert.match(source, /row\.archive_business_id/);
assert.match(source, /production-inbound-settlement/);
});
it("stores the latest production ledger inbound archive shortcut after saving", () => {
assert.match(source, /lastArchiveAction/);
assert.match(source, /buildProductionInboundArchiveAction/);
assert.match(source, /预览结算单/);
assert.match(source, /下载PDF/);
});
});
- Step 2: Run the static test and verify it fails
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node --test src/views/InventoryLedgerView.test.js
Expected: fail because archive entrypoint code has not been added yet.
Task 4: Add Warehouse Ledger Archive Actions And Post-Save Shortcut
Files:
-
Modify:
frontend/src/views/InventoryLedgerView.vue -
Modify:
frontend/src/styles/main.css -
Step 1: Import archive helpers
In frontend/src/views/InventoryLedgerView.vue, add the component import:
import DocumentArchiveActions from "../components/documentForms/DocumentArchiveActions.vue";
Update the API import to include download/open helpers:
import { downloadResource, fetchResource, openResource, postResource, requestJson, uploadResource } from "../services/api";
- Step 2: Add latest archive shortcut state
Near existing refs, add:
const lastArchiveAction = ref(null);
Add this helper near formatArchiveFeedback:
function buildProductionInboundArchiveAction(result) {
if (!result?.archive_business_id) {
return null;
}
return {
typeKey: "production-inbound-settlement",
businessId: Number(result.archive_business_id),
status: result.archive_status || "未生成",
label: "生产入库结算单",
filename: `${result.material_lot_no || "生产入库结算单"}归档.pdf`
};
}
Clear stale shortcut in resetFeedback:
function resetFeedback() {
feedbackMessage.value = "";
errorMessage.value = "";
lastArchiveAction.value = null;
}
- Step 3: Render post-save shortcut near feedback banners
In the main page area, after the existing feedbackMessage banner if present, render:
<div v-if="lastArchiveAction" class="document-archive-feedback-panel">
<span>{{ lastArchiveAction.label }}</span>
<button class="document-archive-button document-archive-button-preview" type="button" @click="previewDocumentArchive(lastArchiveAction.typeKey, lastArchiveAction.businessId)">预览结算单</button>
<button class="document-archive-button document-archive-button-download" type="button" @click="downloadDocumentArchive(lastArchiveAction.typeKey, lastArchiveAction.businessId, lastArchiveAction.filename)">下载PDF</button>
</div>
Also render the same panel inside the productionWorkOrderInboundDrawerOpen form under its feedbackMessage banner, so the shortcut works if a future change keeps the drawer open after save.
- Step 4: Add
单据留档column to warehouse流水
In the warehouse ledger drawer table header, add a column before 备注:
<th>单据留档</th>
In the row body, add the matching cell before 备注:
<td class="warehouse-ledger-archive-cell">
<DocumentArchiveActions
v-if="row.archive_business_id"
class="document-archive-actions-compact"
:status="row.archive_status"
@preview="previewDocumentArchive('production-inbound-settlement', row.archive_business_id)"
@download="downloadDocumentArchive('production-inbound-settlement', row.archive_business_id, buildWarehouseLedgerArchiveFilename(row))"
@regenerate="regenerateDocumentArchive('production-inbound-settlement', row.archive_business_id)"
/>
<span v-else class="muted-cell">-</span>
</td>
Update the empty-row colspan expression by adding 1:
:colspan="activeWeightOnly || activeQuantityOnly ? 13 : 14"
- Step 5: Add archive action handlers
Add these functions near openLogisticsPhoto:
function buildWarehouseLedgerArchiveFilename(row) {
const lotNo = row.source_material_lot_no || row.lot_no || activeInventoryLabel.value || "生产入库结算单";
const typeLabel = formatInventoryTxnTypeLabel(row.txn_type) || "生产入库结算单";
return `${lotNo}-${typeLabel}归档.pdf`;
}
async function previewDocumentArchive(typeKey, businessId) {
resetFeedback();
if (!businessId) {
errorMessage.value = "缺少归档业务ID,无法预览";
return;
}
try {
await openResource(`/document-archives/${typeKey}/${businessId}/latest/preview`);
} catch (error) {
errorMessage.value = error.message || "PDF归档预览失败";
}
}
async function downloadDocumentArchive(typeKey, businessId, fallbackFilename) {
resetFeedback();
if (!businessId) {
errorMessage.value = "缺少归档业务ID,无法下载";
return;
}
try {
await downloadResource(`/document-archives/${typeKey}/${businessId}/latest/download`, fallbackFilename);
feedbackMessage.value = "PDF归档已下载";
} catch (error) {
errorMessage.value = error.message || "PDF归档下载失败";
}
}
async function regenerateDocumentArchive(typeKey, businessId) {
resetFeedback();
if (!businessId) {
errorMessage.value = "缺少归档业务ID,无法重新生成";
return;
}
try {
const result = await postResource(`/document-archives/${typeKey}/${businessId}/generate`, {});
feedbackMessage.value = result.message || "PDF归档已重新生成";
await loadWarehouseLedger();
await loadAll();
} catch (error) {
errorMessage.value = error.message || "PDF归档重新生成失败";
}
}
- Step 6: Store shortcut after
生产台账入库save
In submitProductionWorkOrderInbound, after the API result:
lastArchiveAction.value = buildProductionInboundArchiveAction(result);
feedbackMessage.value = `生产台账入库 ${result.material_lot_no || ""} 已保存${formatArchiveFeedback(result)}`;
Keep the drawer close behavior unchanged.
- Step 7: Add compact styles
Append to frontend/src/styles/main.css:
.warehouse-ledger-archive-cell {
min-width: 260px;
}
.document-archive-actions-compact {
align-items: stretch;
display: grid;
gap: 6px;
}
.document-archive-actions-compact .document-archive-status {
justify-content: center;
min-height: 26px;
}
.document-archive-actions-compact .document-archive-buttons {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 5px;
}
.document-archive-actions-compact .document-archive-button {
min-height: 28px;
padding: 0 7px;
font-size: 11px;
}
.document-archive-feedback-panel {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin: 10px 0 14px;
padding: 10px;
border: 1px solid rgba(31, 122, 74, 0.26);
border-radius: 14px;
background: rgba(232, 246, 232, 0.76);
}
.document-archive-feedback-panel > span {
color: #145c39;
font-weight: 900;
}
- Step 8: Run the frontend static test
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node --test src/views/InventoryLedgerView.test.js
Expected: pass.
Task 5: Verification
Files:
-
No code files.
-
Step 1: Run backend archive metadata test
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend
python -m unittest backend.tests.test_inventory_transaction_ledger_archive_fields -v
Expected: all tests pass.
- Step 2: Run frontend static tests
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node --test src/views/InventoryLedgerView.test.js src/views/ProductionLedgerView.test.js
Expected: all tests pass.
- Step 3: Build frontend
Run:
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build
Expected: build exits 0. The existing Vite chunk-size warning is acceptable.
- Step 4: Browser verify warehouse ledger
Use the in-app browser:
- Open
http://127.0.0.1:5173/inventory-ledger. - Click
工具 -> 流水. - Find a
生产台账related成品入库/生产余料入库/生产废料入库row. - Confirm the
单据留档column shows已归档plus预览PDFand下载PDF. - Click
预览PDF. - Confirm the opened PDF is
生产入库结算单and includes 成品、余料、废料明细 when the settlement action created multiple warehouse transactions.
- Step 5: Browser verify post-save shortcut
Use the in-app browser:
- Open
http://127.0.0.1:5173/inventory-ledger. - Click
工具 -> 生产台账入库. - Select an open production ledger with safe test values.
- Save.
- Confirm the success area shows
预览结算单and下载PDF. - Click
预览结算单. - Confirm it opens the same
生产入库结算单archive as the warehouse ledger row.
Self-Review
- Spec coverage: The plan covers the approved design: a single
生产入库结算单shared by the multiple warehouse流水 rows from one production ledger inbound action, warehouse ledger buttons, production ledger detail already existing, and post-save shortcut. - Placeholder scan: No
TBD,TODO, or vague “add appropriate handling” steps remain. - Type consistency: The plan consistently uses
archive_status,archive_business_id,archive_document_type,archive_error_message, and route keyproduction-inbound-settlement. - Scope control: This plan does not implement all warehouse branch PDFs. It only exposes production settlement archives already generated by the production main chain.