```
- [ ] **Step 7: Run frontend checks**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-production-batch-ledger-ui.mjs
node scripts/test-smart-operation-report-toggle.mjs
npm run build
```
Expected: PASS.
- [ ] **Step 8: Commit**
```bash
git add frontend/src/services/systemFeatures.js frontend/src/views/ProductionLedgerView.vue frontend/src/router/index.js frontend/src/App.vue frontend/src/views/ProductionWorkspaceView.vue frontend/src/views/SystemExtensionView.vue frontend/scripts/test-production-batch-ledger-ui.mjs frontend/scripts/test-smart-operation-report-toggle.mjs
git commit -m "feat: migrate frontend to production batch ledger"
```
---
## Task 8: Update Inventory Inbound UI To Production Ledger
**Files:**
- Modify: `frontend/src/views/InventoryLedgerView.vue`
- Modify: `frontend/scripts/test-production-batch-ledger-ui.mjs`
- Modify: `frontend/scripts/test-production-work-order-inbound-ui.mjs`
- [ ] **Step 1: Extend static checks**
Add to `frontend/scripts/test-production-batch-ledger-ui.mjs`:
```javascript
assert.doesNotMatch(inventory, /生产工单入库/, "inventory should not show old production work-order inbound wording");
assert.doesNotMatch(inventory, /工单号/, "production ledger inbound summary should not use 工单号");
assert.match(inventory, /生产台账入库/, "inventory should show production ledger inbound");
assert.match(inventory, /生产台账/, "branch selectors should use production ledger wording");
assert.match(inventory, /该批材料结单/, "close action should lock material batch production ledger");
assert.match(inventory, /库外未闭环重量/, "summary should show outside unclosed weight");
```
- [ ] **Step 2: Run static test and verify failure**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-production-batch-ledger-ui.mjs
```
Expected: FAIL until inventory UI wording and payloads are migrated.
- [ ] **Step 3: Rename unified inbound drawer**
In `frontend/src/views/InventoryLedgerView.vue`, rename:
```text
生产工单入库 -> 生产台账入库
工单号 -> 生产台账
本次入库后结单 -> 本次入库后该批材料结单
保存生产工单入库 -> 保存生产台账入库
```
Preview endpoint:
```javascript
fetchResource(`/production/production-ledger-inbounds/preview?production_ledger_id=${id}&finished_qty=${finishedQty}`, null)
```
Submit endpoint:
```javascript
postResource("/production/production-ledger-inbounds", {
production_ledger_id: Number(selectedProductionLedgerId.value),
finished_qty: finishedQty,
surplus_weight_kg: surplusWeight,
scrap_weight_kg: scrapWeight,
lock_material_batch: Boolean(productionLedgerInboundForm.lock_material_batch),
lock_confirmed: Boolean(productionLedgerInboundForm.lock_material_batch),
deviation_remark: productionLedgerInboundForm.deviation_remark || null,
remark: productionLedgerInboundForm.remark || null
})
```
- [ ] **Step 4: Update branch payloads**
For 原材料库 · 生产余料入库:
```javascript
source_doc_type: "PRODUCTION_LEDGER",
source_doc_id: Number(selectedProductionLedger.value.production_ledger_id)
```
For 废料库 · 生产废料入库:
```javascript
source_doc_type: "PRODUCTION_LEDGER",
source_doc_id: Number(selectedProductionLedger.value.production_ledger_id)
```
Close checkbox label:
```text
该批材料结单
```
Confirmation text:
```text
确认后该材料库存批次号将从小程序待选列表中剔除;系统会把当前库外未闭环重量作为结单核销记录。
```
- [ ] **Step 5: Run frontend checks**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-production-batch-ledger-ui.mjs
node scripts/test-production-work-order-inbound-ui.mjs
npm run build
```
Expected: PASS after updating old test names or replacing the old script with production ledger checks.
- [ ] **Step 6: Commit**
```bash
git add frontend/src/views/InventoryLedgerView.vue frontend/scripts/test-production-batch-ledger-ui.mjs frontend/scripts/test-production-work-order-inbound-ui.mjs
git commit -m "feat: use production ledger for warehouse production inbound"
```
---
## Task 9: Compatibility And Data Migration
**Files:**
- Create: `backend/sql/migrate_work_orders_to_production_batch_ledger.sql`
- Modify: `backend/app/api/routes/production.py`
- Test: manual SQL verification
- [ ] **Step 1: Create migration SQL**
Create `backend/sql/migrate_work_orders_to_production_batch_ledger.sql`:
```sql
INSERT INTO pp_production_batch_ledger (
material_lot_id,
material_lot_no,
material_item_id,
product_item_id,
status,
miniapp_selectable,
total_issued_weight_kg,
outside_weight_kg,
finished_inbound_qty,
surplus_inbound_weight_kg,
scrap_inbound_weight_kg,
writeoff_weight_kg,
first_issue_time,
last_issue_time,
remark
)
SELECT
issue.source_lot_id,
lot.lot_no,
issue.material_item_id,
wo.product_item_id,
CASE WHEN UPPER(COALESCE(wo.status, '')) = 'SETTLED' THEN '锁单' ELSE '在生产' END,
CASE WHEN UPPER(COALESCE(wo.status, '')) = 'SETTLED' THEN 0 ELSE 1 END,
SUM(COALESCE(issue.issued_weight_kg, 0)),
GREATEST(SUM(COALESCE(issue.issued_weight_kg, 0)) - COALESCE(MAX(wom.returned_weight_kg), 0), 0),
COALESCE(MAX(wo.finished_qty), 0),
COALESCE(MAX(wom.returned_weight_kg), 0),
0,
0,
MIN(issue.issue_time),
MAX(issue.issue_time),
'由历史生产工单迁移生成'
FROM pp_work_order wo
JOIN pp_work_order_material_issue issue ON issue.work_order_id = wo.id
JOIN wh_stock_lot lot ON lot.id = issue.source_lot_id
LEFT JOIN pp_work_order_material wom ON wom.work_order_id = wo.id AND wom.material_item_id = issue.material_item_id
WHERE wo.work_order_type = 'NORMAL'
GROUP BY issue.source_lot_id, lot.lot_no, issue.material_item_id, wo.product_item_id, wo.status
ON DUPLICATE KEY UPDATE
total_issued_weight_kg = VALUES(total_issued_weight_kg),
outside_weight_kg = VALUES(outside_weight_kg),
finished_inbound_qty = VALUES(finished_inbound_qty),
surplus_inbound_weight_kg = VALUES(surplus_inbound_weight_kg),
status = VALUES(status),
miniapp_selectable = VALUES(miniapp_selectable),
remark = CONCAT(COALESCE(pp_production_batch_ledger.remark, ''), ';历史工单迁移已更新');
```
Follow with transaction inserts:
```sql
INSERT INTO pp_production_batch_ledger_txn (
production_ledger_id,
txn_type,
qty_delta,
weight_delta_kg,
outside_weight_after_kg,
source_doc_type,
source_doc_id,
source_line_id,
biz_time,
remark
)
SELECT
ledger.id,
'生产出库',
0,
issue.issued_weight_kg,
ledger.outside_weight_kg,
'历史生产工单',
issue.work_order_id,
issue.id,
issue.issue_time,
'由历史生产工单领料记录迁移'
FROM pp_work_order_material_issue issue
JOIN pp_work_order wo ON wo.id = issue.work_order_id
JOIN pp_production_batch_ledger ledger
ON ledger.material_lot_id = issue.source_lot_id
AND ledger.product_item_id = wo.product_item_id
WHERE wo.work_order_type = 'NORMAL'
AND NOT EXISTS (
SELECT 1
FROM pp_production_batch_ledger_txn existing
WHERE existing.production_ledger_id = ledger.id
AND existing.source_doc_type = '历史生产工单'
AND existing.source_doc_id = issue.work_order_id
AND existing.source_line_id = issue.id
AND existing.txn_type = '生产出库'
);
```
- [ ] **Step 2: Run migration in test DB**
Run:
```bash
mysql -h "$DB_HOST" -P "${DB_PORT:-3306}" -u "$DB_USER" -p"$DB_PASSWORD" --default-character-set=utf8mb4 "$DB_NAME" < /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend/sql/add_production_batch_ledger_patch.sql
mysql -h "$DB_HOST" -P "${DB_PORT:-3306}" -u "$DB_USER" -p"$DB_PASSWORD" --default-character-set=utf8mb4 "$DB_NAME" < /Users/souplearn/Gitlab/py/ForgeFlow-ERP/backend/sql/migrate_work_orders_to_production_batch_ledger.sql
```
Expected: both commands exit `0`.
- [ ] **Step 3: Verify migration**
Run:
```sql
SELECT material_lot_no, product_item_id, status, total_issued_weight_kg, outside_weight_kg
FROM pp_production_batch_ledger
ORDER BY id DESC
LIMIT 20;
```
Expected: rows exist for historical normal work-order issue data.
- [ ] **Step 4: Commit**
```bash
git add backend/sql/migrate_work_orders_to_production_batch_ledger.sql
git commit -m "chore: add legacy work order to production ledger migration"
```
---
## Task 10: Full Verification
**Files:**
- Verify all touched backend, frontend, and miniapp backend files.
- [ ] **Step 1: Backend focused tests**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP
python3 -m pytest \
backend/tests/test_production_batch_ledger.py \
backend/tests/test_smart_operation_report_config.py \
backend/tests/test_selected_stock_lot_production_issue.py \
backend/tests/test_production_work_order_inbound.py \
backend/tests/test_scrap_warehouse_flow.py \
backend/tests/test_miniapp_operation_report_date_filter.py \
-q
```
Expected: PASS.
- [ ] **Step 2: Frontend static checks and build**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
node scripts/test-production-batch-ledger-ui.mjs
node scripts/test-smart-operation-report-toggle.mjs
node scripts/test-operation-report-date-filter.mjs
npm run build
```
Expected: PASS. Vite chunk-size warnings are acceptable.
- [ ] **Step 3: Miniapp backend checks**
Run:
```bash
cd /Users/souplearn/Gitlab/app/JhHardwareWRS_BackPoint
python -m pytest tests/test_work_order_batch_options.py -q
```
Expected: PASS.
- [ ] **Step 4: Manual smoke checks**
Start ERP frontend/backend locally and verify:
- `生产执行` menu shows `生产台账`, not `工单台账`.
- `生产台账` table rows are keyed by `材料库存批次号 + 产品`.
-原材料库生产出库 twice for the same material lot and product updates one ledger row.
- `该批材料结单` changes status to `锁单` and removes the material lot from miniapp dropdown.
- Issuing the same material lot/product after locking reopens the same ledger row to `在生产`.
- When `对接智能报工小程序` is enabled, `工序报工` is visible and ledger detail shows `工序明细`.
- When the switch is disabled, `工序报工` is hidden and ledger detail hides `工序明细`.
- `生产台账入库` calculates theoretical surplus/scrap immediately after finished quantity input.
- Branch entries for production surplus and production scrap update the same production ledger row.
- [ ] **Step 5: Final status check**
Run:
```bash
cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP
git status --short
```
Expected: only intentional files are modified.
---
## Self-Review
- The plan no longer treats `pp_work_order` as the normal production core.
- The new core is `pp_production_batch_ledger`, uniquely representing `材料库存批次号 + 产品`.
- The old `工单台账` wording is replaced by `生产台账`.
- The old “工单结单” concept is replaced by `该批材料结单`.
- The miniapp dropdown source is changed from work orders to active production ledger rows.
- The smart reporting switch remains part of the plan and controls UI/calculation mode.
- Rework work orders are intentionally left on legacy `pp_work_order` because they are a separate return/rework lifecycle.