소스 검색

added scripts for generating guide/test plan for commit(s) in word

production
Fai Luk 3 일 전
부모
커밋
6e2722f1e8
34개의 변경된 파일4179개의 추가작업 그리고 0개의 파일을 삭제
  1. +11
    -0
      .cursor/commands/gen-commit-test-plans.md
  2. +48
    -0
      .cursor/rules/deploy-test-plan.mdc
  3. +28
    -0
      .cursor/rules/gen-commit-test-plan.mdc
  4. +32
    -0
      AGENTS.md
  5. +236
    -0
      docs/MTMS_M18_DATA_MAPPING.md
  6. +33
    -0
      docs/deploy/20260727_isextra_truck_x_ticket_fix.md
  7. +175
    -0
      docs/deploy/HOW_TO_GENERATE_COMMIT_TEST_PLANS.md
  8. +46
    -0
      docs/deploy/TEMPLATE.md
  9. +15
    -0
      docs/deploy/commit-plans/2026-07-20_94dbc8db_no_message.docx
  10. +16
    -0
      docs/deploy/commit-plans/2026-07-22_173e6ce5_workbenchgoodpickexecutiondetail_ui.docx
  11. +30
    -0
      docs/deploy/commit-plans/2026-07-22_3ebf46e3_uom.docx
  12. +11
    -0
      docs/deploy/commit-plans/2026-07-23_18a4d2db_no_message.docx
  13. +13
    -0
      docs/deploy/commit-plans/2026-07-23_a81c6f1c_added_onpack2030_for_pp2404.docx
  14. +14
    -0
      docs/deploy/commit-plans/2026-07-24_5f628db3_no_message.docx
  15. +11
    -0
      docs/deploy/commit-plans/2026-07-24_93c3e931_no_message.docx
  16. +14
    -0
      docs/deploy/commit-plans/2026-07-27_b12b9a49_isextra_truck_ticket_fix.docx
  17. +14
    -0
      docs/deploy/commit-plans/_index.md
  18. +106
    -0
      docs/exports/MTMS_BOM_UserGuide.docx
  19. +149
    -0
      docs/exports/MTMS_DO_Shipping_UserGuide.docx
  20. +105
    -0
      docs/exports/MTMS_JO_Pick_Production_PutAway_UserGuide.docx
  21. +67
    -0
      docs/exports/MTMS_M18_DATA_MAPPING.docx
  22. BIN
      docs/exports/MTMS_M18_DATA_MAPPING.xlsx
  23. +177
    -0
      docs/exports/MTMS_Schedule_JobOrder_UserGuide.docx
  24. +60
    -0
      docs/generated/m18-item-type-mapping.md
  25. +17
    -0
      docs/generated/m18-stsearch-types.md
  26. +256
    -0
      docs/user-guides/MTMS_BOM_使用說明.md
  27. +259
    -0
      docs/user-guides/MTMS_工單提料報工上架_使用說明.md
  28. +459
    -0
      docs/user-guides/MTMS_排程與工單_使用說明.md
  29. +375
    -0
      docs/user-guides/MTMS_送貨訂單與出貨_使用說明.md
  30. +345
    -0
      scripts/export_m18_mapping_office.py
  31. +193
    -0
      scripts/export_user_guide_office.py
  32. +505
    -0
      scripts/generate_commit_test_plans_docx.py
  33. +120
    -0
      scripts/generate_deploy_test_plan.py
  34. +239
    -0
      scripts/generate_m18_mapping_docs.py

+ 11
- 0
.cursor/commands/gen-commit-test-plans.md 파일 보기

@@ -0,0 +1,11 @@
# Gen commit test plans

Generate Word test plans for recent commits (default last 10).

1. Run from FPSMS-backend repo root:
`python scripts/generate_commit_test_plans_docx.py HEAD~10..HEAD`
2. If the user named a range or SHA, use that instead (`HEAD~1..HEAD` for latest only).
3. Reply with the path `docs/deploy/commit-plans/` and summarize `_index.md`.
4. For the newest commit, briefly confirm areas covered; offer to refine from diff if needed.

See `docs/deploy/HOW_TO_GENERATE_COMMIT_TEST_PLANS.md`.

+ 48
- 0
.cursor/rules/deploy-test-plan.mdc 파일 보기

@@ -0,0 +1,48 @@
---
description: Require deploy/release notes with test plan and expected results for commits and PRs
alwaysApply: true
---

# Deploy notes: what changed + how to test

When the user asks to **commit**, **open a PR**, **prepare a deploy**, or **summarize changes for the team**, also produce a short **Deploy / QA note** (Markdown) with test steps and expected results.

## Required sections

```markdown
## Summary
- (1–3 bullets: what changed and why; user-facing impact)

## Scope
- Backend: …
- Frontend: … (if any)
- DB / Liquibase: … (none if N/A)
- Config / ops: … (none if N/A)

## Commits
- `abc1234` — short title

## Test plan
| # | Steps (who / where / data) | Expected result |
|---|----------------------------|-----------------|
| 1 | … | … |
| 2 | … | … |

## Out of scope / not tested
- …

## Rollback
- How to revert or disable if broken (branch, flag, or previous build)
```

## Rules

- Prefer **concrete UI labels** in 「」 and real routes (e.g. `/doworkbench`) when UI is involved.
- Prefer **concrete sample data** when known (item codes, dates, shop codes) from local DB or the change itself.
- Cover **happy path + one failure / edge** when the change adds validation or a fix.
- If only backend API changed, still give **how to verify** (API call, UI screen that uses it, or SQL check).
- Do **not** invent test steps for files you did not inspect; if unclear, say what to confirm with the author.
- Save durable notes under `docs/deploy/` when the user asks to keep them (filename: `YYYYMMDD_short-topic.md`).
- To auto-generate **one Word file per commit** into a folder, run:
`python scripts/generate_commit_test_plans_docx.py <range>` → `docs/deploy/commit-plans/`.
- Commit messages should still be clear; “no message” is not acceptable when we author the commit.

+ 28
- 0
.cursor/rules/gen-commit-test-plan.mdc 파일 보기

@@ -0,0 +1,28 @@
---
description: When user asks to gen test plan(s) for new/recent commit(s), auto-run the Word generator
alwaysApply: true
---

# Gen commit test plans (simple ask)

If the user says anything like:

- 「gen test plan」 / 「generate test plan」
- 「new commit test plan」 / 「test plan for commit」
- 「產測試計畫」 / 「產生 commit 測試計畫」
- 「有新 commit,幫我出 test plan」

Then **do this immediately** (do not only explain):

1. Default range: **new / recent commits** = `HEAD~10..HEAD`
- If user gives a SHA or `A..B`, use that.
- If they say “latest / this commit”, use `HEAD~1..HEAD`.
- If they say “since last deploy” and give a SHA, use `<sha>..HEAD`.
2. Run from repo root:
```bash
python scripts/generate_commit_test_plans_docx.py <range>
```
3. Tell them the output folder: `docs/deploy/commit-plans/` and point to `_index.md`.
4. Optionally refine the **latest** commit’s plan from the real diff if steps look too generic.

Team how-to: `docs/deploy/HOW_TO_GENERATE_COMMIT_TEST_PLANS.md`

+ 32
- 0
AGENTS.md 파일 보기

@@ -10,3 +10,35 @@
- If a task is about UI behavior (charts, clicks, page rendering, dialogs), check `../FPSMS-frontend`.
- If a task is about API/business logic/data query, check this backend repo.
- For end-to-end changes, update both repos and keep API request/response fields aligned.

## M18 ↔ MTMS data mapping

- Handbook: [`docs/MTMS_M18_DATA_MAPPING.md`](docs/MTMS_M18_DATA_MAPPING.md)
- Auto tables: [`docs/generated/`](docs/generated/) — regenerate with `python scripts/generate_m18_mapping_docs.py` after changing `ItemType` / `M18ItemType` or product-type `when` in `M18MasterDataService`.
- Word / Excel (readable exports): `python scripts/export_m18_mapping_office.py` → [`docs/exports/`](docs/exports/)

## User guides (UI-oriented)

- Source Markdown: [`docs/user-guides/`](docs/user-guides/)
- Export Word: `python scripts/export_user_guide_office.py` → `docs/exports/*.docx`
- Guides (UI labels in 「」):
- 排程 → 工單 → `MTMS_Schedule_JobOrder_UserGuide.docx`
- BOM 匯入/啟停/應用 → `MTMS_BOM_UserGuide.docx`
- 工單提料/報工/上架 → `MTMS_JO_Pick_Production_PutAway_UserGuide.docx`
- 送貨訂單/放單/成品出倉 → `MTMS_DO_Shipping_UserGuide.docx`

## Deploy / QA notes (what changed + how to test)

- **How-to for the team:** [`docs/deploy/HOW_TO_GENERATE_COMMIT_TEST_PLANS.md`](docs/deploy/HOW_TO_GENERATE_COMMIT_TEST_PLANS.md)
- **In Cursor:** say `gen test plan` / `產測試計畫`, or slash **`/gen-commit-test-plans`**
- Template: [`docs/deploy/TEMPLATE.md`](docs/deploy/TEMPLATE.md)
- Filled notes: [`docs/deploy/`](docs/deploy/) (e.g. `YYYYMMDD_short-topic.md`)
- **Auto Word test plan per commit** (folder):
```bash
pip install python-docx
python scripts/generate_commit_test_plans_docx.py HEAD~10..HEAD
```
→ [`docs/deploy/commit-plans/`](docs/deploy/commit-plans/) (`YYYY-MM-DD_<sha>_<slug>.docx` + `_index.md`)
Optional: `--also-md` / `--out-dir path` / `--limit N`
- Scaffold one range note: `python scripts/generate_deploy_test_plan.py HEAD~5..HEAD --out docs/deploy/draft.md`
- For critical deploys, ask the agent to refine steps against the real diff (Cursor rule: deploy-test-plan)

+ 236
- 0
docs/MTMS_M18_DATA_MAPPING.md 파일 보기

@@ -0,0 +1,236 @@
# MTMS (FPSMS) ↔ M18 資料對照手冊

本文件說明 **MTMS / FPSMS** 與 **M18** 之間的主檔與交易對應、同步方向,以及已知陷阱。

| 區塊 | 維護方式 |
|------|----------|
| 本手冊(說明、流程、陷阱) | **人手**維護 |
| [`docs/generated/`](./generated/) 對照表 | **腳本產生**(見下方) |

重新產生自動表(改完 enum / mapping 後請跑):

```bash
python scripts/generate_m18_mapping_docs.py
```

匯出 **Word / Excel**(給非技術閱讀;需已安裝 `python-docx`、`openpyxl`):

```bash
pip install python-docx openpyxl
python scripts/export_m18_mapping_office.py
```

產出:

- `docs/exports/MTMS_M18_DATA_MAPPING.docx`
- `docs/exports/MTMS_M18_DATA_MAPPING.xlsx`

---

## 1. 系統與名詞

| 名稱 | 說明 |
|------|------|
| **MTMS / FPSMS** | 本後端 `FPSMS-backend` + 前端 `FPSMS-frontend` |
| **M18** | 外部 ERP/主檔與採購/送貨來源系統 |
| **Pull** | M18 → MTMS(product / vendor / unit / currency / BOM / business unit / PO / DO) |
| **Push** | MTMS → M18(例如 GRN、BOM for shop) |

設定入口:`m18/M18Config.kt`(`m18.config.*`)、scheduler 見 `application.yml` / `application-prod.yml`(`scheduler.m18Sync`、`scheduler.m18Grn`)。

主程式目錄:`src/main/java/com/ffii/fpsms/m18/`。

---

## 2. Master API 類型(`StSearchType`)

完整表見自動產生檔:

→ **[generated/m18-stsearch-types.md](./generated/m18-stsearch-types.md)**

摘要:

| M18 `stSearch` | MTMS 落點 |
|----------------|-----------|
| `pro` | `items` |
| `ven` | `shop`(`type=supplier`) |
| `virDept` | `shop`(`type=shop`) |
| `unit` | `uom_conversion`(+ cunit) |
| `cur` | `currency` |
| `udfbomforshop` | `bom` / materials |

實作:`M18MasterDataService`。

---

## 3. 貨品類型(最常查)

### 3.1 同步規則(自動表)

→ **[generated/m18-item-type-mapping.md](./generated/m18-item-type-mapping.md)**

程式:`M18MasterDataService.saveProduct` / `saveProducts` 依 `pro.udfProducttype`:

```text
Consumable Material → consumables
Non-consumable Material → non-consumables
Product → fg
WIP → sfg
Item → item
(其他,含 CMB) → mat ← default
```

Enum 定義:`modules/master/web/models/NewItemRequest.kt`(`ItemType`、`M18ItemType`)。

### 3.2 UI 顯示(存貨)

存貨 Type 欄:`t(itemType)`,翻譯在 `FPSMS-frontend/src/i18n/zh/inventory.json`。

| `items.type` | 存貨頁(zh) |
|--------------|--------------|
| `mat` | 原料 |
| `fg` | 成品 |
| `sfg` / `wip` | 半成品 |
| `consumables` / `cmb` | 消耗品 |
| `non-consumables` / `nm` | 非消耗品/雜項 |

> 系統 **沒有**「產品」這個 `items.type`。M18 的 **Product** 對應 MTMS **`fg`(成品)**。

### 3.3 可手動改嗎?

可以:Settings → Items → Edit → Type(`ProductDetails.tsx`:`fg` / `wip` / `mat` / `cmb` / `nm`)。

注意:之後若再跑 **product sync**,type 會依 M18 `udfProducttype` **覆寫**(含再次落到 `mat`)。

### 3.4 已知陷阱:`CMB`

M18 實務上可出現 `"udfProducttype": "CMB"`(例如蔗糖水 `MG1852`)。

- `"CMB"` ≠ `"Consumable Material"`
- 也不等於前端的 `cmb`
- → sync 走 **else → `mat`** → 存貨顯示 **原料**

若要顯示消耗品:需改 mapping(例如把 `CMB` 對到 `consumables`),或在 M18 改成已支援的字串;僅手動改 MTMS 可能被下次 sync 蓋掉。

---

## 4. 供應商與店鋪

| 方向 | M18 | MTMS |
|------|-----|------|
| Pull vendors | `ven` | `shop`,`ShopType.SUPPLIER`(`supplier`) |
| Pull business units | `virDept` | `shop`,`ShopType.SHOP`(`shop`) |

鍵:`shop.m18Id`、`shop.code`。名稱優先 `descZhTW` → `descZhCN` → `desc`。

`ShopType`:`modules/master/enums/ShopType.kt`。

---

## 5. 單位(UoM)

| M18 | MTMS |
|-----|------|
| Unit master (`unit`) | `uom_conversion`(`code`、`udfudesc`、`udfShortDesc`、`m18Id`…) |
| Cunit 明細 | `M18CunitService.replaceForUnit` |

Item 級採購/庫存/銷售單位在 sync product price 時寫入 `item_uom`(見 `M18MasterDataService` product 區塊)。

PO/DO 行常同時保留:

| 欄位 | 意義 |
|------|------|
| `qty` / `uomId` | MTMS 業務單位(例如採購單位換算後) |
| `qtyM18` / `uomIdM18` | M18 原始單位數量 |

PO 換算邏輯見 `M18PurchaseOrderService`(`convertQtyToPurchaseQty`)。

---

## 6. 貨幣、BOM

| M18 | MTMS | Service |
|-----|------|---------|
| Currency | `currency` | `saveCurrencies` |
| BOM (`udfbomforshop`) | `bom` / `bom_material` | `saveBoms` |

Shop BOM **回寫** M18:`M18BomForShopService`(push)。

---

## 7. 交易文件(摘要)

| 文件 | 方向 | MTMS 主表 | 筆記 |
|------|------|-----------|------|
| PO | M18 → MTMS | `purchase_order` / `purchase_order_line` | `m18Id` / data log;qty 可能換算 |
| DO | M18 → MTMS | `delivery_order` / `delivery_order_line` | 含 `qtyM18`、`uomIdM18` |
| GRN | MTMS → M18 | stock-in → M18 GRN API | 部分 `m18CreatedUId` **不送** GRN(見下) |

### GRN 略過規則

`m18/M18GrnRules.kt`:

| M18 PO `createUid` | 備註 | 行為 |
|--------------------|------|------|
| `2569` | legato | 不 post GRN |
| `2676` | xtech | 不 post GRN |

---

## 8. Config 鍵(對照時常用)

見 `M18Config` / `application-*.yml`:

- `m18.config.seriesId.pp|pf|sc|se|sf|sr`
- `m18.config.beId.pp|pf|toa`
- `m18.config.supplier-not.material-po`
- `m18.config.supplier.shop-po` / `oem-po`
- `scheduler.m18Sync.enabled`
- `scheduler.m18Grn.createEnabled`

---

## 9. 驗證用 SQL 範例

```sql
-- 某貨品目前 type(決定存貨顯示)
SELECT code, name, type, m18Id, m18LastModifyDate
FROM items
WHERE deleted = 0 AND code = 'MG1852';

-- 統計 type 分佈
SELECT type, COUNT(*) AS cnt
FROM items
WHERE deleted = 0
GROUP BY type
ORDER BY cnt DESC;
```

若 M18 回傳 `udfProducttype` 可與上表比對;對不上表中「exact string」者皆會變 `mat`。

---

## 10. 維護約定

1. **改 mapping**:先改 Kotlin enum / `when`,再跑 `python scripts/generate_m18_mapping_docs.py`,把 `docs/generated/*` 一併 commit。
2. **改說明/陷阱**:只改本檔,勿手改 `docs/generated/`。
3. **給營運/開會用**:跑 `python scripts/export_m18_mapping_office.py`,打開 `docs/exports/*.docx` / `*.xlsx`。
4. **新發現的 M18 值**(如新的 `udfProducttype`):記入 generated 腳本的 `KNOWN_UNMAPPED_M18_VALUES`,或補正式 mapping 後重生。
5. PR 若動到 `NewItemRequest.kt` / `M18MasterDataService` product type 分支,review 應檢查 generated docs 是否已更新。

---

## 11. 相關程式索引

| 主題 | 路徑 |
|------|------|
| Item / M18 type enums | `modules/master/web/models/NewItemRequest.kt` |
| Product sync | `m18/service/M18MasterDataService.kt` |
| StSearch | `m18/model/M18MasterDataRequest.kt` |
| Shop type | `modules/master/enums/ShopType.kt` |
| GRN skip | `m18/M18GrnRules.kt` |
| PO sync | `m18/service/M18PurchaseOrderService.kt` |
| DO sync | `m18/service/M18DeliveryOrderService.kt` |
| BOM→M18 | `m18/service/M18BomForShopService.kt` |
| 存貨 Type 顯示 | `InventoryTable.tsx` + `i18n/zh/inventory.json` |
| 物品 Type 下拉 | `CreateItem/ProductDetails.tsx` |

+ 33
- 0
docs/deploy/20260727_isextra_truck_x_ticket_fix.md 파일 보기

@@ -0,0 +1,33 @@
# Deploy note — isExtra / 車線-X workbench ticket fix
Date: 2026-07-27
Branch / build: production (`b12b9a49`)
Author: (fill)

## Summary
- Workbench「加單」檢視納入整組 Etra 放單類型(`isExtra` / `isExtrabatch` / `isExtrasingle`),不再只認單一 `isExtra`。
- 「車線-X」加單票依供應商偏好樓層在畫面上拆成「2/F」/「4/F」顯示(不改 DB `storeId`),撳單指派篩選與之一致。

## Scope
- Backend: `DoWorkbenchMainService`, `DoWorkbenchDopoAssignmentService`, `WorkbenchReleaseTypeSupport`, `DoDetailResponse`
- Frontend: 無(沿用現有 `/doworkbench` 加單 UI)
- DB / Liquibase: none
- Config / ops: none

## Commits
- `b12b9a49` — isextra truck X ticket fix

## Test plan
| # | Steps | Expected result |
|---|--------|-----------------|
| 1 | 「倉庫管理」→「成品出倉」→ Tab「加單」。選有 **isExtrabatch/isExtrasingle** 票的「是日/翌日」日期。 | 加單車線面板看得到這些票(不只舊的 `isExtra`);未撳數/總單數合理。 |
| 2 | 同一日找 **車線-X** 且 `storeId` 空的加單票;對照供應商屬 2F/4F 設定。 | 票出現在對應「2/F 票」或「4/F 票」檢視;不應因 storeId 空而整組消失。 |
| 3 | 在加單模式對該車線按撳單/「確認分配」。 | 指派成功;票進入提料明細,`ticketStatus` 變提貨中;無「此樓層沒有可用的提料單」誤報(若確實有未分配票)。 |
| 4 | 對照:一般「批量/單量」非加單票、非車線-X。 | 行為與修前相同(回歸)。 |
| 5 | (負向)加單日完全無 Etra 票。 | 「該樓層未有需處理訂單」或空面板,無 500。 |

## Out of scope / not tested
- 送貨訂單「批量放單」建立票邏輯本身(僅測 Workbench 加單顯示/指派)
- 前端 UI 大改(其他 commit)

## Rollback
- 還原 `b12b9a49` 或 redeploy 前一版 backend build

+ 175
- 0
docs/deploy/HOW_TO_GENERATE_COMMIT_TEST_PLANS.md 파일 보기

@@ -0,0 +1,175 @@
# How to generate commit test plans (Word)

This guide is for QA / deploy teammates. Use it when a build is ready and you need **what changed** and **how to test** for each commit.

## Easiest: just tell Cursor

In Cursor chat (Agent), say any of:

- `gen test plan`
- `generate test plan for new commits`
- `產測試計畫` / `有新 commit,幫我出 test plan`
- Or use the slash command: **`/gen-commit-test-plans`**

Cursor will run the generator and put Word files in `docs/deploy/commit-plans/`.

You can be more specific:

- `gen test plan for latest commit`
- `gen test plan for HEAD~5..HEAD`
- `gen test plan since abc1234`

---

## What you get

Running the generator creates **one Word file per commit** under:

```
docs/deploy/commit-plans/
YYYY-MM-DD_<shortsha>_<slug>.docx
_index.md ← list of all generated plans
```

Each `.docx` includes:

- Commit id, date, author, message
- Auto-detected areas (e.g. 送貨出倉, 工單, BOM)
- Files touched
- **Test plan table**: Steps + Expected result

> These plans are a **starting point** (from commit message + file paths). For critical releases, refine steps with the developer or ask the Cursor agent to review the real diff.

---

## Prerequisites

1. Clone / pull latest `FPSMS-backend`.
2. Install Python 3, then:

```bash
pip install python-docx
```

3. Open a terminal at the **repo root** (`FPSMS-backend`).

---

## Basic usage

Generate plans for the last 10 commits:

```bash
python scripts/generate_commit_test_plans_docx.py HEAD~10..HEAD
```

Then open the folder:

```
docs/deploy/commit-plans/
```

Start from `_index.md` to see the list, then open each `.docx`.

---

## Common examples

### Last N commits on current branch

```bash
python scripts/generate_commit_test_plans_docx.py HEAD~5..HEAD
```

### Only the latest commit

```bash
python scripts/generate_commit_test_plans_docx.py HEAD~1..HEAD
```

### Commits since a known SHA (e.g. last production deploy)

```bash
python scripts/generate_commit_test_plans_docx.py <last_deployed_sha>..HEAD
```

Example:

```bash
python scripts/generate_commit_test_plans_docx.py b12b9a49..HEAD
```

### Commits on `production` that are not yet on another branch

```bash
python scripts/generate_commit_test_plans_docx.py origin/staging..origin/production
```

(Adjust branch names to match your remote.)

### Custom output folder

```bash
python scripts/generate_commit_test_plans_docx.py HEAD~10..HEAD --out-dir docs/deploy/commit-plans/release-2026-08-01
```

### Also write Markdown next to Word

```bash
python scripts/generate_commit_test_plans_docx.py HEAD~10..HEAD --also-md
```

### Cap how many commits are processed

```bash
python scripts/generate_commit_test_plans_docx.py HEAD~50..HEAD --limit 15
```

---

## Suggested team workflow

1. Developer merges / tags the build to deploy.
2. QA or release owner runs the generator for **only the commits in this deploy** (use `last_sha..new_sha`).
3. Share the `docs/deploy/commit-plans/` folder (or zip it) with testers.
4. Testers follow Steps / Expected result in each Word file; mark pass/fail.
5. For vague commits (`no message`), ask the author to clarify before sign-off.
6. Optional: keep a hand-written summary in `docs/deploy/YYYYMMDD_topic.md` (see `TEMPLATE.md`).

---

## Related files

| File | Purpose |
|------|---------|
| `scripts/generate_commit_test_plans_docx.py` | Auto Word plans **per commit** |
| `scripts/generate_deploy_test_plan.py` | One Markdown scaffold for a **whole range** |
| `docs/deploy/TEMPLATE.md` | Manual deploy / QA note template |
| `docs/deploy/20260727_isextra_truck_x_ticket_fix.md` | Example of a filled manual note |

---

## Troubleshooting

| Problem | What to try |
|---------|-------------|
| `No module named 'docx'` | `pip install python-docx` |
| `No commits in range` | Check the range syntax: `A..B` means commits reachable from B but not from A |
| Word filename looks odd | Filenames are ASCII-only on purpose (Windows-safe). Chinese text is still inside the document |
| Steps look too generic | Commit had `no message` or unusual paths — ask author / refine with agent |
| Need frontend checks | This repo is backend; also check `FPSMS-frontend` if the change is UI |

---

## Quick copy-paste (release day)

```bash
cd /path/to/FPSMS-backend
git fetch
git checkout <deploy-branch>
pip install python-docx
python scripts/generate_commit_test_plans_docx.py <previous_release_sha>..<this_release_sha>
explorer docs\deploy\commit-plans
```

(On Mac/Linux, open `docs/deploy/commit-plans` in Finder/files instead of `explorer`.)

+ 46
- 0
docs/deploy/TEMPLATE.md 파일 보기

@@ -0,0 +1,46 @@
# Deploy / QA note template

Copy this for each deploy (or ask the agent: “寫 deploy test plan for commits X..Y”).

```markdown
# Deploy note — <short title>
Date: YYYY-MM-DD
Branch / build: <branch or tag>
Author: <name>

## Summary
-

## Scope
- Backend:
- Frontend:
- DB / Liquibase: none
- Config / ops: none

## Commits
- `________` —

## Test plan
| # | Steps | Expected result |
|---|--------|-----------------|
| 1 | | |
| 2 | | |

## Out of scope / not tested
-

## Rollback
-
```

Filled examples live in `docs/deploy/` after each release prep.

### Auto-generate Word plans (one file per commit)

Full team instructions: [`HOW_TO_GENERATE_COMMIT_TEST_PLANS.md`](HOW_TO_GENERATE_COMMIT_TEST_PLANS.md)

```bash
pip install python-docx
python scripts/generate_commit_test_plans_docx.py HEAD~10..HEAD
# → docs/deploy/commit-plans/<date>_<sha>_<slug>.docx
```

+ 15
- 0
docs/deploy/commit-plans/2026-07-20_94dbc8db_no_message.docx 파일 보기

@@ -0,0 +1,15 @@
Test plan -- 94dbc8db
no messageCommit: 94dbc8db9a0daa677613bfd16bbc039fcd092f02
Date: 2026-07-20 | Author: tommy
Generated: 2026-08-01 15:55 UTCSummary
no messageAreas (auto-detected)
庫存 / 上架 / 出入倉
Files touched
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
src/main/java/com/ffii/fpsms/modules/stock/trace/ItemLotTraceOrchestrator.kt
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceMovementLoader.kt
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceOutboundLoader.kt
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1依改動點進「上架掃碼」或相關庫存查詢頁。掃碼/查詢結果與庫存數量合理。2做一筆小量入/出/調撥(測試庫)。成功;庫存異動可查。3部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。4(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。5向作者確認此 commit 的實際意圖(訊息為 empty/no message)。補上說明後再簽核上線。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 16
- 0
docs/deploy/commit-plans/2026-07-22_173e6ce5_workbenchgoodpickexecutiondetail_ui.docx 파일 보기

@@ -0,0 +1,16 @@
Test plan -- 173e6ce5
WorkbenchGoodPickExecutionDetail UI 大改Commit: 173e6ce580b779c4577c3c89449b7bfbb18fd43f
Date: 2026-07-22 | Author: CANCERYS\kw093
Generated: 2026-08-01 15:55 UTCSummary
WorkbenchGoodPickExecutionDetail UI 大改WorkbenchTicketReleaseTable 加 user filter

DO release / FloorLanePanel:Truck X 票 + 搜尋依樓層Areas (auto-detected)
送貨訂單 / 成品出倉
Files touched
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1「送貨訂單」依預計送貨日搜索相關單,必要時「批量放單」或詳情「放單」。放單成功;產生提料票,無未預期 500。2「成品出倉」`/doworkbench`:撳單 --> 掃碼提料 --> 填箱數列印。票可撳、可提、可完成;狀態「待撳單」-->「提貨中」-->「已完成」。3若涉及加單/車線-X:開「加單」分頁與「車線-X」對照。票出現在正確樓層/車線;指派篩選與顯示一致。4核對「車線-X」票在 2/F/4/F 顯示。出現在正確樓層區塊,可指派。5部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。6(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 30
- 0
docs/deploy/commit-plans/2026-07-22_3ebf46e3_uom.docx 파일 보기

@@ -0,0 +1,30 @@
Test plan -- 3ebf46e3
建議批號 UOM 檢核/不符則擋、標籤列印只顯示同 UOMCommit: 3ebf46e304ccc847f178cb57dc5ed106f140e0ec
Date: 2026-07-22 | Author: CANCERYS\kw093
Generated: 2026-08-01 15:55 UTCSummary
建議批號 UOM 檢核/不符則擋、標籤列印只顯示同 UOMAreas (auto-detected)
提料單 / Workbench
建議批號 / UOM
庫存 / 上架 / 出入倉
來貨 / 品檢
主檔 (Item/Shop 等)
庫存查詢
Files touched
src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt
src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt
src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderWorkbenchService.kt
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt
src/main/java/com/ffii/fpsms/modules/stock/entity/projection/InventoryLotLineInfo.kt
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineWorkbenchService.kt
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt
src/main/java/com/ffii/fpsms/modules/stock/web/InventoryController.kt
src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt
src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1「成品出倉」撳單並完成一張提料票(測試庫)。掃碼/提交正常;完成後記錄頁可見。2「查看提貨情況」核對該票狀態。狀態與負責人符合操作。3出倉或提料時掃建議批號;刻意掃 UOM 不符批號。不符 UOM 被擋並有明確提示;相符批號可提交。4標籤列印/批號列表只應出現同 UOM 選項(若本次改動涵蓋)。列表無錯誤 UOM 批號。5依改動點進「上架掃碼」或相關庫存查詢頁。掃碼/查詢結果與庫存數量合理。6做一筆小量入/出/調撥(測試庫)。成功;庫存異動可查。7「工單生產流程」-->「品檢」或待 QC 列表。可開品檢;確定後狀態更新。8主檔搜索受影響編號,核對顯示欄位。名稱/單位/類型等與預期一致。9庫存搜索頁用受影響貨品/倉位查詢。批號、數量、單位正確。10掃一筆 UOM 不符的批號/物料。系統拒絕或明確提示;相符者可過。11選打印機後列印標籤/送貨單標籤。成功列印;內容正確。12部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。13(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 11
- 0
docs/deploy/commit-plans/2026-07-23_18a4d2db_no_message.docx 파일 보기

@@ -0,0 +1,11 @@
Test plan -- 18a4d2db
no messageCommit: 18a4d2dbffd46cba5582aae035bf75bc0af505ba
Date: 2026-07-23 | Author: Fai Luk
Generated: 2026-08-01 15:55 UTCSummary
no messageAreas (auto-detected)
資料庫變更
Files touched
src/main/resources/db/changelog/changes/20260723_onpack_qr_pp2404.sql
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1部署後確認 Liquibase/changelog 已套用(或啟動 log 無 changeset 失敗)。DB schema/資料符合 changeset。2用相關畫面或 SQL 抽樣驗證新欄位/約束。讀寫正常,無缺欄錯誤。3選打印機後列印標籤/送貨單標籤。成功列印;內容正確。4部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。5(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。6向作者確認此 commit 的實際意圖(訊息為 empty/no message)。補上說明後再簽核上線。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 13
- 0
docs/deploy/commit-plans/2026-07-23_a81c6f1c_added_onpack2030_for_pp2404.docx 파일 보기

@@ -0,0 +1,13 @@
Test plan -- a81c6f1c
added onpack2030 for pp2404Commit: a81c6f1cf66632c0c715b0f23ed9148fa019b79e
Date: 2026-07-23 | Author: Fai Luk
Generated: 2026-08-01 15:55 UTCSummary
added onpack2030 for pp2404Areas (auto-detected)
標籤 / OnPack
Files touched
src/main/resources/onpack2030/3960CFF6FC46C5A174CA3C78D690CA15.bmp
src/main/resources/onpack2030/PP2404.image
src/main/resources/onpack2030/PP2404.job
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1對新增/修改的貨品編號列印標籤(測試機)。圖檔/job 正確;可印出。2選打印機後列印標籤/送貨單標籤。成功列印;內容正確。3部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。4(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 14
- 0
docs/deploy/commit-plans/2026-07-24_5f628db3_no_message.docx 파일 보기

@@ -0,0 +1,14 @@
Test plan -- 5f628db3
no messageCommit: 5f628db30cb513b88f2aaa164700db0240700ea7
Date: 2026-07-24 | Author: Fai Luk
Generated: 2026-08-01 15:55 UTCSummary
no messageAreas (auto-detected)
標籤 / OnPack
Files touched
src/main/resources/onpack2030/PP2404.image
src/main/resources/onpack2030/PP2404.job
src/main/resources/onpack2030/pp2404.image
src/main/resources/onpack2030/pp2404.job
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1對新增/修改的貨品編號列印標籤(測試機)。圖檔/job 正確;可印出。2選打印機後列印標籤/送貨單標籤。成功列印;內容正確。3部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。4(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。5向作者確認此 commit 的實際意圖(訊息為 empty/no message)。補上說明後再簽核上線。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 11
- 0
docs/deploy/commit-plans/2026-07-24_93c3e931_no_message.docx 파일 보기

@@ -0,0 +1,11 @@
Test plan -- 93c3e931
no messageCommit: 93c3e9318fa3f5fc65c3ab1ca06b3e73d1caf0e6
Date: 2026-07-24 | Author: Fai Luk
Generated: 2026-08-01 15:55 UTCSummary
no messageAreas (auto-detected)
標籤 / OnPack
Files touched
src/main/resources/onpack2030/PP2404.image
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1對新增/修改的貨品編號列印標籤(測試機)。圖檔/job 正確;可印出。2選打印機後列印標籤/送貨單標籤。成功列印;內容正確。3部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。4(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。5向作者確認此 commit 的實際意圖(訊息為 empty/no message)。補上說明後再簽核上線。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 14
- 0
docs/deploy/commit-plans/2026-07-27_b12b9a49_isextra_truck_ticket_fix.docx 파일 보기

@@ -0,0 +1,14 @@
Test plan -- b12b9a49
isextra truck X ticket fixCommit: b12b9a499f63730c0a61e40075e6eda7785f6919
Date: 2026-07-27 | Author: CANCERYS\kw093
Generated: 2026-08-01 15:55 UTCSummary
isextra truck X ticket fixAreas (auto-detected)
送貨訂單 / 成品出倉
Files touched
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/WorkbenchReleaseTypeSupport.kt
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt
Test plan
Auto-generated from paths + commit message. Refine before sign-off on critical deploys.#StepsExpected result1「送貨訂單」依預計送貨日搜索相關單,必要時「批量放單」或詳情「放單」。放單成功;產生提料票,無未預期 500。2「成品出倉」`/doworkbench`:撳單 --> 掃碼提料 --> 填箱數列印。票可撳、可提、可完成;狀態「待撳單」-->「提貨中」-->「已完成」。3若涉及加單/車線-X:開「加單」分頁與「車線-X」對照。票出現在正確樓層/車線;指派篩選與顯示一致。4「成品出倉」-->「加單」檢視該日票。加單票可見且可撳單。5核對「車線-X」票在 2/F/4/F 顯示。出現在正確樓層區塊,可指派。6重現原問題步驟一次。問題不再出現;無新副作用。7部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。頁面可開、無全域錯誤橫幅。8(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。無明顯回退。Out of scope / notes
Frontend-only changes may live in FPSMS-frontend -- verify paired repo if UI behavior is expected.Rollback: revert this commit or redeploy previous backend build.

+ 14
- 0
docs/deploy/commit-plans/_index.md 파일 보기

@@ -0,0 +1,14 @@
# Commit test plans (auto-generated)

Generated: 2026-08-01 15:55 UTC

| Date | SHA | Subject | Word |
|------|-----|---------|------|
| 2026-07-20 | `94dbc8db` | no message | [2026-07-20_94dbc8db_no_message.docx](2026-07-20_94dbc8db_no_message.docx) |
| 2026-07-22 | `3ebf46e3` | 建議批號 UOM 檢核/不符則擋、標籤列印只顯示同 UOM | [2026-07-22_3ebf46e3_uom.docx](2026-07-22_3ebf46e3_uom.docx) |
| 2026-07-22 | `173e6ce5` | WorkbenchGoodPickExecutionDetail UI 大改 | [2026-07-22_173e6ce5_workbenchgoodpickexecutiondetail_ui.docx](2026-07-22_173e6ce5_workbenchgoodpickexecutiondetail_ui.docx) |
| 2026-07-23 | `a81c6f1c` | added onpack2030 for pp2404 | [2026-07-23_a81c6f1c_added_onpack2030_for_pp2404.docx](2026-07-23_a81c6f1c_added_onpack2030_for_pp2404.docx) |
| 2026-07-23 | `18a4d2db` | no message | [2026-07-23_18a4d2db_no_message.docx](2026-07-23_18a4d2db_no_message.docx) |
| 2026-07-24 | `93c3e931` | no message | [2026-07-24_93c3e931_no_message.docx](2026-07-24_93c3e931_no_message.docx) |
| 2026-07-24 | `5f628db3` | no message | [2026-07-24_5f628db3_no_message.docx](2026-07-24_5f628db3_no_message.docx) |
| 2026-07-27 | `b12b9a49` | isextra truck X ticket fix | [2026-07-27_b12b9a49_isextra_truck_ticket_fix.docx](2026-07-27_b12b9a49_isextra_truck_ticket_fix.docx) |

+ 106
- 0
docs/exports/MTMS_BOM_UserGuide.docx 파일 보기

@@ -0,0 +1,106 @@
MTMS 使用說明:BOM(匯入/啟停/應用)
本手冊依目前前端畫面與繁中文案整理,按鈕/選單名稱以畫面上「」內文字為準。
適用範圍:側欄「設定」下的 BOM 相關頁、建立工單時選 BOM、排程「自動生成工單」。
「一附」章節使用本機資料庫 fpsmsdb 的真實例子(查詢當下快照);其他環境請改條件再對。
產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。
這份手冊怎麼用
符號意思「......」畫面上看得到的按鈕、選單、標題-->下一步操作建議閱讀順序:
先匯入/啟用 BOM(「設定」-->「匯入 BOM」)
再檢查單位問題(ADMIN:「BOM / 物料單位問題」)
再建工單/排程產單(選 active BOM)
一、整體流程(BOM 從主檔到工單)
【匯入 BOM】Excel 上傳並檢查 -->「確認匯入」
【BOM 明細】確認「啟用」/必要時「停用」
↓(選做)「BOM 權重得分」調整加權
↓(ADMIN)「BOM / 物料單位問題」巡檢
【建工單】「建立工單」選「BOM」
或【排程】「自動生成工單」(僅含具 BOM 的物料)一附、本地資料庫實例(方便對照畫面)
以下數字來自本機 fpsmsdb 查詢結果。不同環境/日期資料會不同。
BOM 狀態:active-->「啟用」、inactive-->「停用」。本機約有 214 筆啟用、3 筆停用。
例 A:啟用中的成品 BOM(適合在「BOM 明細」搜索)
「物品編號」「物品名稱」「BOM 狀態」約材料筆數PP2167牛丼啟用10PP2277烚意粉啟用3PP2390熱情香果醬啟用4PP1224柚子蒜蓉汁啟用5PP1136白粥啟用9PP0259牛肉水(2KG/包)啟用0(無材料列,排程/提料前宜先確認)怎麼練:
「設定」-->「匯入 BOM」--> Tab「BOM 明細」。
「物品編號」填 PP2167 -->「搜索」。
載入後看「基本資訊」「材料清單」「製程與設備」;「BOM 狀態」應為「啟用」。
例 B:材料清單長什麼樣(PP2167 牛丼)
「物品編號」「物品名稱」約「基本數量」MA0471日式超薄肥牛272.16FA0164洋蔥絲145.15GI3236清水60.00PP2214丼飯汁(2磅/包)41.73MG1507江泉牌燒肉汁(1.8L/支)9.00MG1299李錦記特選老抽(8L/桶)8.00MG0302佛手牌味粉(10lb/罐)4.54MG0239韓國雪白幼砂糖(30kg/包)4.54例 C:用 BOM 建工單(接《排程與工單》手冊)
本機 2026-07-29 手動工單多掛在 active BOM 上,例如:「工單編號」「狀態」成品 BOM「需求數量」JO-260729-038待處理PP2277 烚意粉51JO-260729-044提料中PP2383 辣椒菜脯265怎麼練:「搜索工單/ 建立工單」-->「建立工單」-->「BOM」下拉選 PP2277(僅顯示啟用中的 BOM)。
自行查核用 SQL(選用)
SELECT b.code, i.name, b.status,
(SELECT COUNT(*) FROM bom_material bm
WHERE bm.bomId = b.id AND IFNULL(bm.deleted,0)=0) AS mats
FROM bom b
JOIN items i ON i.id = b.itemId
WHERE b.deleted = 0 AND b.code = 'PP2167';

SELECT i.code, i.name, bm.qty
FROM bom_material bm
JOIN items i ON i.id = bm.itemId
JOIN bom b ON b.id = bm.bomId
WHERE b.code = 'PP2167' AND IFNULL(bm.deleted,0)=0;二、匯入 BOM(「設定」-->「匯入 BOM」)
2.1 入口
側欄 「設定」 --> 「匯入 BOM」(路徑通常為 /settings/importBom)
頁內標題可能顯示英文 Import BOM;側欄與 Tab 以繁中「匯入 BOM」為準。
2.2 兩個分頁
Tab用途「匯入 BOM」上傳 Excel、檢查、確認寫入「BOM 明細」查已有 BOM、啟用/停用2.3 操作步驟:上傳並匯入
開 Tab 「匯入 BOM」。
「選擇 BOM Excel 檔案」 -->「選擇檔案」或「選擇資料夾」(可多選 .xlsx)。
確認「已選 N 個檔案」後,點 「上傳並檢查」。
進行中:「上傳與檢查中...」、進度「已檢查 x / y 個檔案...」。
檢視結果:
「正確 BOM 列表(可匯入)」
「問題 BOM 列表」
必要時勾選類型相關選項(如「飲料」「箱料粉」等)或用「搜索檔名」篩選。
確認無誤後點 「確認匯入」。
成功提示類似 「匯入完成」;亦可先「下載檢查結果 Excel」留底。
若要重來:點「返回重選檔案」。
2.4 常見錯誤(匯入)
畫面提示建議處理「請至少選擇一個 .xlsx 檔案」先選檔再上傳「上傳或檢查失敗,請稍後再試。」/伺服器 500檢查網路/檔案格式後重試檔名重複相關提示(_2、_3...)依提示整理檔名後重傳「匯入失敗,請查看主控台。」記下失敗檔,修正 Excel 後只重傳問題檔三、BOM 明細:查詢與啟停
3.1 搜索
Tab 「BOM 明細」。
「物品編號」/「物品名稱」-->「搜索」/「重置」。
多筆時:「找到多筆 BOM,請選擇一筆載入明細」(按鈕上會標「成品|半成品」「啟用|停用」)。
3.2 可改什麼
區塊說明「BOM 狀態」「啟用」/「停用」-->「儲存狀態」「基本資訊」產出數量、類型、過敏原、色深/浮沉/濃淡、時段、複雜度、基礎得分等(多為檢視)「材料清單」物品編號/名稱、基本/庫存/銷售數量與單位「製程與設備」製程檢視 注意: 完整欄位線上「編輯/儲存」目前前端關閉(SHOW_BOM_FULL_EDIT = false)。日常維護以 Excel 匯入 與 啟用/停用 為主。
3.3 何時「停用」
配方作廢、暫不允許再建工單時,將「BOM 狀態」改「停用」並「儲存狀態」。
「建立工單」的 BOM 下拉只列出啟用項目;停用後新單選不到該 BOM。
四、BOM 權重得分
4.1 入口
「設定」--> 「BOM 權重得分」(/settings/bomWeighting)
4.2 分頁
Tab用途「BOM 加權」調整各評分項目的「權重」(總和須=1)「BOM得分」查看各貨品「基礎得分」4.3 操作
「BOM 加權」-->「編輯」-->改「權重」-->「儲存」。
校驗失敗時:「權重必須為數字」或「權重總和必須等於 1(目前總和: x)」。
成功:「更新成功(已重新計算 N 筆 BOM 基礎分)」。
到「BOM得分」核對「貨品編號」「物品名稱」「基礎得分」。
五、BOM/物料單位問題(ADMIN)
5.1 入口
「設定」--> 「BOM / 物料單位問題」(僅 ADMIN;/settings/masterDataIssues)
頁標題常顯示:「BOM/貨品單位問題」
5.2 操作
Tab「BOM」或「貨品」。
「搜索」「類型」(「全部」「BOM 總表」「BOM 原材料」)。
「重新檢查」更新清單;「複製清單」方便貼到表單/郵件。
點列開詳情:「問題」「應為」「實際」-->「關閉」。
5.3 常見問題文案(節錄)
「BOM 編號為空」「BOM 名稱為空」
「BOM 產出單位與成品銷售單位不一致」
「BOM 原料銷售/基本/庫存單位與貨品主檔不一致」
「BOM 編號與關聯貨品不一致」
側欄紅點例:BOM N 筆 · 貨品 M 筆。空狀態:「目前沒有問題。」六、應用:建立工單時選 BOM
6.1 手動建立
「管理工單」-->「搜索工單/ 建立工單」-->「建立工單」。
「BOM」(必填)選成品/半成品;同名時可能標「(成品)」「(半成品)」。
「標準生產數」x「批數」=「需求數量」(單位來自 BOM 產出 UOM)。
「預計生產日期」等填妥 -->「建立」。
成功常提示「成功更新資料」。未選 BOM 時可能出現「請選擇 BOM」/Bom required!。
6.2 排程自動生成
「排程」-->「生產排程」-->「詳細」。
「自動生成工單」(僅處理具 BOM 的物料;說明文案類似「選擇日期範圍(僅含具 BOM 的物料...)」)。
舊細排頁另有「生成工單」「查看 BOM」(材料表含「編號」「名稱」「可用數量」「需求數量」)。
詳見《MTMS 排程與工單 使用說明》。七、權限與路徑速查
畫面路徑備註匯入 BOM/settings/importBom「設定」下BOM 權重得分/settings/bomWeightingBOM/物料單位問題/settings/masterDataIssuesADMIN建立工單/jo選 active BOM排程產單/ps需 BOM八、常見問題速查
情況建議建工單下拉找不到某成品到「BOM 明細」確認是否「啟用」;或尚未匯入排程產單跳過某物料該物料可能無 BOM/不在排期 BOM 範圍單位對不上、品檢/提料異常ADMIN 開「BOM / 物料單位問題」對照主檔後修正 Excel 再匯入權重儲存失敗確認各權重為數字且總和=1

+ 149
- 0
docs/exports/MTMS_DO_Shipping_UserGuide.docx 파일 보기

@@ -0,0 +1,149 @@
MTMS 使用說明:送貨訂單 --> 放單 --> 成品出倉
本手冊依目前前端畫面與繁中文案整理,按鈕/選單名稱以畫面上「」內文字為準。
適用範圍:側欄「送貨訂單」、倉庫「成品出倉」(撳單/掃碼出倉/列印標籤)、加單與車線-X。
「一附」章節使用本機資料庫 fpsmsdb 的真實例子(查詢當下快照)。
產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。
這份手冊怎麼用
符號意思「......」畫面上看得到的按鈕、選單、標題-->下一步操作角色分工(白話):
角色動作主要畫面看單、放單、加單、補貨「送貨訂單」/do撳單、掃碼提貨、列印 DN/標籤「成品出倉」/doworkbench調整提料順序等(ADMIN)「成品出倉管理」 「放單」!=「撳單」:放單=由送貨訂單產生提料票;撳單=倉庫領取該票開始出倉。
建議閱讀順序:送貨訂單篩選 --> 放單 --> 撳單 --> 掃碼出倉 --> 填箱數列印。一、整體流程
【送貨訂單 /do】篩選「2/F」「4/F」「車線-X」「加單」
↓ 「批量放單」或詳情「放單」
【產生提料單/提票】狀態進入待撳單
【成品出倉 /doworkbench】「撳單/提料單詳情」
↓ 選日期/批量|單量/樓層票 --> 點車線「確認分配」
【掃碼提料】「開始QR掃描」-->「提交所有已掃描項目」
【成品提貨記錄】輸入「箱數」-->「列印送貨單標籤」等
【查看提貨情況】核對「已完成」一附、本地資料庫實例
DO「來貨狀態」:pending-->「待處理」、receiving-->「接收中」、completed-->「已完成」。
提票在「查看提貨情況」:pending-->「待撳單」、released-->「提貨中」、completed-->「已完成」。
例 A:待放單/待出貨的送貨訂單(2026-07-30)
本機「預計送貨日期」2026-07-30 仍有多張「待處理」,例如:「門店訂單編號」「來貨狀態」店鋪TOUR03PO26070304待處理UR03TOCF28PO26070199待處理CF28TOCF28PO26070200待處理CF28TOCF02PO26070176待處理CF02TOCF18PO26070194待處理CF18怎麼練:
「送貨訂單」--> 選樓層分頁 -->「預計送貨日期」填 2026-07-30 --> 搜索。
找上表編號點「詳情」看行項與「庫存可用」。
測試庫才建議真的「放單」/「批量放單」(會產生提料票)。
例 B:近一週 DO 狀態量級(本機 2026-07-28~08-05)
「來貨狀態」約筆數使用者常做待處理1,425篩選後「批量放單」已完成1,059查歷史/補貨原單接收中96出倉進行中對應例 C:提料票(撳單前「待撳單」風格樣本)
本機較早提票例(do_pick_order,狀態 pending≈待撳/待處理):「提票號碼」車線資訊店鋪「需求日期」放單類型TI-S-20260504-4F-001P06B_Sat_區1_港島東MC492026-05-04single(單量)TI-S-20260504-2F-001車線-F1MC492026-05-04singleTI-S-20260504-2F-001車線-XHP652026-05-04singleTI-S-20260505-4F-002P06B_Tue_區5_九龍中HP152026-05-05single怎麼練:「成品出倉」-->「撳單/提料單詳情」--> 日期選對應「是日/翌日...」;或「成品提貨記錄(全部)」用「提票號碼」搜索。注意「車線-X」會獨立分組。
自行查核用 SQL(選用)
SELECT d.code, d.status, DATE(d.estimatedArrivalDate) AS eta, s.code AS shop
FROM delivery_order d
LEFT JOIN shop s ON s.id = d.shopId
WHERE d.deleted = 0 AND DATE(d.estimatedArrivalDate) = '2026-07-30'
ORDER BY d.id DESC
LIMIT 20;

SELECT ticket_no, TruckLanceCode, ticket_status, ShopCode,
DATE(RequiredDeliveryDate) AS req_date, release_type
FROM do_pick_order
WHERE deleted = 0 AND RequiredDeliveryDate >= '2026-05-01'
ORDER BY id DESC
LIMIT 20;二、送貨訂單(/do)
2.1 入口與分頁
側欄 「送貨訂單」。
分頁用途「2/F」「4/F」依樓層票別看/放單「車線-X」無匹配車線或歸入 X 的訂單「加單」isExtra 加單;批量放單可合併「補貨」已完成原單補到目標單2.2 搜索欄
「門店訂單編號」「店鋪名稱」「車線號碼」「預計送貨日期」「來貨狀態」
狀態選項:「待處理」「接收中」「已完成」(及「全部」)
注意:「已填寫車線號碼時,請一併選擇預計送貨日期後再搜索。」/「需選擇預計送貨日期」
2.3 結果表常見欄
「詳情」「門店訂單編號」「店鋪名稱」「供應商名稱」「車線號碼」「訂單日期」「預計送貨日期」「來貨狀態」。2.4 詳情頁(/do/edit?id=)
標題:「編輯送貨訂單詳情」
動作:「放單」「提料單分配」「分配2/F」「分配4/F」「放單2/F」「放單4/F」「返回」
行表:「商品編號」「貨品名稱」「數量」「庫存可用」「庫存狀態」
單張成功提示:「送貨訂單放單成功!提料單已建立。」
三、放單詳解
3.1 批量放單(常用)
在「送貨訂單」搜出目標日/樓層的列,勾選需要的店(可取消勾選排除)。
點 「批量放單」。
對話框顯示「已選擇店舖數量: N」;「確認」執行。
「加單」分頁額外選項:
「確認合併放單」(合併同車線 --> TI-M- 合併票;文案含「合併同車線送貨訂單(TI-M- 合併票)」)
「確認不放合併放單」
成功:「已完成批量放單」。
Workbench 路徑按鈕亦可能顯示為「批量放單」(鍵名 Workbench Batch Release)。3.2 單張放單
詳情頁「放單」,或先「分配2/F/4/F」再「放單2/F/4/F」。3.3 放單前/失敗常見提示
提示處理「沒有選擇送貨訂單進行批量放單...」先勾選列「車線可用性警告」「問題送貨訂單」核對目標日是否有車線;或走「車線-X」「放單提料單失敗,請稍後再試。」稍後重試;查該店是否已放過四、成品出倉(/doworkbench) -- 主路徑
4.1 入口
「倉庫管理」--> 「成品出倉」(現行主選單指向 /doworkbench)。
舊頁 /finishedGood 標題同為「成品出倉」,一般以 Workbench 為準。
4.2 頁頂打印機
「A4 打印機」「標籤打印機」「列印全部草稿 (N)」
未選機:「請先選擇打印機」/「請先選擇標籤打印機」
4.3 分頁一覽
tab標籤0「撳單/提料單詳情」1「加單」(徽章:當日未完成加單票數)2「成品提貨記錄」3「成品提貨記錄(全部)」4「查看提貨情況」5「成品出倉出箱數量」6「送貨路線摘要」五、撳單(領票)
5.1 條件列
「請選擇日期」:「是日」「翌日」「後日」
「放單類型」:「批量」「單量」
「票別(樓層)」:「2/F 票」「4/F 票」
5.2 車線面板
車線按鈕顯示「(未撳數/總單數)」、裝載序/出發時間等。
點車線 -->「確認分配」(含「位置」「車線號碼」「裝載順序」「出發時間」「所需日期」「可用訂單」)-->「確認」。
成功後進入提料明細掃碼。
無單時:「該樓層未有需處理訂單」/「此樓層沒有可用的提料單」。
「未完成提料單」可搜商店/車線/送貨單編號再「選擇」。
限制:「請先完成目前的提料單,再提取下一張」。
5.3 「車線-X」
DO 與出倉皆有獨立「車線-X」區塊;無匹配車線時顯示「車線-X」。
「以前」:今日前未完成的車線-X。
出箱儀表會統計「車線-X 出箱數」。
六、掃碼出倉(提料執行)
在已撳單的明細中看「所有提料單批號」「進度」。
「開始QR掃描」/「停止QR掃描」;「掃描結果」正確時「二維碼驗證成功。」
可「改數」「提交數量」;問題回報含不良/遺失等。
「提交所有已掃描項目」;可選「列印空白頁數標籤」。
無掃碼可直接完成的列可用「已完成」(Just Completed)。
全部完成後通常導向「成品提貨記錄」(帶提票號)。
掃碼常見錯誤
提示處理「二維碼不符合當前訂單中的任何貨品。」確認掃的是本票貨品「此批號不可用...」「此批次尚未上架」換批或先完成上架「此批號單位不符...」「此批號已提貨...」換批「掃描批號已過期...」換未過期批「此批次貨品已被其他送貨單留起...」換批或協調留貨換批雙掃說明依畫面再掃一次確認七、加單專章
7.1 在「送貨訂單」
開「加單」分頁搜索與「批量放單」。
合併選項見 §3.1(TI-M- 合併票)。
7.2 在「成品出倉」
Tab「加單」;進入前確認:「進入加單檢視?」
說明:「加單檢視會依選定日期,將 isExtra 票依店鋪與車線顯示。」
「目前是加單票,顯示與操作已切換為加單模式。」/「離開加單檢視」「返回一般指派分頁」。
「合併加單提料單」:僅「未分配」且同店鋪、樓層(2/F、4/F 或車線-X)、車線、出發時間可合併。
類型顯示可能為「合拼單」「加單」「批量」「單量」。
八、補貨(「送貨訂單」-->「補貨」)
「補貨填表」「對單」-->「待提交列表」-->「提交」/「清空」。
「送貨單號末四位」「原送貨單」「目標送貨單」「補貨數量」「原出貨數」「車線」。
無車線時畫面可能顯示「車線-X」。
「補貨進度追蹤」:待處理/處理中/已完成。
限制例:「只有已送貨(completed)的送貨單可作為原送貨單。」「補貨數量必須大於零」。
九、箱數與列印
9.1 草稿/空白
「列印全部草稿 (N)」--> 確認「確認列印全部草稿?(總數量:N份)」-->「成功列印」。
提料中:「列印空白頁數標籤」-->「請輸入要列印的標籤數量:」。
9.2 完成後正式列印(「成品提貨記錄」)
「查看詳情」或列表動作。
「列印提料單」「列印送貨單標籤」「列印提料單和送貨單標籤」「補印標籤」。
彈窗「請輸入總箱數」,欄位「箱數」(>=1)。
成功:「成功列印」。
9.3 補印
「補印送貨單標籤」:「起始箱號」「結束箱號」「總箱數」
校驗:起始>=1、結束>=起始、結束<=總箱數等。
9.4 送貨路線摘要
Tab「送貨路線摘要」--> 選「車線」-->「下載報告 (PDF)」。
若未執完:「此車線仍有 N 張訂單未執拾。是否仍要列印 / 下載送貨路線摘要?」
9.5 出箱數量
Tab「成品出倉出箱數量」:按日「2/F 出箱數」「4/F 出箱數」「車線-X 出箱數」「總出箱數」(來自完成時填的箱數)。
十、查看提貨情況與管理動作
10.1 查詢
「目標日期」「重新載入」「樓層」「狀態」(「待撳單」「提貨中」「已完成」)。
欄含貨車/車線/裝載順序/提票號碼/負責員工/訂單項目數量等。
10.2 管理(常需 ADMIN)
動作意義(畫面說明意涵)「撤銷領取」清空負責人,單據回待分配,他人可再領「強制完成提貨單」標完成並歸檔,不改已揀數量;適用已全部提交但系統未結案未授權:「僅管理員(ADMIN 權限)可使用」。
十一、狀態對照(避免搞混三套名稱)
11.1 送貨訂單「來貨狀態」
鍵畫面pending「待處理」receiving「接收中」completed「已完成」(部分流程)released / picking「已放單」/「提料中」11.2 「查看提貨情況」提票狀態
鍵畫面白話pending「待撳單」已放單、尚未領取released「提貨中」已撳單/出倉中completed「已完成」提貨完成11.3 提料單通用(pickOrder)
「待處理」「已放單」「提料中」「已完成」 -- -- 與上表用詞接近但場景不同;操作時以目前所在分頁的 Chip 文案為準。十二、成品出倉管理(ADMIN)
「倉庫管理」-->「成品出倉管理」(/finishedGood/management)。
「提料順序」:上移/下移/置頂/置底、「新增物品」「儲存」「重新載入」。
「出貨倉位」「入貨倉位」等主檔維護。
十三、路徑速查
畫面路徑送貨訂單/doDO 詳情/放單/do/edit?id=成品出倉(主)/doworkbench成品出倉管理/finishedGood/management舊成品出倉/finishedGood(附錄對照用)十四、常見問題速查
情況建議批量放單後倉庫看不到票核對日期「是日/翌日」、樓層票別、批量/單量、是否加單檢視車線按鈕 0 單換日期/樓層;查「車線-X」「未完成提料單」掃碼一直失敗批號是否上架、是否被留貨/過期/單位不符印不出標籤先選 A4/標籤打印機;完成後記得填「箱數」加單與正單混在一起明確進/出「加單」檢視;合併規則要同店同線同時段畫面突然英文少數錯誤字串尚未進 zh,以實機為準並回報補譯

+ 105
- 0
docs/exports/MTMS_JO_Pick_Production_PutAway_UserGuide.docx 파일 보기

@@ -0,0 +1,105 @@
MTMS 使用說明:工單提料/報工/上架
本手冊依目前前端畫面與繁中文案整理,按鈕/選單名稱以畫面上「」內文字為準。
適用範圍:放單之後的「工單提料」「工單生產流程」「上架掃碼」(品檢/上架)。
建單與排程請先看《MTMS 排程與工單 使用說明》;BOM 請看《MTMS BOM 使用說明》。
「一附」章節使用本機資料庫 fpsmsdb 的真實例子(查詢當下快照)。
產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。
這份手冊怎麼用
符號意思「......」畫面上看得到的按鈕、選單、標題-->下一步操作建議閱讀順序:
放單(「搜索工單/ 建立工單」)
提料(「工單提料」)
對料 --> 工序 --> 完成工單(「工單生產流程」)
品檢 --> 上架(「品檢」+「上架掃碼」)
一、整體流程
【/jo】「規劃中」──「放單」──►「待處理」/「提料中」
【/jodetail】「工單提料」──「查看詳情」──► 掃碼提料 ──「提交」/「提交所有已掃描項目」
【/productionProcess】「工單生產流程」
├─(可選)「工單對料」--> 二次掃碼 -->「確認所有提料」
├─「查看詳情」--> 各工序「開始」/「訂單完成」或「已完成」(Just Pass)
「完成工單」(確認:「確認要完成此工單嗎?」)
「品檢」-->「確定品檢結果」(可列印/下載 QR)
【/putAway】「上架掃碼」:掃貨品 QR --> 掃倉庫 QR -->「確定及上架貨物」
「已上架工單」;工單「已完成」一附、本地資料庫實例
狀態中文:pending-->「待處理」、packaging/picking-->「提料中」、processing-->「生產中」、storing-->「待品檢入倉」、completed-->「已完成」。
例 A:適合練提料(2026-07-29)
「工單編號」「狀態」成品「需求數量」建議下一步JO-260729-038待處理PP2277 烚意粉51「工單提料」開單掃碼JO-260729-008提料中PP2257 咖哩汁箱料粉1繼續提交提料JO-260729-013提料中PP1043 豆豉汁(2磅/包)309繼續提交提料JO-260729-044提料中PP2383 辣椒菜脯265繼續提交提料JO-260729-034生產中PP2302 酸甜蘿蔔粒箱料粉1「工單生產流程」做工序/完成工單怎麼練:
「搜索工單/ 建立工單」-->「預計生產日期」2026-07-29 -->「搜索」。
「狀態」篩「待處理」找 JO-260729-038,或直接到「工單提料」找同日卡片。
「生產中」單到「工單生產流程」練「查看詳情」/「完成工單」(完成會改資料,請用測試庫)。
例 B:狀態與畫面入口對照
「狀態」常用入口待處理/提料中「工單提料」生產中「工單生產流程」-->「工藝流程」待品檢/待品檢入倉「工單生產流程」-->「品檢」或「待QC上架工單」已完成「已上架工單」/工單搜索「已完成」自行查核用 SQL(選用)
SELECT jo.code, jo.status, i.code, i.name, jo.reqQty
FROM job_order jo
LEFT JOIN bom b ON b.id = jo.bomId
LEFT JOIN items i ON i.id = b.itemId
WHERE jo.deleted = 0 AND DATE(jo.planStart) = '2026-07-29'
AND jo.status IN ('pending','packaging','processing','storing')
ORDER BY jo.status, jo.code
LIMIT 20;二、放單(前置,在「搜索工單/ 建立工單」)
側欄「管理工單」-->「搜索工單/ 建立工單」(/jo)。
找到「規劃中」工單 -->「放單」或「放單 (N)」。
放單後狀態變「待處理」/「提料中」,即可去「工單提料」。
詳情內可見庫存摘要字樣如「可提料項目數量:」「未能提料項目數量:」。
亦可在生產流程詳情內對仍屬規劃中的單按「放單」。
三、工單提料(/jodetail)
3.1 入口與分頁
側欄「管理工單」--> 「工單提料」;頁標題「工單提料」。
Tab文案0「工單提料詳情」1「已完成工單記錄」2「物料提料狀態」3「膠茜數目使用數量」3.2 「工單提料詳情」列表
品類:「全部」「飲料」「箱料粉」「其他」
樓層:「2F」「3F」「4F」「沒有批號」等
卡片常見:「工單」「批號」「提料單」「物品名稱」「需求數量」、狀態 Chip
點 「查看詳情」 進入掃碼提料
3.3 掃碼提料步驟
點「開始掃碼」(可「停止掃碼」)。
掃描物料/批號 QR;必要時開「批號QR碼掃描」或「手動輸入」-->「提交」。
畫面上應出現「QR碼驗證成功。」/「驗證成功!」;進度見「掃碼結果」「提交數量」。
單行「提交」或一次「提交所有已掃描項目」(進行中「提交中...」)。
完成後「返回列表」。
3.4 「已完成工單記錄」
「查看詳情」「打印版頭紙」(2F/3F/4F)、「打印數量」「打印機」
「對料狀態」:「對料待處理」/「對料已完成」
提示語例:「工單已完成提料和對料」
3.5 提料常見錯誤
提示處理「此批次已拒收,請掃描另一個批次。」換批「掃描的批次已被其他用戶完全提料。請掃描其他批次。」換可用批「物品數量不足」/數量大於需求/可用量改「提交數量」「請先選擇打印機」列印版頭紙前先選機四、工單生產流程(/productionProcess)
4.1 入口與頂層分頁
側欄「管理工單」--> 「工單生產流程」。
Tab文案0「工藝流程」1「待QC上架工單」2「已上架工單」3 - 6各類「儀表板 - ...」4.2 「工藝流程」卡片動作
按鈕用途「查看詳情」進工序/BOM/對料等「工單對料」提料完成後二次掃批號確認「完成工單」整張 JO 完工(確認:「確認要完成此工單嗎?」;權限常限 ADMIN)「品檢」開品檢 Modal(條件滿足且有入庫行時才出現)詳情內 Tabs:「工單信息」「BOM 材料」「工藝流程」「工藝明細」「工單對料」;另有「返回列表」「取消工單」「刪除工單」等。
4.3 工單對料(二次掃)
點「工單對料」。
對已提料批再掃「批號QR碼掃描」(或「手動輸入」)。
「驗證成功!」後,全部核對完點 「確認所有提料」。
「返回列表」可能解除指派,勿中途亂退。
4.4 工序報工(單步)
「查看詳情」-->「工藝流程」/「工藝明細」。
待處理工序點 「開始」 --> Dialog「掃描操作員和設備」。
「開始掃碼」:先操作員/員工,再設備 -->「提交並開始」。
執行中可「暫停」/「繼續」(「暫停原因」);結束該步用 「訂單完成」(填「工序產出」「不良品」「損耗」)。
若允許略過執行:按鈕「已完成」(Just Pass),確認「確認要通過此工序嗎?」。
勿混淆: 工序「訂單完成」=結束一步;卡片「完成工單」=結束整張工單。
4.5 品檢
條件滿足後點「品檢」。
Modal 常見 Tab:「處理來貨及品檢」/「來貨及品檢詳情」。
填結果後 「確定品檢結果」。
可「打印機」「列印數量」「列印」「下載QR碼」(供之後上架掃碼)。
校驗例:「請決定品檢結果」「有未完成品檢項目」「請輸入不合格數量」「請輸入到期日!」。
亦可從 Tab「待QC上架工單」或提醒鈴深連結進入。五、上架掃碼(/putAway)
5.1 入口
側欄「倉庫管理」--> 「上架掃碼」;頁標題「上架」。
5.2 兩段掃碼
待機:「等待掃瞄中,請掃瞄貨品二維碼開始上架程序」。
掃貨品/來貨行 QR --> 開 Modal。
填「是次上架數量」;再掃倉庫 QR(「掃瞄倉庫二維碼」/「請掃瞄倉庫二維碼」)。
點 「確定及上架貨物」。
可在「是次上架記錄」核對。
失敗:「讀取不成功,請重新掃瞄」;數量:「上架數量不得大於 ...」「最小為1」等。入庫行狀態語意:「待上架」-->「已部分上架」-->「已上架」。完成後可在「已上架工單」看到。六、狀態對照(操作頁為準)
代碼畫面planning「規劃中」pending「待處理」packaging / picking「提料中」processing「生產中」pendingQC「待品檢」storing「待品檢入倉」completed「已完成」cancelled「已取消」工序行:「待處理」-->「進行中」-->(可「已暫停」)-->「完成」/「已完成」(Pass)。
七、常見問題速查
情況建議提料頁找不到單確認已「放單」;日期/樓層/品類篩選是否過窄對料按鈕灰/沒有提料單未完成、已指派他人、或對料已完成「完成工單」按不到權限或工序未齊;確認提示「確認要完成此工單嗎?」沒有「品檢」按鈕尚未完成工單/無 stock-in 行上架掃不到先品檢並列印/下載 QR;或用 ?stockInLineId= 深連結掃碼驗證失敗換批、確認未拒收、確認單位/可用量八、路徑速查
畫面路徑搜索/放單/jo工單提料/jodetail工單生產流程/productionProcess上架掃碼/putAway

+ 67
- 0
docs/exports/MTMS_M18_DATA_MAPPING.docx 파일 보기

@@ -0,0 +1,67 @@
MTMS (FPSMS) <--> M18 資料對照手冊
本文件說明 MTMS / FPSMS 與 M18 之間的主檔與交易對應、同步方向,以及已知陷阱。區塊維護方式本手冊(說明、流程、陷阱)人手維護docs/generated/ 對照表腳本產生(見下方)重新產生自動表(改完 enum / mapping 後請跑):
python scripts/generate_m18_mapping_docs.py1. 系統與名詞
名稱說明MTMS / FPSMS本後端 FPSMS-backend + 前端 FPSMS-frontendM18外部 ERP/主檔與採購/送貨來源系統PullM18 --> MTMS(product / vendor / unit / currency / BOM / business unit / PO / DO)PushMTMS --> M18(例如 GRN、BOM for shop)設定入口:m18/M18Config.kt(m18.config.)、scheduler 見 application.yml / application-prod.yml(scheduler.m18Sync、scheduler.m18Grn)。
主程式目錄:src/main/java/com/ffii/fpsms/m18/。2. Master API 類型(StSearchType)
完整表見自動產生檔:--> generated/m18-stsearch-types.md摘要:M18 stSearchMTMS 落點proitemsvenshop(type=supplier)virDeptshop(type=shop)unituom_conversion(+ cunit)curcurrencyudfbomforshopbom / materials實作:M18MasterDataService。
3. 貨品類型(最常查)
3.1 同步規則(自動表)
--> generated/m18-item-type-mapping.md程式:M18MasterDataService.saveProduct / saveProducts 依 pro.udfProducttype:Consumable Material --> consumables
Non-consumable Material --> non-consumables
Product --> fg
WIP --> sfg
Item --> item
(其他,含 CMB) --> mat <-- defaultEnum 定義:modules/master/web/models/NewItemRequest.kt(ItemType、M18ItemType)。3.2 UI 顯示(存貨)
存貨 Type 欄:t(itemType),翻譯在 FPSMS-frontend/src/i18n/zh/inventory.json。items.type存貨頁(zh)mat原料fg成品sfg / wip半成品consumables / cmb消耗品non-consumables / nm非消耗品/雜項 系統 沒有「產品」這個 items.type。M18 的 Product 對應 MTMS fg(成品)。
3.3 可手動改嗎?
可以:Settings --> Items --> Edit --> Type(ProductDetails.tsx:fg / wip / mat / cmb / nm)。注意:之後若再跑 product sync,type 會依 M18 udfProducttype 覆寫(含再次落到 mat)。3.4 已知陷阱:CMB
M18 實務上可出現 "udfProducttype": "CMB"(例如蔗糖水 MG1852)。"CMB" != "Consumable Material"
也不等於前端的 cmb
--> sync 走 else --> mat --> 存貨顯示 原料
若要顯示消耗品:需改 mapping(例如把 CMB 對到 consumables),或在 M18 改成已支援的字串;僅手動改 MTMS 可能被下次 sync 蓋掉。4. 供應商與店鋪
方向M18MTMSPull vendorsvenshop,ShopType.SUPPLIER(supplier)Pull business unitsvirDeptshop,ShopType.SHOP(shop)鍵:shop.m18Id、shop.code。名稱優先 descZhTW --> descZhCN --> desc。
ShopType:modules/master/enums/ShopType.kt。5. 單位(UoM)
M18MTMSUnit master (unit)uom_conversion(code、udfudesc、udfShortDesc、m18Id...)Cunit 明細M18CunitService.replaceForUnitItem 級採購/庫存/銷售單位在 sync product price 時寫入 item_uom(見 M18MasterDataService product 區塊)。
PO/DO 行常同時保留:欄位意義qty / uomIdMTMS 業務單位(例如採購單位換算後)qtyM18 / uomIdM18M18 原始單位數量PO 換算邏輯見 M18PurchaseOrderService(convertQtyToPurchaseQty)。
6. 貨幣、BOM
M18MTMSServiceCurrencycurrencysaveCurrenciesBOM (udfbomforshop)bom / bom_materialsaveBomsShop BOM 回寫 M18:M18BomForShopService(push)。
7. 交易文件(摘要)
文件方向MTMS 主表筆記POM18 --> MTMSpurchase_order / purchase_order_linem18Id / data log;qty 可能換算DOM18 --> MTMSdelivery_order / delivery_order_line含 qtyM18、uomIdM18GRNMTMS --> M18stock-in --> M18 GRN API部分 m18CreatedUId 不送 GRN(見下)GRN 略過規則
m18/M18GrnRules.kt:M18 PO createUid備註行為2569legato不 post GRN2676xtech不 post GRN8. Config 鍵(對照時常用)
見 M18Config / application-.yml:m18.config.seriesId.pp|pf|sc|se|sf|sr
m18.config.beId.pp|pf|toa
m18.config.supplier-not.material-po
m18.config.supplier.shop-po / oem-po
scheduler.m18Sync.enabled
scheduler.m18Grn.createEnabled
9. 驗證用 SQL 範例
-- 某貨品目前 type(決定存貨顯示)
SELECT code, name, type, m18Id, m18LastModifyDate
FROM items
WHERE deleted = 0 AND code = 'MG1852';

-- 統計 type 分佈
SELECT type, COUNT(*) AS cnt
FROM items
WHERE deleted = 0
GROUP BY type
ORDER BY cnt DESC;若 M18 回傳 udfProducttype 可與上表比對;對不上表中「exact string」者皆會變 mat。10. 維護約定
改 mapping:先改 Kotlin enum / when,再跑 python scripts/generate_m18_mapping_docs.py,把 docs/generated/ 一併 commit。
改說明/陷阱:只改本檔,勿手改 docs/generated/。
新發現的 M18 值(如新的 udfProducttype):記入 generated 腳本的 KNOWN_UNMAPPED_M18_VALUES,或補正式 mapping 後重生。
PR 若動到 NewItemRequest.kt / M18MasterDataService product type 分支,review 應檢查 generated docs 是否已更新。
11. 相關程式索引
主題路徑Item / M18 type enumsmodules/master/web/models/NewItemRequest.ktProduct syncm18/service/M18MasterDataService.ktStSearchm18/model/M18MasterDataRequest.ktShop typemodules/master/enums/ShopType.ktGRN skipm18/M18GrnRules.ktPO syncm18/service/M18PurchaseOrderService.ktDO syncm18/service/M18DeliveryOrderService.ktBOM-->M18m18/service/M18BomForShopService.kt存貨 Type 顯示InventoryTable.tsx + i18n/zh/inventory.json物品 Type 下拉CreateItem/ProductDetails.tsx
M18 udfProducttype --> MTMS items.type
_Generated: 2026-08-01 10:01 UTC_Source of truthEnums: NewItemRequest.kt --> ItemType, M18ItemType
Sync: M18MasterDataService.saveProduct / saveProducts (when (pro.udfProducttype))
UI labels (inventory): FPSMS-frontend/src/i18n/zh/inventory.json
Sync mapping
M18 udfProducttype (exact string)M18ItemTypeMTMS items.typeItemTypeInventory UI (zh)Consumable MaterialCONSUMABLESconsumablesCONSUMABLES消耗品Non-consumable MaterialNONCONSUMABLESnon-consumablesNONCONSUMABLES非消耗品ProductFGfgFG成品WIPSFGsfgSFG半成品ItemITEMitemITEM貨品(any other value / empty) -- matMATERIAL原料Enum inventories
M18ItemType
ConstantString valueUsed in sync when?CONSUMABLESConsumable MaterialyesNONCONSUMABLESNon-consumable MaterialyesFGProductyesSFGWIPyesITEMItemyesItemType (MTMS stored values)
Constantitems.typeInventory UI (zh)MATERIALmat原料CONSUMABLESconsumables消耗品NONCONSUMABLESnon-consumables非消耗品FGfg成品SFGsfg半成品ITEMitem貨品Known gaps (not auto-mapped)
M18 value seenEffectNotesCMB--> mat (else)Seen on M18 pro.udfProducttype (e.g. MG1852). Falls through to mat.Frontend Settings --> Items edit also offers cmb / wip / nm as local types;
those are not written by the current M18 udfProducttype mapper.Regenerate
python scripts/generate_m18_mapping_docs.pyM18 StSearchType (master list APIs)
_Generated: 2026-08-01 10:01 UTC_Source: m18/model/M18MasterDataRequest.ktConstantstSearch valueTypical MTMS sync targetPRODUCTproitems (+ item_uom via prices)VENDORvenshop (type=supplier)CUSTOMERcus(enum present; sync usage varies)UNITunituom_conversion (+ m18 cunit)CURRENCYcurcurrencyBOMudfbomforshopbom / bom_material (udfbomforshop)BUSINESS_UNITvirDeptshop (type=shop)

BIN
docs/exports/MTMS_M18_DATA_MAPPING.xlsx 파일 보기


+ 177
- 0
docs/exports/MTMS_Schedule_JobOrder_UserGuide.docx 파일 보기

@@ -0,0 +1,177 @@
MTMS 使用說明:排程 --> 開工單
本手冊依目前前端畫面與繁中文案整理,按鈕/選單名稱以畫面上「」內文字為準。
適用範圍:側欄「排程」、管理工單(搜索/建立、提料、生產流程)。
「一附」章節使用本機資料庫 fpsmsdb 的真實例子(查詢當下快照);其他環境請改日期再對。
產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。
這份手冊怎麼用
符號意思「......」畫面上看得到的按鈕、選單、標題-->下一步操作節點流程中的一個階段(狀態/畫面)建議閱讀順序:
先排期(側欄「排程」)
再放單/建工單(「管理工單」-->「搜索工單/ 建立工單」)
提料 --> 生產 --> 完成
一、整體流程(從排期到完工)
【排程】預測/查看排期
↓ 「自動生成工單」 或 手動「建立工單」
【規劃中】工單已建立、尚未放單
↓ 「放單」
【待提料/提料中】
↓ 「工單提料」掃碼提交
【生產中】「工單生產流程」各工序開始/完成
↓ 「完成工單」
【品檢/上架】「待QC上架工單」-->「已上架工單」也可不經排程,在「搜索工單/ 建立工單」直接「建立工單」(手動工單)。一附、本地資料庫實例(方便對照畫面)
以下數字來自本機 fpsmsdb 查詢結果,用於說明「畫面上大概會看到什麼」。
不同環境/日期資料會不同;請用「搜索」條件改成你們當天日期再核對。
狀態中文依前端翻譯:planning-->「規劃中」、pending-->「待處理」、packaging-->「提料中」、processing-->「生產中」、storing-->「待品檢入倉」、completed-->「已完成」。
例 A:已有排期、尚未產工單(適合練習「詳細」-->「自動生成工單」)
在「生產排程」用「生產日期」搜 2026-08-03,本機有一筆細排(production_schedule.id = 928,type = detailed):畫面概念本機資料「生產日期」2026-08-03「預計生產數」(約)17,334「成品款數」(約)47打開「詳細」後,明細列會類似(節錄):
「編號」「名稱」約「需求數量」約「存貨量」約需工單數PP1175鮮檸檬汁(P+4)1,4061,40074PP1224柚子蒜蓉汁140501PP0259牛肉水(2KG/包)415141PP2284油醋汁(1KG/包)28321PP2390熱情香果醬12164怎麼練:
「排程」--> 生產日期選 2026-08-03 -->「搜索」-->「詳細」。
對照上表成品是否出現在明細。
若環境允許,再試「自動生成工單」(會真正建 JO,請在測試庫操作)。
查詢當下:此排期尚未有透過 prodScheduleLineId 掛上的工單(適合示範「產工單前」)。
例 B:排期已產工單且已完成(歷史成功路徑)
本機較早一筆:2026-06-17 細排(約預計生產 17,421、成品款數 33),曾產生多張 type = detailed 工單,例如:「工單編號」「狀態」「需求數量」成品JO-260617-008已完成568PP1234 日式咖哩汁JO-260617-012已完成15PP2288 香水檸檬汁P+3JO-260617-015已完成600PP1136 白粥JO-260617-024已完成28PP2284 油醋汁(1KG/包)JO-260617-030已完成508PP2290 韓式豬軟骨怎麼練:
「搜索工單/ 建立工單」-->「預計生產日期」填 2026-06-17 -->「搜索」。
找上表工單編號,點「查看」看已完成工單長怎樣。
對照:這類工單來自排程 release(type 在庫為 detailed),不是手動「建立工單」的 manual。
例 C:手動「建立工單」(不經排程)
本機 2026-07-29 工單幾乎皆為手動(type = manual,且未掛排期行)。例子:「工單編號」「狀態」「需求數量」成品JO-260729-045待處理3PP2390 熱情香果醬JO-260729-038待處理51PP2277 烚意粉JO-260729-034生產中1PP2302 酸甜蘿蔔粒箱料粉JO-260729-044提料中265PP2383 辣椒菜脯怎麼練:
「預計生產日期」選 2026-07-29 -->「搜索」。
用「狀態」篩「待處理」--> 應能看到類似 JO-260729-045。
若該單仍「規劃中」,可練習「放單」;若已是「待處理」,可接「工單提料」。
同日狀態分佈(本機快照):約 17 張「待處理」、24 張「提料中」、4 張「生產中」 -- -- 正好對應「放單後 --> 提料 --> 生產」不同節點。例 D:近兩週工單狀態分佈(看流程卡在哪)
本機最近約 14 天(未隱藏工單)概況:「狀態」約筆數使用者下一步常做什麼已完成359可當完成範本「查看」待處理42「工單提料」提料中37繼續掃碼/提交提料待品檢入倉33「工單生產流程」品檢/上架生產中4「工單生產流程」繼續工序例 E:還在「規劃中」的單(適合練刪除/放單)
本機仍有例如:JO-260427-039(沙薑醬 PP2205,需求約 291,「規劃中」)。若只需練習「查看」--> 看「放單」「刪除工單」按鈕是否出現。
勿在正式/共用庫隨意刪除;測試庫才建議真的按「刪除工單」。
自行查核用 SQL(選用)
-- 某日排期摘要
SELECT id, DATE(produceAt) AS produce_date, type,
totalEstProdCount, totalFGType
FROM production_schedule
WHERE deleted = 0 AND DATE(produceAt) = '2026-08-03';

-- 該排期成品明細(前 20)
SELECT i.code, i.name, psl.prodQty, psl.stockQty, psl.needNoOfJobOrder
FROM production_schedule_line psl
JOIN production_schedule ps ON ps.id = psl.prodScheduleId
JOIN items i ON i.id = psl.itemId
WHERE ps.deleted = 0 AND psl.deleted = 0
AND DATE(ps.produceAt) = '2026-08-03'
ORDER BY psl.itemPriority, i.code
LIMIT 20;

-- 某日工單+狀態
SELECT code, status, type, reqQty, DATE(planStart) AS plan_date
FROM job_order
WHERE deleted = 0 AND (isHidden = 0 OR isHidden IS NULL)
AND DATE(planStart) = '2026-07-29'
ORDER BY status, code;二、排程(側欄「排程」)
2.1 入口
側欄點 「排程」
進入頁面標題:「生產排程」(路徑通常為 /ps)
說明:系統另有舊版「需求預測」「詳細排程」頁(/scheduling/...),目前側欄主入口是「排程」-->「生產排程」。以下以主入口為準。
2.2 畫面上常見按鈕
按鈕/功能用途(白話)「預測排期」依日期/天數計算產生預計排期「搜索」依「生產日期」等條件查已有排期「詳細」打開該筆排期的明細「自動生成工單」依此排期一次產生多張工單(在詳情裡)「關閉」關閉詳情視窗「排期設定」庫存/排期相關設定與匯入匯出「匯出計劃/物料需求Excel」匯出計劃與物料需求「匯出送貨單數量」匯出送貨單數量區間資料列表常見欄位:「生產日期」「預計生產數」「成品款數」等。
2.3 操作步驟:做出排期(Happy path)
進入 「排程」 --> 「生產排程」。
點 「預測排期」。
在對話框 「準備生成預計排期」 中填:
「開始日期」
「排期日數」
點 「計算預測排期」。
成功時畫面會提示類似 「成功計算排期!」;失敗會提示計算錯誤或不明狀況。
選擇「生產日期」後點 「搜索」,在列表找到該日排期。
點該列 「詳細」,打開 「排期詳細」。
確認內容無誤後,點 「自動生成工單」 --> 系統依排期建立工單。
點 「關閉」 結束。
2.4 節點說明(排程)
節點使用者在做什麼下一個常見動作尚未有排期進「生產排程」但列表空/無當日資料「預測排期」已有排期列表用「搜索」找日期「詳細」排期詳細已打開檢查預計生產內容「自動生成工單」已生成工單工單出現在「搜索工單/ 建立工單」去「放單」2.5 排程常見問題
情況建議處理「計算預測排期」失敗記下畫面錯誤訊息;檢查開始日期/天數;稍後再試或聯絡系統/IT「自動生成工單」失敗畫面可能顯示失敗訊息;確認排期內容是否完整、BOM/物料是否齊全找不到某日排期確認「生產日期」與「搜索」條件;必要時再跑一次「預測排期」想改數量再開工單主入口 /ps 詳情偏「一次自動生成」;若需逐行改量/發佈,需使用舊版「詳細排程」編輯頁(見附錄)2.6 附錄:舊版「詳細排程」/「需求預測」(進階)
若單位仍使用直連網址:畫面標題(約)重點按鈕需求預測列表「需求預測」「測試粗排」「搜索」「詳情」需求預測詳情「成品及物料需求預測詳情」多為檢視(依成品/依物料、「查看 BOM」)詳細排程列表「詳細排程」「詳細排程」(產生)、「匯出排程」「詳情」FG 生產排程「成品生產排程」/「FG 生產排程」列「發佈」、改「需求數量」後儲存、「生成工單」、「返回」注意(舊版細排詳情):
「生成工單」 常僅允許生產日期為今天;否則可能跳出英文提示(畫面未必有完整中文翻譯)。
前端沒有「從某一筆需求預測一鍵跳到對應詳細排程」的按鈕;兩條線在畫面上是分開的。
設定選單另有 「需求預測設定」(成品排除日、星期等),屬主檔設定,不是每日排期操作。三、工單:搜索/建立/放單
3.1 入口
側欄 「管理工單」 --> 「搜索工單/ 建立工單」頁面標題:「搜索工單/建立工單」3.2 畫面上常見按鈕
按鈕用途「建立工單」手動開一張新工單「放單」/「放單 (N)」將「規劃中」工單放出,進入後續提料「重置」清空搜尋條件「查看」開「工單詳情」「取消工單」取消後工單從列表隱藏(非規劃中等情況)搜尋條件常見:「工單編號」「成品/半成品名稱」「預計生產日期」~「預計生產日期至」「工單類型」「狀態」。
3.3 操作步驟:手動建立工單
點 「建立工單」,打開標題為 「建立工單」 的視窗。
填寫:
「BOM」
「標準生產數」(通常唯讀)
「批數」
「需求數量」(常由標準x批數自動帶出)
「工單類型」(選 BOM 後可能自動對應)
「生產優先序」(常見預設約 50,範圍約 1 - 100)
「預計生產日期」
可勾選「記住為預設日期」
點 「建立」。
成功後通常提示資料已更新;新工單多為 「規劃中」 狀態。
在列表勾選/找到該工單,點 「放單」(或上方「放單 (N)」批量)。
3.4 操作步驟:從排程來的工單
在「生產排程」詳情已按 「自動生成工單」(或舊版「生成工單」)。
到 「搜索工單/ 建立工單」,用「預計生產日期」等搜尋。
確認工單後執行 「放單」。
3.5 工單詳情(「查看」)
標題:「工單詳情」常見分頁/區塊:「工單信息」
「BOM 材料」
「工藝流程」/「工藝明細」
常見動作:按鈕何時可用(概念)說明「放單」多為「規劃中」放出工單「刪除工單」多為仍在「規劃中」確認後無法復原「取消工單」已非規劃中等確認後從列表隱藏;已上架通常不可取消「儲存」/「取消」編輯 Dialog存檔或放棄「返回列表」 -- 回搜尋頁庫存摘要常見字樣:「所需貨品項目數量:」「可提料項目數量:」「未能提料項目數量:」。
刪除確認:「確認刪除工單」
「確定要刪除此工單嗎?此操作無法復原。」
取消確認:「確認取消工單」
「確定要取消此工單嗎?工單將從列表中隱藏。」
按鈕:「取消工單」/「取消」(後者為放棄這次取消操作)
3.6 工單狀態(搜尋/列表常見)
畫面上可能出現(實際以該列顯示為準):狀態(常見中文)白話「規劃中」已建立,尚未放單「待處理」/待提料相關已放單,等提料「提料中」/「進行中」正在提料或進行中「已開始工序」/「生產中」已進入生產工序「成品入倉中」/「待品檢入倉」生產後待品檢/入倉「已上架」已完成上架「已取消」已取消篩選下拉亦可能見:「待處理」「提料中」「已開始工序」「成品入倉中」「已上架」「已取消」等。
3.7 工單建立/放單常見問題
情況建議處理「建立」沒反應檢查 BOM、批數、日期、工單類型是否已填;看是否有必填未選「放單」失敗記下提示;檢查是否仍為可放單狀態、物料/權限想刪掉剛建錯的單「規劃中」-->「查看」-->「刪除工單」--> 確認已放單但不想做了「取消工單」--> 確認隱藏;已上架通常不能取消批量「放單 (N)」部分失敗依成功/失敗筆數訊息,對失敗單再個別處理四、工單提料
4.1 入口
「管理工單」 --> 「工單提料」4.2 常見分頁
「工單提料詳情」
「已完成工單記錄」
「物料提料狀態」
(及其他與膠茜/用量相關分頁,以畫面為準)
4.3 操作步驟(概念)
在「工單提料詳情」找到已放單、待提料的工單。
點 「查看詳情」 進入提料執行畫面。
依畫面使用 「開始掃碼」、掃描物料/批號。
需要時點 「提交」/「提交所有已掃描項目」/「確認」。
完成後可在「已完成工單記錄」核對。
對料相關:生產流程中可能有 「工單對料」,並有 「確認所有提料」。4.4 提料常見問題
情況建議處理批號不符依 Dialog 提示按「確認」,改掃正確批號庫存不足/過期依畫面提示;先補貨或調批號,勿強行提交掃錯要重來用畫面上的「取消」/清除已掃項目(以當頁按鈕為準)後重掃提料狀態常見:「待提料」「已掃碼」「對料待處理」「對料已完成」;提料單「已放單」「已完成」。
五、工單生產流程
5.1 入口
「管理工單」 --> 「工單生產流程」5.2 常見分頁/區塊
「工藝流程」
「待QC上架工單」
「已上架工單」
「儀表板 - 工單狀態」等
5.3 工序操作(Happy path)
在「工藝流程」找到工單,點 「查看詳情」。
(可選)「工單對料」 確認物料。
各工序:
「開始」 開工序
可依畫面掃 操作員/設備(未掃可能提示「請先掃描操作員編號」「請掃描設備編號」)
可 「暫停」
「完成步驟」/「通過」/「已完成」(Just Pass 等,以畫面為準)
確認通過時可能問:「確認要通過此工序嗎?」
全部就緒後點 「完成工單」,確認:「確認要完成此工單嗎?」
需要時做 「品檢」,再到「待QC上架工單」完成上架,最後在「已上架工單」可見。
其他可能按鈕:「提交並開始」「提交包裝袋消耗」等。5.4 工序狀態(常見)
「待處理」「進行中」「已暫停」「完成」/「已完成」「通過」「未開始」「已停止」「已取消」等。5.5 生產常見問題
情況建議處理「完成工單」按不了通常尚有工序未完成或條件未滿足;先完成各步驟掃碼驗證失敗「驗證失敗. 請檢查操作員和設備.」--> 重掃正確編號API/系統錯誤「發生錯誤,請稍後再試。」--> 稍後重試或聯絡支援做到一半要停用「暫停」;勿與「取消工單」混淆(取消是整張工單層級)六、取消、刪除、返回 -- -- 怎麼選?
你想做的事建議用哪個按鈕結果建錯、還在規劃中,不要了「刪除工單」刪除,不可復原已放單/進行中,不要繼續「取消工單」確認後從列表隱藏只是關掉視窗、不改資料「關閉」/「取消」/「返回」「返回列表」不取消工單本身工序暫停一下「暫停」工單仍在,之後可再「開始」七、快速檢查清單(每日)
「排程」--> 需要時「預測排期」-->「搜索」-->「詳細」-->「自動生成工單」
「搜索工單/ 建立工單」--> 核對當日工單 -->「放單」
「工單提料」--> 掃碼提交
「工單生產流程」--> 各工序開始/完成 -->「完成工單」--> 品檢/上架
手動補單:同頁「建立工單」-->「建立」-->「放單」。八、文件維護說明(給內部)
文案來源:前端 i18n/zh/navigation.json、jo.json、schedule.json、productionProcess.json,以及「生產排程」頁硬編碼繁中。
主程式入口:
排程:FPSMS-frontend/src/app/(main)/ps/page.tsx
工單搜尋/建立:.../jo/page.tsx、JoWorkbenchSearch、JoCreateFormModal
提料:.../jodetail/
生產:.../productionProcess/
若按鈕改名或流程改版,請同步改本手冊,並重新匯出 Word。
重新匯出 Word(需已安裝套件):pip install python-docx
python scripts/export_user_guide_office.py產出:docs/exports/MTMS_Schedule_JobOrder_UserGuide.docx

+ 60
- 0
docs/generated/m18-item-type-mapping.md 파일 보기

@@ -0,0 +1,60 @@
<!-- AUTO-GENERATED by scripts/generate_m18_mapping_docs.py — do not edit by hand -->

# M18 `udfProducttype` → MTMS `items.type`

_Generated: 2026-08-01 10:01 UTC_

**Source of truth**

- Enums: `NewItemRequest.kt` → `ItemType`, `M18ItemType`
- Sync: `M18MasterDataService.saveProduct` / `saveProducts` (`when (pro.udfProducttype)`)
- UI labels (inventory): `FPSMS-frontend/src/i18n/zh/inventory.json`

## Sync mapping

| M18 `udfProducttype` (exact string) | `M18ItemType` | MTMS `items.type` | `ItemType` | Inventory UI (zh) |
|---|---|---|---|---|
| `Consumable Material` | `CONSUMABLES` | `consumables` | `CONSUMABLES` | 消耗品 |
| `Non-consumable Material` | `NONCONSUMABLES` | `non-consumables` | `NONCONSUMABLES` | 非消耗品 |
| `Product` | `FG` | `fg` | `FG` | 成品 |
| `WIP` | `SFG` | `sfg` | `SFG` | 半成品 |
| `Item` | `ITEM` | `item` | `ITEM` | 貨品 |
| *(any other value / empty)* | — | `mat` | `MATERIAL` | 原料 |

## Enum inventories

### `M18ItemType`

| Constant | String value | Used in sync `when`? |
|---|---|---|
| `CONSUMABLES` | `Consumable Material` | yes |
| `NONCONSUMABLES` | `Non-consumable Material` | yes |
| `FG` | `Product` | yes |
| `SFG` | `WIP` | yes |
| `ITEM` | `Item` | yes |

### `ItemType` (MTMS stored values)

| Constant | `items.type` | Inventory UI (zh) |
|---|---|---|
| `MATERIAL` | `mat` | 原料 |
| `CONSUMABLES` | `consumables` | 消耗品 |
| `NONCONSUMABLES` | `non-consumables` | 非消耗品 |
| `FG` | `fg` | 成品 |
| `SFG` | `sfg` | 半成品 |
| `ITEM` | `item` | 貨品 |

## Known gaps (not auto-mapped)

| M18 value seen | Effect | Notes |
|---|---|---|
| `CMB` | → `mat` (else) | Seen on M18 pro.udfProducttype (e.g. MG1852). Falls through to mat. |

Frontend Settings → Items edit also offers `cmb` / `wip` / `nm` as local types;
those are **not** written by the current M18 `udfProducttype` mapper.

## Regenerate

```bash
python scripts/generate_m18_mapping_docs.py
```

+ 17
- 0
docs/generated/m18-stsearch-types.md 파일 보기

@@ -0,0 +1,17 @@
<!-- AUTO-GENERATED by scripts/generate_m18_mapping_docs.py — do not edit by hand -->

# M18 `StSearchType` (master list APIs)

_Generated: 2026-08-01 10:01 UTC_

**Source:** `m18/model/M18MasterDataRequest.kt`

| Constant | `stSearch` value | Typical MTMS sync target |
|---|---|---|
| `PRODUCT` | `pro` | items (+ item_uom via prices) |
| `VENDOR` | `ven` | shop (`type=supplier`) |
| `CUSTOMER` | `cus` | (enum present; sync usage varies) |
| `UNIT` | `unit` | uom_conversion (+ m18 cunit) |
| `CURRENCY` | `cur` | currency |
| `BOM` | `udfbomforshop` | bom / bom_material (udfbomforshop) |
| `BUSINESS_UNIT` | `virDept` | shop (`type=shop`) |

+ 256
- 0
docs/user-guides/MTMS_BOM_使用說明.md 파일 보기

@@ -0,0 +1,256 @@
# MTMS 使用說明:BOM(匯入/啟停/應用)

> 本手冊依目前前端畫面與繁中文案整理,**按鈕/選單名稱以畫面上「」內文字為準**。
> 適用範圍:側欄「設定」下的 BOM 相關頁、建立工單時選 BOM、排程「自動生成工單」。
> **「一附」章節**使用本機資料庫 `fpsmsdb` 的真實例子(查詢當下快照);其他環境請改條件再對。
> 產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。

---

## 這份手冊怎麼用

| 符號 | 意思 |
|------|------|
| 「……」 | 畫面上看得到的按鈕、選單、標題 |
| → | 下一步操作 |

建議閱讀順序:

1. **先匯入/啟用 BOM**(「設定」→「匯入 BOM」)
2. **再檢查單位問題**(ADMIN:「BOM / 物料單位問題」)
3. **再建工單/排程產單**(選 active BOM)

---

## 一、整體流程(BOM 從主檔到工單)

```text
【匯入 BOM】Excel 上傳並檢查 →「確認匯入」
【BOM 明細】確認「啟用」/必要時「停用」
↓(選做)「BOM 權重得分」調整加權
↓(ADMIN)「BOM / 物料單位問題」巡檢
【建工單】「建立工單」選「BOM」
或【排程】「自動生成工單」(僅含具 BOM 的物料)
```

---

## 一附、本地資料庫實例(方便對照畫面)

> 以下數字來自本機 `fpsmsdb` 查詢結果。**不同環境/日期資料會不同**。
> BOM 狀態:`active`→「啟用」、`inactive`→「停用」。本機約有 **214** 筆啟用、**3** 筆停用。

### 例 A:啟用中的成品 BOM(適合在「BOM 明細」搜索)

| 「物品編號」 | 「物品名稱」 | 「BOM 狀態」 | 約材料筆數 |
|--------------|--------------|--------------|------------|
| PP2167 | 牛丼 | 啟用 | 10 |
| PP2277 | 烚意粉 | 啟用 | 3 |
| PP2390 | 熱情香果醬 | 啟用 | 4 |
| PP1224 | 柚子蒜蓉汁 | 啟用 | 5 |
| PP1136 | 白粥 | 啟用 | 9 |
| PP0259 | 牛肉水(2KG/包) | 啟用 | 0(無材料列,排程/提料前宜先確認) |

**怎麼練:**

1. 「設定」→「匯入 BOM」→ Tab「BOM 明細」。
2. 「物品編號」填 **PP2167** →「搜索」。
3. 載入後看「基本資訊」「材料清單」「製程與設備」;「BOM 狀態」應為「啟用」。

### 例 B:材料清單長什麼樣(PP2167 牛丼)

| 「物品編號」 | 「物品名稱」 | 約「基本數量」 |
|--------------|--------------|----------------|
| MA0471 | 日式超薄肥牛 | 272.16 |
| FA0164 | 洋蔥絲 | 145.15 |
| GI3236 | 清水 | 60.00 |
| PP2214 | 丼飯汁(2磅/包) | 41.73 |
| MG1507 | 江泉牌燒肉汁(1.8L/支) | 9.00 |
| MG1299 | 李錦記特選老抽(8L/桶) | 8.00 |
| MG0302 | 佛手牌味粉(10lb/罐) | 4.54 |
| MG0239 | 韓國雪白幼砂糖(30kg/包) | 4.54 |

### 例 C:用 BOM 建工單(接《排程與工單》手冊)

本機 **2026-07-29** 手動工單多掛在 active BOM 上,例如:

| 「工單編號」 | 「狀態」 | 成品 BOM | 「需求數量」 |
|--------------|----------|----------|--------------|
| JO-260729-038 | 待處理 | PP2277 烚意粉 | 51 |
| JO-260729-044 | 提料中 | PP2383 辣椒菜脯 | 265 |

**怎麼練:**「搜索工單/ 建立工單」→「建立工單」→「BOM」下拉選 **PP2277**(僅顯示啟用中的 BOM)。

### 自行查核用 SQL(選用)

```sql
SELECT b.code, i.name, b.status,
(SELECT COUNT(*) FROM bom_material bm
WHERE bm.bomId = b.id AND IFNULL(bm.deleted,0)=0) AS mats
FROM bom b
JOIN items i ON i.id = b.itemId
WHERE b.deleted = 0 AND b.code = 'PP2167';

SELECT i.code, i.name, bm.qty
FROM bom_material bm
JOIN items i ON i.id = bm.itemId
JOIN bom b ON b.id = bm.bomId
WHERE b.code = 'PP2167' AND IFNULL(bm.deleted,0)=0;
```

---

## 二、匯入 BOM(「設定」→「匯入 BOM」)

### 2.1 入口

- 側欄 **「設定」** → **「匯入 BOM」**(路徑通常為 `/settings/importBom`)
- 頁內標題可能顯示英文 **Import BOM**;側欄與 Tab 以繁中「匯入 BOM」為準。

### 2.2 兩個分頁

| Tab | 用途 |
|-----|------|
| 「匯入 BOM」 | 上傳 Excel、檢查、確認寫入 |
| 「BOM 明細」 | 查已有 BOM、啟用/停用 |

### 2.3 操作步驟:上傳並匯入

1. 開 Tab **「匯入 BOM」**。
2. **「選擇 BOM Excel 檔案」** →「選擇檔案」或「選擇資料夾」(可多選 `.xlsx`)。
3. 確認「已選 N 個檔案」後,點 **「上傳並檢查」**。
- 進行中:「上傳與檢查中…」、進度「已檢查 x / y 個檔案…」。
4. 檢視結果:
- 「正確 BOM 列表(可匯入)」
- 「問題 BOM 列表」
5. 必要時勾選類型相關選項(如「飲料」「箱料粉」等)或用「搜索檔名」篩選。
6. 確認無誤後點 **「確認匯入」**。
7. 成功提示類似 **「匯入完成」**;亦可先「下載檢查結果 Excel」留底。
8. 若要重來:點「返回重選檔案」。

### 2.4 常見錯誤(匯入)

| 畫面提示 | 建議處理 |
|----------|----------|
| 「請至少選擇一個 .xlsx 檔案」 | 先選檔再上傳 |
| 「上傳或檢查失敗,請稍後再試。」/伺服器 500 | 檢查網路/檔案格式後重試 |
| 檔名重複相關提示(_2、_3…) | 依提示整理檔名後重傳 |
| 「匯入失敗,請查看主控台。」 | 記下失敗檔,修正 Excel 後只重傳問題檔 |

---

## 三、BOM 明細:查詢與啟停

### 3.1 搜索

1. Tab **「BOM 明細」**。
2. 「物品編號」/「物品名稱」→「搜索」/「重置」。
3. 多筆時:「找到多筆 BOM,請選擇一筆載入明細」(按鈕上會標「成品|半成品」「啟用|停用」)。

### 3.2 可改什麼

| 區塊 | 說明 |
|------|------|
| 「BOM 狀態」 | 「啟用」/「停用」→「儲存狀態」 |
| 「基本資訊」 | 產出數量、類型、過敏原、色深/浮沉/濃淡、時段、複雜度、基礎得分等(多為檢視) |
| 「材料清單」 | 物品編號/名稱、基本/庫存/銷售數量與單位 |
| 「製程與設備」 | 製程檢視 |

> **注意:** 完整欄位線上「編輯/儲存」目前前端關閉(`SHOW_BOM_FULL_EDIT = false`)。日常維護以 **Excel 匯入** 與 **啟用/停用** 為主。

### 3.3 何時「停用」

- 配方作廢、暫不允許再建工單時,將「BOM 狀態」改「停用」並「儲存狀態」。
- 「建立工單」的 BOM 下拉**只列出啟用**項目;停用後新單選不到該 BOM。

---

## 四、BOM 權重得分

### 4.1 入口

- 「設定」→ **「BOM 權重得分」**(`/settings/bomWeighting`)

### 4.2 分頁

| Tab | 用途 |
|-----|------|
| 「BOM 加權」 | 調整各評分項目的「權重」(總和須=1) |
| 「BOM得分」 | 查看各貨品「基礎得分」 |

### 4.3 操作

1. 「BOM 加權」→「編輯」→改「權重」→「儲存」。
2. 校驗失敗時:「權重必須為數字」或「權重總和必須等於 1(目前總和: x)」。
3. 成功:「更新成功(已重新計算 N 筆 BOM 基礎分)」。
4. 到「BOM得分」核對「貨品編號」「物品名稱」「基礎得分」。

---

## 五、BOM/物料單位問題(ADMIN)

### 5.1 入口

- 「設定」→ **「BOM / 物料單位問題」**(僅 **ADMIN**;`/settings/masterDataIssues`)
- 頁標題常顯示:「BOM/貨品單位問題」

### 5.2 操作

1. Tab「BOM」或「貨品」。
2. 「搜索」「類型」(「全部」「BOM 總表」「BOM 原材料」)。
3. 「重新檢查」更新清單;「複製清單」方便貼到表單/郵件。
4. 點列開詳情:「問題」「應為」「實際」→「關閉」。

### 5.3 常見問題文案(節錄)

- 「BOM 編號為空」「BOM 名稱為空」
- 「BOM 產出單位與成品銷售單位不一致」
- 「BOM 原料銷售/基本/庫存單位與貨品主檔不一致」
- 「BOM 編號與關聯貨品不一致」

側欄紅點例:`BOM N 筆 · 貨品 M 筆`。空狀態:「目前沒有問題。」

---

## 六、應用:建立工單時選 BOM

### 6.1 手動建立

1. 「管理工單」→「搜索工單/ 建立工單」→「建立工單」。
2. **「BOM」**(必填)選成品/半成品;同名時可能標「(成品)」「(半成品)」。
3. 「標準生產數」×「批數」=「需求數量」(單位來自 BOM 產出 UOM)。
4. 「預計生產日期」等填妥 →「建立」。
5. 成功常提示「成功更新資料」。未選 BOM 時可能出現「請選擇 BOM」/`Bom required!`。

### 6.2 排程自動生成

1. 「排程」→「生產排程」→「詳細」。
2. 「自動生成工單」(僅處理**具 BOM** 的物料;說明文案類似「選擇日期範圍(僅含具 BOM 的物料…)」)。
3. 舊細排頁另有「生成工單」「查看 BOM」(材料表含「編號」「名稱」「可用數量」「需求數量」)。

詳見《MTMS 排程與工單 使用說明》。

---

## 七、權限與路徑速查

| 畫面 | 路徑 | 備註 |
|------|------|------|
| 匯入 BOM | `/settings/importBom` | 「設定」下 |
| BOM 權重得分 | `/settings/bomWeighting` | |
| BOM/物料單位問題 | `/settings/masterDataIssues` | **ADMIN** |
| 建立工單 | `/jo` | 選 active BOM |
| 排程產單 | `/ps` | 需 BOM |

---

## 八、常見問題速查

| 情況 | 建議 |
|------|------|
| 建工單下拉找不到某成品 | 到「BOM 明細」確認是否「啟用」;或尚未匯入 |
| 排程產單跳過某物料 | 該物料可能無 BOM/不在排期 BOM 範圍 |
| 單位對不上、品檢/提料異常 | ADMIN 開「BOM / 物料單位問題」對照主檔後修正 Excel 再匯入 |
| 權重儲存失敗 | 確認各權重為數字且總和=1 |

+ 259
- 0
docs/user-guides/MTMS_工單提料報工上架_使用說明.md 파일 보기

@@ -0,0 +1,259 @@
# MTMS 使用說明:工單提料/報工/上架

> 本手冊依目前前端畫面與繁中文案整理,**按鈕/選單名稱以畫面上「」內文字為準**。
> 適用範圍:放單之後的「工單提料」「工單生產流程」「上架掃碼」(品檢/上架)。
> 建單與排程請先看《MTMS 排程與工單 使用說明》;BOM 請看《MTMS BOM 使用說明》。
> **「一附」章節**使用本機資料庫 `fpsmsdb` 的真實例子(查詢當下快照)。
> 產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。

---

## 這份手冊怎麼用

| 符號 | 意思 |
|------|------|
| 「……」 | 畫面上看得到的按鈕、選單、標題 |
| → | 下一步操作 |

建議閱讀順序:

1. **放單**(「搜索工單/ 建立工單」)
2. **提料**(「工單提料」)
3. **對料 → 工序 → 完成工單**(「工單生產流程」)
4. **品檢 → 上架**(「品檢」+「上架掃碼」)

---

## 一、整體流程

```text
【/jo】「規劃中」──「放單」──►「待處理」/「提料中」
【/jodetail】「工單提料」──「查看詳情」──► 掃碼提料 ──「提交」/「提交所有已掃描項目」
【/productionProcess】「工單生產流程」
├─(可選)「工單對料」→ 二次掃碼 →「確認所有提料」
├─「查看詳情」→ 各工序「開始」/「訂單完成」或「已完成」(Just Pass)
「完成工單」(確認:「確認要完成此工單嗎?」)
「品檢」→「確定品檢結果」(可列印/下載 QR)
【/putAway】「上架掃碼」:掃貨品 QR → 掃倉庫 QR →「確定及上架貨物」
「已上架工單」;工單「已完成」
```

---

## 一附、本地資料庫實例

> 狀態中文:`pending`→「待處理」、`packaging`/`picking`→「提料中」、`processing`→「生產中」、`storing`→「待品檢入倉」、`completed`→「已完成」。

### 例 A:適合練提料(2026-07-29)

| 「工單編號」 | 「狀態」 | 成品 | 「需求數量」 | 建議下一步 |
|--------------|----------|------|--------------|------------|
| JO-260729-038 | 待處理 | PP2277 烚意粉 | 51 | 「工單提料」開單掃碼 |
| JO-260729-008 | 提料中 | PP2257 咖哩汁箱料粉 | 1 | 繼續提交提料 |
| JO-260729-013 | 提料中 | PP1043 豆豉汁(2磅/包) | 309 | 繼續提交提料 |
| JO-260729-044 | 提料中 | PP2383 辣椒菜脯 | 265 | 繼續提交提料 |
| JO-260729-034 | 生產中 | PP2302 酸甜蘿蔔粒箱料粉 | 1 | 「工單生產流程」做工序/完成工單 |

**怎麼練:**

1. 「搜索工單/ 建立工單」→「預計生產日期」**2026-07-29** →「搜索」。
2. 「狀態」篩「待處理」找 JO-260729-038,或直接到「工單提料」找同日卡片。
3. 「生產中」單到「工單生產流程」練「查看詳情」/「完成工單」(完成會改資料,請用測試庫)。

### 例 B:狀態與畫面入口對照

| 「狀態」 | 常用入口 |
|----------|----------|
| 待處理/提料中 | 「工單提料」 |
| 生產中 | 「工單生產流程」→「工藝流程」 |
| 待品檢/待品檢入倉 | 「工單生產流程」→「品檢」或「待QC上架工單」 |
| 已完成 | 「已上架工單」/工單搜索「已完成」 |

### 自行查核用 SQL(選用)

```sql
SELECT jo.code, jo.status, i.code, i.name, jo.reqQty
FROM job_order jo
LEFT JOIN bom b ON b.id = jo.bomId
LEFT JOIN items i ON i.id = b.itemId
WHERE jo.deleted = 0 AND DATE(jo.planStart) = '2026-07-29'
AND jo.status IN ('pending','packaging','processing','storing')
ORDER BY jo.status, jo.code
LIMIT 20;
```

---

## 二、放單(前置,在「搜索工單/ 建立工單」)

1. 側欄「管理工單」→「搜索工單/ 建立工單」(`/jo`)。
2. 找到「規劃中」工單 →「放單」或「放單 (N)」。
3. 放單後狀態變「待處理」/「提料中」,即可去「工單提料」。
4. 詳情內可見庫存摘要字樣如「可提料項目數量:」「未能提料項目數量:」。

> 亦可在生產流程詳情內對仍屬規劃中的單按「放單」。

---

## 三、工單提料(`/jodetail`)

### 3.1 入口與分頁

- 側欄「管理工單」→ **「工單提料」**;頁標題「工單提料」。

| Tab | 文案 |
|-----|------|
| 0 | 「工單提料詳情」 |
| 1 | 「已完成工單記錄」 |
| 2 | 「物料提料狀態」 |
| 3 | 「膠茜數目使用數量」 |

### 3.2 「工單提料詳情」列表

- 品類:「全部」「飲料」「箱料粉」「其他」
- 樓層:「2F」「3F」「4F」「沒有批號」等
- 卡片常見:「工單」「批號」「提料單」「物品名稱」「需求數量」、狀態 Chip
- 點 **「查看詳情」** 進入掃碼提料

### 3.3 掃碼提料步驟

1. 點「開始掃碼」(可「停止掃碼」)。
2. 掃描物料/批號 QR;必要時開「批號QR碼掃描」或「手動輸入」→「提交」。
3. 畫面上應出現「QR碼驗證成功。」/「驗證成功!」;進度見「掃碼結果」「提交數量」。
4. 單行「提交」或一次「提交所有已掃描項目」(進行中「提交中...」)。
5. 完成後「返回列表」。

### 3.4 「已完成工單記錄」

- 「查看詳情」「打印版頭紙」(2F/3F/4F)、「打印數量」「打印機」
- 「對料狀態」:「對料待處理」/「對料已完成」
- 提示語例:「工單已完成提料和對料」

### 3.5 提料常見錯誤

| 提示 | 處理 |
|------|------|
| 「此批次已拒收,請掃描另一個批次。」 | 換批 |
| 「掃描的批次已被其他用戶完全提料。請掃描其他批次。」 | 換可用批 |
| 「物品數量不足」/數量大於需求/可用量 | 改「提交數量」 |
| 「請先選擇打印機」 | 列印版頭紙前先選機 |

---

## 四、工單生產流程(`/productionProcess`)

### 4.1 入口與頂層分頁

- 側欄「管理工單」→ **「工單生產流程」**。

| Tab | 文案 |
|-----|------|
| 0 | 「工藝流程」 |
| 1 | 「待QC上架工單」 |
| 2 | 「已上架工單」 |
| 3–6 | 各類「儀表板 - …」 |

### 4.2 「工藝流程」卡片動作

| 按鈕 | 用途 |
|------|------|
| 「查看詳情」 | 進工序/BOM/對料等 |
| 「工單對料」 | 提料完成後二次掃批號確認 |
| 「完成工單」 | 整張 JO 完工(確認:「確認要完成此工單嗎?」;權限常限 ADMIN) |
| 「品檢」 | 開品檢 Modal(條件滿足且有入庫行時才出現) |

詳情內 Tabs:「工單信息」「BOM 材料」「工藝流程」「工藝明細」「工單對料」;另有「返回列表」「取消工單」「刪除工單」等。

### 4.3 工單對料(二次掃)

1. 點「工單對料」。
2. 對已提料批再掃「批號QR碼掃描」(或「手動輸入」)。
3. 「驗證成功!」後,全部核對完點 **「確認所有提料」**。
4. 「返回列表」可能解除指派,勿中途亂退。

### 4.4 工序報工(單步)

1. 「查看詳情」→「工藝流程」/「工藝明細」。
2. 待處理工序點 **「開始」** → Dialog「掃描操作員和設備」。
3. 「開始掃碼」:先操作員/員工,再設備 →「提交並開始」。
4. 執行中可「暫停」/「繼續」(「暫停原因」);結束該步用 **「訂單完成」**(填「工序產出」「不良品」「損耗」)。
5. 若允許略過執行:按鈕「已完成」(Just Pass),確認「確認要通過此工序嗎?」。

> **勿混淆:** 工序「訂單完成」=結束**一步**;卡片「完成工單」=結束**整張工單**。

### 4.5 品檢

1. 條件滿足後點「品檢」。
2. Modal 常見 Tab:「處理來貨及品檢」/「來貨及品檢詳情」。
3. 填結果後 **「確定品檢結果」**。
4. 可「打印機」「列印數量」「列印」「下載QR碼」(供之後上架掃碼)。
5. 校驗例:「請決定品檢結果」「有未完成品檢項目」「請輸入不合格數量」「請輸入到期日!」。

亦可從 Tab「待QC上架工單」或提醒鈴深連結進入。

---

## 五、上架掃碼(`/putAway`)

### 5.1 入口

- 側欄「倉庫管理」→ **「上架掃碼」**;頁標題「上架」。

### 5.2 兩段掃碼

1. 待機:「等待掃瞄中,請掃瞄貨品二維碼開始上架程序」。
2. 掃**貨品/來貨行** QR → 開 Modal。
3. 填「是次上架數量」;再掃**倉庫** QR(「掃瞄倉庫二維碼」/「請掃瞄倉庫二維碼」)。
4. 點 **「確定及上架貨物」**。
5. 可在「是次上架記錄」核對。

失敗:「讀取不成功,請重新掃瞄」;數量:「上架數量不得大於 …」「最小為1」等。

入庫行狀態語意:「待上架」→「已部分上架」→「已上架」。完成後可在「已上架工單」看到。

---

## 六、狀態對照(操作頁為準)

| 代碼 | 畫面 |
|------|------|
| planning | 「規劃中」 |
| pending | 「待處理」 |
| packaging / picking | 「提料中」 |
| processing | 「生產中」 |
| pendingQC | 「待品檢」 |
| storing | 「待品檢入倉」 |
| completed | 「已完成」 |
| cancelled | 「已取消」 |

工序行:「待處理」→「進行中」→(可「已暫停」)→「完成」/「已完成」(Pass)。

---

## 七、常見問題速查

| 情況 | 建議 |
|------|------|
| 提料頁找不到單 | 確認已「放單」;日期/樓層/品類篩選是否過窄 |
| 對料按鈕灰/沒有 | 提料單未完成、已指派他人、或對料已完成 |
| 「完成工單」按不到 | 權限或工序未齊;確認提示「確認要完成此工單嗎?」 |
| 沒有「品檢」按鈕 | 尚未完成工單/無 stock-in 行 |
| 上架掃不到 | 先品檢並列印/下載 QR;或用 `?stockInLineId=` 深連結 |
| 掃碼驗證失敗 | 換批、確認未拒收、確認單位/可用量 |

---

## 八、路徑速查

| 畫面 | 路徑 |
|------|------|
| 搜索/放單 | `/jo` |
| 工單提料 | `/jodetail` |
| 工單生產流程 | `/productionProcess` |
| 上架掃碼 | `/putAway` |

+ 459
- 0
docs/user-guides/MTMS_排程與工單_使用說明.md 파일 보기

@@ -0,0 +1,459 @@
# MTMS 使用說明:排程 → 開工單

> 本手冊依目前前端畫面與繁中文案整理,**按鈕/選單名稱以畫面上「」內文字為準**。
> 適用範圍:側欄「排程」、管理工單(搜索/建立、提料、生產流程)。
> **「一附」章節**使用本機資料庫 `fpsmsdb` 的真實例子(查詢當下快照);其他環境請改日期再對。
> 產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。

---

## 這份手冊怎麼用

| 符號 | 意思 |
|------|------|
| 「……」 | 畫面上看得到的按鈕、選單、標題 |
| → | 下一步操作 |
| 節點 | 流程中的一個階段(狀態/畫面) |

建議閱讀順序:

1. **先排期**(側欄「排程」)
2. **再放單/建工單**(「管理工單」→「搜索工單/ 建立工單」)
3. **提料 → 生產 → 完成**

---

## 一、整體流程(從排期到完工)

```text
【排程】預測/查看排期
↓ 「自動生成工單」 或 手動「建立工單」
【規劃中】工單已建立、尚未放單
↓ 「放單」
【待提料/提料中】
↓ 「工單提料」掃碼提交
【生產中】「工單生產流程」各工序開始/完成
↓ 「完成工單」
【品檢/上架】「待QC上架工單」→「已上架工單」
```

也可**不經排程**,在「搜索工單/ 建立工單」直接「建立工單」(手動工單)。

---

## 一附、本地資料庫實例(方便對照畫面)

> 以下數字來自本機 `fpsmsdb` 查詢結果,用於說明「畫面上大概會看到什麼」。
> **不同環境/日期資料會不同**;請用「搜索」條件改成你們當天日期再核對。
> 狀態中文依前端翻譯:`planning`→「規劃中」、`pending`→「待處理」、`packaging`→「提料中」、`processing`→「生產中」、`storing`→「待品檢入倉」、`completed`→「已完成」。

### 例 A:已有排期、尚未產工單(適合練習「詳細」→「自動生成工單」)

在「生產排程」用「生產日期」搜 **2026-08-03**,本機有一筆細排(`production_schedule.id = 928`,`type = detailed`):

| 畫面概念 | 本機資料 |
|----------|----------|
| 「生產日期」 | 2026-08-03 |
| 「預計生產數」(約) | 17,334 |
| 「成品款數」(約) | 47 |

打開「詳細」後,明細列會類似(節錄):

| 「編號」 | 「名稱」 | 約「需求數量」 | 約「存貨量」 | 約需工單數 |
|----------|----------|----------------|--------------|------------|
| PP1175 | 鮮檸檬汁(P+4) | 1,406 | 1,400 | 74 |
| PP1224 | 柚子蒜蓉汁 | 140 | 50 | 1 |
| PP0259 | 牛肉水(2KG/包) | 41 | 51 | 41 |
| PP2284 | 油醋汁(1KG/包) | 28 | 32 | 1 |
| PP2390 | 熱情香果醬 | 12 | 16 | 4 |

**怎麼練:**

1. 「排程」→ 生產日期選 **2026-08-03** →「搜索」→「詳細」。
2. 對照上表成品是否出現在明細。
3. 若環境允許,再試「自動生成工單」(會真正建 JO,請在測試庫操作)。

> 查詢當下:此排期**尚未**有透過 `prodScheduleLineId` 掛上的工單(適合示範「產工單前」)。

### 例 B:排期已產工單且已完成(歷史成功路徑)

本機較早一筆:**2026-06-17** 細排(約預計生產 17,421、成品款數 33),曾產生多張 `type = detailed` 工單,例如:

| 「工單編號」 | 「狀態」 | 「需求數量」 | 成品 |
|--------------|----------|--------------|------|
| JO-260617-008 | 已完成 | 568 | PP1234 日式咖哩汁 |
| JO-260617-012 | 已完成 | 15 | PP2288 香水檸檬汁P+3 |
| JO-260617-015 | 已完成 | 600 | PP1136 白粥 |
| JO-260617-024 | 已完成 | 28 | PP2284 油醋汁(1KG/包) |
| JO-260617-030 | 已完成 | 508 | PP2290 韓式豬軟骨 |

**怎麼練:**

1. 「搜索工單/ 建立工單」→「預計生產日期」填 **2026-06-17** →「搜索」。
2. 找上表工單編號,點「查看」看已完成工單長怎樣。
3. 對照:這類工單來自排程 release(`type` 在庫為 `detailed`),不是手動「建立工單」的 `manual`。

### 例 C:手動「建立工單」(不經排程)

本機 **2026-07-29** 工單幾乎皆為手動(`type = manual`,且未掛排期行)。例子:

| 「工單編號」 | 「狀態」 | 「需求數量」 | 成品 |
|--------------|----------|--------------|------|
| JO-260729-045 | 待處理 | 3 | PP2390 熱情香果醬 |
| JO-260729-038 | 待處理 | 51 | PP2277 烚意粉 |
| JO-260729-034 | 生產中 | 1 | PP2302 酸甜蘿蔔粒箱料粉 |
| JO-260729-044 | 提料中 | 265 | PP2383 辣椒菜脯 |

**怎麼練:**

1. 「預計生產日期」選 **2026-07-29** →「搜索」。
2. 用「狀態」篩「待處理」→ 應能看到類似 JO-260729-045。
3. 若該單仍「規劃中」,可練習「放單」;若已是「待處理」,可接「工單提料」。

同日狀態分佈(本機快照):約 17 張「待處理」、24 張「提料中」、4 張「生產中」——正好對應「放單後 → 提料 → 生產」不同節點。

### 例 D:近兩週工單狀態分佈(看流程卡在哪)

本機最近約 14 天(未隱藏工單)概況:

| 「狀態」 | 約筆數 | 使用者下一步常做什麼 |
|----------|--------|----------------------|
| 已完成 | 359 | 可當完成範本「查看」 |
| 待處理 | 42 | 「工單提料」 |
| 提料中 | 37 | 繼續掃碼/提交提料 |
| 待品檢入倉 | 33 | 「工單生產流程」品檢/上架 |
| 生產中 | 4 | 「工單生產流程」繼續工序 |

### 例 E:還在「規劃中」的單(適合練刪除/放單)

本機仍有例如:**JO-260427-039**(沙薑醬 PP2205,需求約 291,「規劃中」)。

- 若只需練習「查看」→ 看「放單」「刪除工單」按鈕是否出現。
- **勿在正式/共用庫隨意刪除**;測試庫才建議真的按「刪除工單」。

### 自行查核用 SQL(選用)

```sql
-- 某日排期摘要
SELECT id, DATE(produceAt) AS produce_date, type,
totalEstProdCount, totalFGType
FROM production_schedule
WHERE deleted = 0 AND DATE(produceAt) = '2026-08-03';

-- 該排期成品明細(前 20)
SELECT i.code, i.name, psl.prodQty, psl.stockQty, psl.needNoOfJobOrder
FROM production_schedule_line psl
JOIN production_schedule ps ON ps.id = psl.prodScheduleId
JOIN items i ON i.id = psl.itemId
WHERE ps.deleted = 0 AND psl.deleted = 0
AND DATE(ps.produceAt) = '2026-08-03'
ORDER BY psl.itemPriority, i.code
LIMIT 20;

-- 某日工單+狀態
SELECT code, status, type, reqQty, DATE(planStart) AS plan_date
FROM job_order
WHERE deleted = 0 AND (isHidden = 0 OR isHidden IS NULL)
AND DATE(planStart) = '2026-07-29'
ORDER BY status, code;
```

---

## 二、排程(側欄「排程」)

### 2.1 入口

- 側欄點 **「排程」**
- 進入頁面標題:**「生產排程」**(路徑通常為 `/ps`)

> 說明:系統另有舊版「需求預測」「詳細排程」頁(`/scheduling/...`),**目前側欄主入口是「排程」→「生產排程」**。以下以主入口為準。

### 2.2 畫面上常見按鈕

| 按鈕/功能 | 用途(白話) |
|------------|--------------|
| 「預測排期」 | 依日期/天數**計算產生**預計排期 |
| 「搜索」 | 依「生產日期」等條件查已有排期 |
| 「詳細」 | 打開該筆排期的明細 |
| 「自動生成工單」 | 依此排期**一次產生多張工單**(在詳情裡) |
| 「關閉」 | 關閉詳情視窗 |
| 「排期設定」 | 庫存/排期相關設定與匯入匯出 |
| 「匯出計劃/物料需求Excel」 | 匯出計劃與物料需求 |
| 「匯出送貨單數量」 | 匯出送貨單數量區間資料 |

列表常見欄位:「生產日期」「預計生產數」「成品款數」等。

### 2.3 操作步驟:做出排期(Happy path)

1. 進入 **「排程」** → **「生產排程」**。
2. 點 **「預測排期」**。
3. 在對話框 **「準備生成預計排期」** 中填:
- 「開始日期」
- 「排期日數」
4. 點 **「計算預測排期」**。
5. 成功時畫面會提示類似 **「成功計算排期!」**;失敗會提示計算錯誤或不明狀況。
6. 選擇「生產日期」後點 **「搜索」**,在列表找到該日排期。
7. 點該列 **「詳細」**,打開 **「排期詳細」**。
8. 確認內容無誤後,點 **「自動生成工單」** → 系統依排期建立工單。
9. 點 **「關閉」** 結束。

### 2.4 節點說明(排程)

| 節點 | 使用者在做什麼 | 下一個常見動作 |
|------|----------------|----------------|
| 尚未有排期 | 進「生產排程」但列表空/無當日資料 | 「預測排期」 |
| 已有排期列表 | 用「搜索」找日期 | 「詳細」 |
| 排期詳細已打開 | 檢查預計生產內容 | 「自動生成工單」 |
| 已生成工單 | 工單出現在「搜索工單/ 建立工單」 | 去「放單」 |

### 2.5 排程常見問題

| 情況 | 建議處理 |
|------|----------|
| 「計算預測排期」失敗 | 記下畫面錯誤訊息;檢查開始日期/天數;稍後再試或聯絡系統/IT |
| 「自動生成工單」失敗 | 畫面可能顯示失敗訊息;確認排期內容是否完整、BOM/物料是否齊全 |
| 找不到某日排期 | 確認「生產日期」與「搜索」條件;必要時再跑一次「預測排期」 |
| 想改數量再開工單 | 主入口 `/ps` 詳情偏「一次自動生成」;若需逐行改量/發佈,需使用舊版「詳細排程」編輯頁(見附錄) |

### 2.6 附錄:舊版「詳細排程」/「需求預測」(進階)

若單位仍使用直連網址:

| 畫面 | 標題(約) | 重點按鈕 |
|------|------------|----------|
| 需求預測列表 | 「需求預測」 | 「測試粗排」「搜索」「詳情」 |
| 需求預測詳情 | 「成品及物料需求預測詳情」 | 多為**檢視**(依成品/依物料、「查看 BOM」) |
| 詳細排程列表 | 「詳細排程」 | 「詳細排程」(產生)、「匯出排程」「詳情」 |
| FG 生產排程 | 「成品生產排程」/「FG 生產排程」 | 列「發佈」、改「需求數量」後儲存、**「生成工單」**、「返回」 |

注意(舊版細排詳情):

- **「生成工單」** 常僅允許**生產日期為今天**;否則可能跳出英文提示(畫面未必有完整中文翻譯)。
- 前端**沒有**「從某一筆需求預測一鍵跳到對應詳細排程」的按鈕;兩條線在畫面上是分開的。

設定選單另有 **「需求預測設定」**(成品排除日、星期等),屬主檔設定,不是每日排期操作。

---

## 三、工單:搜索/建立/放單

### 3.1 入口

側欄 **「管理工單」** → **「搜索工單/ 建立工單」**

頁面標題:**「搜索工單/建立工單」**

### 3.2 畫面上常見按鈕

| 按鈕 | 用途 |
|------|------|
| 「建立工單」 | 手動開一張新工單 |
| 「放單」/「放單 (N)」 | 將「規劃中」工單放出,進入後續提料 |
| 「重置」 | 清空搜尋條件 |
| 「查看」 | 開「工單詳情」 |
| 「取消工單」 | 取消後工單從列表隱藏(非規劃中等情況) |

搜尋條件常見:「工單編號」「成品/半成品名稱」「預計生產日期」~「預計生產日期至」「工單類型」「狀態」。

### 3.3 操作步驟:手動建立工單

1. 點 **「建立工單」**,打開標題為 **「建立工單」** 的視窗。
2. 填寫:
- 「BOM」
- 「標準生產數」(通常唯讀)
- 「批數」
- 「需求數量」(常由標準×批數自動帶出)
- 「工單類型」(選 BOM 後可能自動對應)
- 「生產優先序」(常見預設約 50,範圍約 1–100)
- 「預計生產日期」
- 可勾選「記住為預設日期」
3. 點 **「建立」**。
4. 成功後通常提示資料已更新;新工單多為 **「規劃中」** 狀態。
5. 在列表勾選/找到該工單,點 **「放單」**(或上方「放單 (N)」批量)。

### 3.4 操作步驟:從排程來的工單

1. 在「生產排程」詳情已按 **「自動生成工單」**(或舊版「生成工單」)。
2. 到 **「搜索工單/ 建立工單」**,用「預計生產日期」等搜尋。
3. 確認工單後執行 **「放單」**。

### 3.5 工單詳情(「查看」)

標題:**「工單詳情」**

常見分頁/區塊:

- 「工單信息」
- 「BOM 材料」
- 「工藝流程」/「工藝明細」

常見動作:

| 按鈕 | 何時可用(概念) | 說明 |
|------|------------------|------|
| 「放單」 | 多為「規劃中」 | 放出工單 |
| 「刪除工單」 | 多為仍在「規劃中」 | 確認後**無法復原** |
| 「取消工單」 | 已非規劃中等 | 確認後從列表**隱藏**;已上架通常不可取消 |
| 「儲存」/「取消」 | 編輯 Dialog | 存檔或放棄 |
| 「返回列表」 | — | 回搜尋頁 |

庫存摘要常見字樣:「所需貨品項目數量:」「可提料項目數量:」「未能提料項目數量:」。

刪除確認:

- 「確認刪除工單」
- 「確定要刪除此工單嗎?此操作無法復原。」

取消確認:

- 「確認取消工單」
- 「確定要取消此工單嗎?工單將從列表中隱藏。」
- 按鈕:「取消工單」/「取消」(後者為放棄這次取消操作)

### 3.6 工單狀態(搜尋/列表常見)

畫面上可能出現(實際以該列顯示為準):

| 狀態(常見中文) | 白話 |
|------------------|------|
| 「規劃中」 | 已建立,尚未放單 |
| 「待處理」/待提料相關 | 已放單,等提料 |
| 「提料中」/「進行中」 | 正在提料或進行中 |
| 「已開始工序」/「生產中」 | 已進入生產工序 |
| 「成品入倉中」/「待品檢入倉」 | 生產後待品檢/入倉 |
| 「已上架」 | 已完成上架 |
| 「已取消」 | 已取消 |

篩選下拉亦可能見:「待處理」「提料中」「已開始工序」「成品入倉中」「已上架」「已取消」等。

### 3.7 工單建立/放單常見問題

| 情況 | 建議處理 |
|------|----------|
| 「建立」沒反應 | 檢查 BOM、批數、日期、工單類型是否已填;看是否有必填未選 |
| 「放單」失敗 | 記下提示;檢查是否仍為可放單狀態、物料/權限 |
| 想刪掉剛建錯的單 | 「規劃中」→「查看」→「刪除工單」→ 確認 |
| 已放單但不想做了 | 「取消工單」→ 確認隱藏;**已上架**通常不能取消 |
| 批量「放單 (N)」部分失敗 | 依成功/失敗筆數訊息,對失敗單再個別處理 |

---

## 四、工單提料

### 4.1 入口

**「管理工單」** → **「工單提料」**

### 4.2 常見分頁

- 「工單提料詳情」
- 「已完成工單記錄」
- 「物料提料狀態」
- (及其他與膠茜/用量相關分頁,以畫面為準)

### 4.3 操作步驟(概念)

1. 在「工單提料詳情」找到已放單、待提料的工單。
2. 點 **「查看詳情」** 進入提料執行畫面。
3. 依畫面使用 **「開始掃碼」**、掃描物料/批號。
4. 需要時點 **「提交」**/**「提交所有已掃描項目」**/**「確認」**。
5. 完成後可在「已完成工單記錄」核對。

對料相關:生產流程中可能有 **「工單對料」**,並有 **「確認所有提料」**。

### 4.4 提料常見問題

| 情況 | 建議處理 |
|------|----------|
| 批號不符 | 依 Dialog 提示按「確認」,改掃正確批號 |
| 庫存不足/過期 | 依畫面提示;先補貨或調批號,勿強行提交 |
| 掃錯要重來 | 用畫面上的「取消」/清除已掃項目(以當頁按鈕為準)後重掃 |

提料狀態常見:「待提料」「已掃碼」「對料待處理」「對料已完成」;提料單「已放單」「已完成」。

---

## 五、工單生產流程

### 5.1 入口

**「管理工單」** → **「工單生產流程」**

### 5.2 常見分頁/區塊

- 「工藝流程」
- 「待QC上架工單」
- 「已上架工單」
- 「儀表板 - 工單狀態」等

### 5.3 工序操作(Happy path)

1. 在「工藝流程」找到工單,點 **「查看詳情」**。
2. (可選)**「工單對料」** 確認物料。
3. 各工序:
- **「開始」** 開工序
- 可依畫面掃 **操作員**/**設備**(未掃可能提示「請先掃描操作員編號」「請掃描設備編號」)
- 可 **「暫停」**
- **「完成步驟」**/**「通過」**/**「已完成」**(Just Pass 等,以畫面為準)
- 確認通過時可能問:**「確認要通過此工序嗎?」**
4. 全部就緒後點 **「完成工單」**,確認:**「確認要完成此工單嗎?」**
5. 需要時做 **「品檢」**,再到「待QC上架工單」完成上架,最後在「已上架工單」可見。

其他可能按鈕:「提交並開始」「提交包裝袋消耗」等。

### 5.4 工序狀態(常見)

「待處理」「進行中」「已暫停」「完成」/「已完成」「通過」「未開始」「已停止」「已取消」等。

### 5.5 生產常見問題

| 情況 | 建議處理 |
|------|----------|
| 「完成工單」按不了 | 通常尚有工序未完成或條件未滿足;先完成各步驟 |
| 掃碼驗證失敗 | 「驗證失敗. 請檢查操作員和設備.」→ 重掃正確編號 |
| API/系統錯誤 | 「發生錯誤,請稍後再試。」→ 稍後重試或聯絡支援 |
| 做到一半要停 | 用「暫停」;勿與「取消工單」混淆(取消是整張工單層級) |

---

## 六、取消、刪除、返回——怎麼選?

| 你想做的事 | 建議用哪個按鈕 | 結果 |
|------------|----------------|------|
| 建錯、還在規劃中,不要了 | 「刪除工單」 | 刪除,**不可復原** |
| 已放單/進行中,不要繼續 | 「取消工單」 | 確認後從列表**隱藏** |
| 只是關掉視窗、不改資料 | 「關閉」/「取消」/「返回」「返回列表」 | 不取消工單本身 |
| 工序暫停一下 | 「暫停」 | 工單仍在,之後可再「開始」 |

---

## 七、快速檢查清單(每日)

1. 「排程」→ 需要時「預測排期」→「搜索」→「詳細」→「自動生成工單」
2. 「搜索工單/ 建立工單」→ 核對當日工單 →「放單」
3. 「工單提料」→ 掃碼提交
4. 「工單生產流程」→ 各工序開始/完成 →「完成工單」→ 品檢/上架

手動補單:同頁「建立工單」→「建立」→「放單」。

---

## 八、文件維護說明(給內部)

- 文案來源:前端 `i18n/zh/navigation.json`、`jo.json`、`schedule.json`、`productionProcess.json`,以及「生產排程」頁硬編碼繁中。
- 主程式入口:
- 排程:`FPSMS-frontend/src/app/(main)/ps/page.tsx`
- 工單搜尋/建立:`.../jo/page.tsx`、`JoWorkbenchSearch`、`JoCreateFormModal`
- 提料:`.../jodetail/`
- 生產:`.../productionProcess/`
- 若按鈕改名或流程改版,請同步改本手冊,並重新匯出 Word。

重新匯出 Word(需已安裝套件):

```bash
pip install python-docx
python scripts/export_user_guide_office.py
```

產出:`docs/exports/MTMS_Schedule_JobOrder_UserGuide.docx`

+ 375
- 0
docs/user-guides/MTMS_送貨訂單與出貨_使用說明.md 파일 보기

@@ -0,0 +1,375 @@
# MTMS 使用說明:送貨訂單 → 放單 → 成品出倉

> 本手冊依目前前端畫面與繁中文案整理,**按鈕/選單名稱以畫面上「」內文字為準**。
> 適用範圍:側欄「送貨訂單」、倉庫「成品出倉」(撳單/掃碼出倉/列印標籤)、加單與車線-X。
> **「一附」章節**使用本機資料庫 `fpsmsdb` 的真實例子(查詢當下快照)。
> 產生日期:依原始碼現況(若畫面改版,請以實際 UI 為準)。

---

## 這份手冊怎麼用

| 符號 | 意思 |
|------|------|
| 「……」 | 畫面上看得到的按鈕、選單、標題 |
| → | 下一步操作 |

**角色分工(白話):**

| 角色動作 | 主要畫面 |
|----------|----------|
| 看單、放單、加單、補貨 | 「送貨訂單」`/do` |
| 撳單、掃碼提貨、列印 DN/標籤 | 「成品出倉」`/doworkbench` |
| 調整提料順序等(ADMIN) | 「成品出倉管理」 |

> **「放單」≠「撳單」**:放單=由送貨訂單產生提料票;撳單=倉庫領取該票開始出倉。

建議閱讀順序:送貨訂單篩選 → 放單 → 撳單 → 掃碼出倉 → 填箱數列印。

---

## 一、整體流程

```text
【送貨訂單 /do】篩選「2/F」「4/F」「車線-X」「加單」
↓ 「批量放單」或詳情「放單」
【產生提料單/提票】狀態進入待撳單
【成品出倉 /doworkbench】「撳單/提料單詳情」
↓ 選日期/批量|單量/樓層票 → 點車線「確認分配」
【掃碼提料】「開始QR掃描」→「提交所有已掃描項目」
【成品提貨記錄】輸入「箱數」→「列印送貨單標籤」等
【查看提貨情況】核對「已完成」
```

---

## 一附、本地資料庫實例

> DO「來貨狀態」:`pending`→「待處理」、`receiving`→「接收中」、`completed`→「已完成」。
> 提票在「查看提貨情況」:`pending`→「待撳單」、`released`→「提貨中」、`completed`→「已完成」。

### 例 A:待放單/待出貨的送貨訂單(2026-07-30)

本機「預計送貨日期」**2026-07-30** 仍有多張「待處理」,例如:

| 「門店訂單編號」 | 「來貨狀態」 | 店鋪 |
|------------------|--------------|------|
| TOUR03PO26070304 | 待處理 | UR03 |
| TOCF28PO26070199 | 待處理 | CF28 |
| TOCF28PO26070200 | 待處理 | CF28 |
| TOCF02PO26070176 | 待處理 | CF02 |
| TOCF18PO26070194 | 待處理 | CF18 |

**怎麼練:**

1. 「送貨訂單」→ 選樓層分頁 →「預計送貨日期」填 **2026-07-30** → 搜索。
2. 找上表編號點「詳情」看行項與「庫存可用」。
3. 測試庫才建議真的「放單」/「批量放單」(會產生提料票)。

### 例 B:近一週 DO 狀態量級(本機 2026-07-28~08-05)

| 「來貨狀態」 | 約筆數 | 使用者常做 |
|--------------|--------|------------|
| 待處理 | 1,425 | 篩選後「批量放單」 |
| 已完成 | 1,059 | 查歷史/補貨原單 |
| 接收中 | 96 | 出倉進行中對應 |

### 例 C:提料票(撳單前「待撳單」風格樣本)

本機較早提票例(`do_pick_order`,狀態 pending≈待撳/待處理):

| 「提票號碼」 | 車線資訊 | 店鋪 | 「需求日期」 | 放單類型 |
|--------------|----------|------|--------------|----------|
| TI-S-20260504-4F-001 | P06B_Sat_區1_港島東 | MC49 | 2026-05-04 | single(單量) |
| TI-S-20260504-2F-001 | 車線-F1 | MC49 | 2026-05-04 | single |
| TI-S-20260504-2F-001 | 車線-X | HP65 | 2026-05-04 | single |
| TI-S-20260505-4F-002 | P06B_Tue_區5_九龍中 | HP15 | 2026-05-05 | single |

**怎麼練:**「成品出倉」→「撳單/提料單詳情」→ 日期選對應「是日/翌日…」;或「成品提貨記錄(全部)」用「提票號碼」搜索。注意「車線-X」會獨立分組。

### 自行查核用 SQL(選用)

```sql
SELECT d.code, d.status, DATE(d.estimatedArrivalDate) AS eta, s.code AS shop
FROM delivery_order d
LEFT JOIN shop s ON s.id = d.shopId
WHERE d.deleted = 0 AND DATE(d.estimatedArrivalDate) = '2026-07-30'
ORDER BY d.id DESC
LIMIT 20;

SELECT ticket_no, TruckLanceCode, ticket_status, ShopCode,
DATE(RequiredDeliveryDate) AS req_date, release_type
FROM do_pick_order
WHERE deleted = 0 AND RequiredDeliveryDate >= '2026-05-01'
ORDER BY id DESC
LIMIT 20;
```

---

## 二、送貨訂單(`/do`)

### 2.1 入口與分頁

- 側欄 **「送貨訂單」**。

| 分頁 | 用途 |
|------|------|
| 「2/F」「4/F」 | 依樓層票別看/放單 |
| 「車線-X」 | 無匹配車線或歸入 X 的訂單 |
| 「加單」 | isExtra 加單;批量放單可合併 |
| 「補貨」 | 已完成原單補到目標單 |

### 2.2 搜索欄

- 「門店訂單編號」「店鋪名稱」「車線號碼」「預計送貨日期」「來貨狀態」
- 狀態選項:「待處理」「接收中」「已完成」(及「全部」)
- 注意:「已填寫車線號碼時,請一併選擇預計送貨日期後再搜索。」/「需選擇預計送貨日期」

### 2.3 結果表常見欄

「詳情」「門店訂單編號」「店鋪名稱」「供應商名稱」「車線號碼」「訂單日期」「預計送貨日期」「來貨狀態」。

### 2.4 詳情頁(`/do/edit?id=`)

- 標題:「編輯送貨訂單詳情」
- 動作:「放單」「提料單分配」「分配2/F」「分配4/F」「放單2/F」「放單4/F」「返回」
- 行表:「商品編號」「貨品名稱」「數量」「庫存可用」「庫存狀態」
- 單張成功提示:「送貨訂單放單成功!提料單已建立。」

---

## 三、放單詳解

### 3.1 批量放單(常用)

1. 在「送貨訂單」搜出目標日/樓層的列,勾選需要的店(可取消勾選排除)。
2. 點 **「批量放單」**。
3. 對話框顯示「已選擇店舖數量: N」;「確認」執行。
4. **「加單」分頁**額外選項:
- 「確認合併放單」(合併同車線 → TI-M- 合併票;文案含「合併同車線送貨訂單(TI-M- 合併票)」)
- 「確認不放合併放單」
5. 成功:「已完成批量放單」。

Workbench 路徑按鈕亦可能顯示為「批量放單」(鍵名 Workbench Batch Release)。

### 3.2 單張放單

詳情頁「放單」,或先「分配2/F/4/F」再「放單2/F/4/F」。

### 3.3 放單前/失敗常見提示

| 提示 | 處理 |
|------|------|
| 「沒有選擇送貨訂單進行批量放單…」 | 先勾選列 |
| 「車線可用性警告」「問題送貨訂單」 | 核對目標日是否有車線;或走「車線-X」 |
| 「放單提料單失敗,請稍後再試。」 | 稍後重試;查該店是否已放過 |

---

## 四、成品出倉(`/doworkbench`)— 主路徑

### 4.1 入口

- 「倉庫管理」→ **「成品出倉」**(現行主選單指向 `/doworkbench`)。
- 舊頁 `/finishedGood` 標題同為「成品出倉」,一般以 Workbench 為準。

### 4.2 頁頂打印機

- 「A4 打印機」「標籤打印機」「列印全部草稿 (N)」
- 未選機:「請先選擇打印機」/「請先選擇標籤打印機」

### 4.3 分頁一覽

| tab | 標籤 |
|-----|------|
| 0 | 「撳單/提料單詳情」 |
| 1 | 「加單」(徽章:當日未完成加單票數) |
| 2 | 「成品提貨記錄」 |
| 3 | 「成品提貨記錄(全部)」 |
| 4 | 「查看提貨情況」 |
| 5 | 「成品出倉出箱數量」 |
| 6 | 「送貨路線摘要」 |

---

## 五、撳單(領票)

### 5.1 條件列

- 「請選擇日期」:「是日」「翌日」「後日」
- 「放單類型」:「批量」「單量」
- 「票別(樓層)」:「2/F 票」「4/F 票」

### 5.2 車線面板

1. 車線按鈕顯示「(未撳數/總單數)」、裝載序/出發時間等。
2. 點車線 →「確認分配」(含「位置」「車線號碼」「裝載順序」「出發時間」「所需日期」「可用訂單」)→「確認」。
3. 成功後進入提料明細掃碼。
4. 無單時:「該樓層未有需處理訂單」/「此樓層沒有可用的提料單」。
5. 「未完成提料單」可搜商店/車線/送貨單編號再「選擇」。
6. **限制:**「請先完成目前的提料單,再提取下一張」。

### 5.3 「車線-X」

- DO 與出倉皆有獨立「車線-X」區塊;無匹配車線時顯示「車線-X」。
- 「以前」:今日前未完成的車線-X。
- 出箱儀表會統計「車線-X 出箱數」。

---

## 六、掃碼出倉(提料執行)

1. 在已撳單的明細中看「所有提料單批號」「進度」。
2. 「開始QR掃描」/「停止QR掃描」;「掃描結果」正確時「二維碼驗證成功。」
3. 可「改數」「提交數量」;問題回報含不良/遺失等。
4. 「提交所有已掃描項目」;可選「列印空白頁數標籤」。
5. 無掃碼可直接完成的列可用「已完成」(Just Completed)。
6. 全部完成後通常導向「成品提貨記錄」(帶提票號)。

### 掃碼常見錯誤

| 提示 | 處理 |
|------|------|
| 「二維碼不符合當前訂單中的任何貨品。」 | 確認掃的是本票貨品 |
| 「此批號不可用…」「此批次尚未上架」 | 換批或先完成上架 |
| 「此批號單位不符…」「此批號已提貨…」 | 換批 |
| 「掃描批號已過期…」 | 換未過期批 |
| 「此批次貨品已被其他送貨單留起…」 | 換批或協調留貨 |
| 換批雙掃說明 | 依畫面再掃一次確認 |

---

## 七、加單專章

### 7.1 在「送貨訂單」

- 開「加單」分頁搜索與「批量放單」。
- 合併選項見 §3.1(TI-M- 合併票)。

### 7.2 在「成品出倉」

1. Tab「加單」;進入前確認:「進入加單檢視?」
2. 說明:「加單檢視會依選定日期,將 isExtra 票依店鋪與車線顯示。」
3. 「目前是加單票,顯示與操作已切換為加單模式。」/「離開加單檢視」「返回一般指派分頁」。
4. 「合併加單提料單」:僅「未分配」且同店鋪、樓層(2/F、4/F 或車線-X)、車線、出發時間可合併。
5. 類型顯示可能為「合拼單」「加單」「批量」「單量」。

---

## 八、補貨(「送貨訂單」→「補貨」)

1. 「補貨填表」「對單」→「待提交列表」→「提交」/「清空」。
2. 「送貨單號末四位」「原送貨單」「目標送貨單」「補貨數量」「原出貨數」「車線」。
3. 無車線時畫面可能顯示「車線-X」。
4. 「補貨進度追蹤」:待處理/處理中/已完成。
5. 限制例:「只有已送貨(completed)的送貨單可作為原送貨單。」「補貨數量必須大於零」。

---

## 九、箱數與列印

### 9.1 草稿/空白

- 「列印全部草稿 (N)」→ 確認「確認列印全部草稿?(總數量:N份)」→「成功列印」。
- 提料中:「列印空白頁數標籤」→「請輸入要列印的標籤數量:」。

### 9.2 完成後正式列印(「成品提貨記錄」)

1. 「查看詳情」或列表動作。
2. 「列印提料單」「列印送貨單標籤」「列印提料單和送貨單標籤」「補印標籤」。
3. 彈窗「請輸入總箱數」,欄位「箱數」(≥1)。
4. 成功:「成功列印」。

### 9.3 補印

- 「補印送貨單標籤」:「起始箱號」「結束箱號」「總箱數」
- 校驗:起始≥1、結束≥起始、結束≤總箱數等。

### 9.4 送貨路線摘要

- Tab「送貨路線摘要」→ 選「車線」→「下載報告 (PDF)」。
- 若未執完:「此車線仍有 N 張訂單未執拾。是否仍要列印 / 下載送貨路線摘要?」

### 9.5 出箱數量

- Tab「成品出倉出箱數量」:按日「2/F 出箱數」「4/F 出箱數」「車線-X 出箱數」「總出箱數」(來自完成時填的箱數)。

---

## 十、查看提貨情況與管理動作

### 10.1 查詢

- 「目標日期」「重新載入」「樓層」「狀態」(「待撳單」「提貨中」「已完成」)。
- 欄含貨車/車線/裝載順序/提票號碼/負責員工/訂單項目數量等。

### 10.2 管理(常需 ADMIN)

| 動作 | 意義(畫面說明意涵) |
|------|----------------------|
| 「撤銷領取」 | 清空負責人,單據回待分配,他人可再領 |
| 「強制完成提貨單」 | 標完成並歸檔,不改已揀數量;適用已全部提交但系統未結案 |

未授權:「僅管理員(ADMIN 權限)可使用」。

---

## 十一、狀態對照(避免搞混三套名稱)

### 11.1 送貨訂單「來貨狀態」

| 鍵 | 畫面 |
|----|------|
| pending | 「待處理」 |
| receiving | 「接收中」 |
| completed | 「已完成」 |
| (部分流程)released / picking | 「已放單」/「提料中」 |

### 11.2 「查看提貨情況」提票狀態

| 鍵 | 畫面 | 白話 |
|----|------|------|
| pending | 「待撳單」 | 已放單、尚未領取 |
| released | 「提貨中」 | 已撳單/出倉中 |
| completed | 「已完成」 | 提貨完成 |

### 11.3 提料單通用(pickOrder)

「待處理」「已放單」「提料中」「已完成」——與上表用詞接近但場景不同;操作時以**目前所在分頁**的 Chip 文案為準。

---

## 十二、成品出倉管理(ADMIN)

- 「倉庫管理」→「成品出倉管理」(`/finishedGood/management`)。
- 「提料順序」:上移/下移/置頂/置底、「新增物品」「儲存」「重新載入」。
- 「出貨倉位」「入貨倉位」等主檔維護。

---

## 十三、路徑速查

| 畫面 | 路徑 |
|------|------|
| 送貨訂單 | `/do` |
| DO 詳情/放單 | `/do/edit?id=` |
| 成品出倉(主) | `/doworkbench` |
| 成品出倉管理 | `/finishedGood/management` |
| 舊成品出倉 | `/finishedGood`(附錄對照用) |

---

## 十四、常見問題速查

| 情況 | 建議 |
|------|------|
| 批量放單後倉庫看不到票 | 核對日期「是日/翌日」、樓層票別、批量/單量、是否加單檢視 |
| 車線按鈕 0 單 | 換日期/樓層;查「車線-X」「未完成提料單」 |
| 掃碼一直失敗 | 批號是否上架、是否被留貨/過期/單位不符 |
| 印不出標籤 | 先選 A4/標籤打印機;完成後記得填「箱數」 |
| 加單與正單混在一起 | 明確進/出「加單」檢視;合併規則要同店同線同時段 |
| 畫面突然英文 | 少數錯誤字串尚未進 zh,以實機為準並回報補譯 |

+ 345
- 0
scripts/export_m18_mapping_office.py 파일 보기

@@ -0,0 +1,345 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Export MTMS ↔ M18 mapping docs to Word (.docx) and Excel (.xlsx).

Prereqs:
pip install python-docx openpyxl

Usage (from repo root):
python scripts/export_m18_mapping_office.py

Outputs:
docs/exports/MTMS_M18_DATA_MAPPING.docx
docs/exports/MTMS_M18_DATA_MAPPING.xlsx
"""

from __future__ import annotations

import re
from datetime import datetime, timezone
from pathlib import Path

from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter

ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"
GEN = DOCS / "generated"
OUT = DOCS / "exports"
HANDBOOK = DOCS / "MTMS_M18_DATA_MAPPING.md"
ITEM_TYPE_MD = GEN / "m18-item-type-mapping.md"
STSEARCH_MD = GEN / "m18-stsearch-types.md"


def strip_md_inline(s: str) -> str:
s = s.strip()
s = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", s) # links
s = s.replace("**", "").replace("`", "").replace("*", "")
return s.strip()


def parse_md_tables(text: str) -> list[tuple[str, list[str], list[list[str]]]]:
"""
Return list of (section_title, headers, rows) for each markdown table.
section_title = nearest preceding ## / ### heading.
"""
lines = text.splitlines()
current_h = ""
tables: list[tuple[str, list[str], list[list[str]]]] = []
i = 0
while i < len(lines):
line = lines[i]
if line.startswith("#"):
current_h = strip_md_inline(re.sub(r"^#+\s*", "", line))
i += 1
continue
if line.strip().startswith("|") and i + 1 < len(lines) and re.match(
r"^\|[\s\-:|]+\|$", lines[i + 1].strip()
):
header = [strip_md_inline(c) for c in line.strip().strip("|").split("|")]
i += 2
rows: list[list[str]] = []
while i < len(lines) and lines[i].strip().startswith("|"):
row = [strip_md_inline(c) for c in lines[i].strip().strip("|").split("|")]
rows.append(row)
i += 1
tables.append((current_h, header, rows))
continue
i += 1
return tables


def add_runs_with_code(paragraph, text: str) -> None:
"""Simple split on backticks for monospace-ish plain text."""
parts = re.split(r"`([^`]+)`", text)
for idx, part in enumerate(parts):
if not part:
continue
run = paragraph.add_run(part)
run.font.name = "Calibri"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei")
if idx % 2 == 1:
run.font.name = "Consolas"
run.font.size = Pt(9)


def md_to_docx(md_path: Path, out_path: Path, extra_md_files: list[Path] | None = None) -> None:
doc = Document()
section = doc.sections[0]
section.top_margin = Cm(2)
section.bottom_margin = Cm(2)
section.left_margin = Cm(2.2)
section.right_margin = Cm(2.2)

style = doc.styles["Normal"]
style.font.name = "Calibri"
style.font.size = Pt(11)
style._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei")

files = [md_path] + (extra_md_files or [])
first = True
for path in files:
if not path.is_file():
continue
if not first:
doc.add_page_break()
first = False
_append_md_file(doc, path)

footer = doc.sections[0].footer.paragraphs[0]
footer.text = (
f"MTMS ↔ M18 mapping · exported {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}"
)
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER

out_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(out_path)
print(f"Wrote {out_path.relative_to(ROOT)}")


def _append_md_file(doc: Document, path: Path) -> None:
lines = path.read_text(encoding="utf-8").splitlines()
i = 0
in_code = False
code_buf: list[str] = []

while i < len(lines):
line = lines[i]

if line.startswith("<!--"):
i += 1
continue

if line.strip().startswith("```"):
if not in_code:
in_code = True
code_buf = []
else:
in_code = False
p = doc.add_paragraph()
run = p.add_run("\n".join(code_buf))
run.font.name = "Consolas"
run.font.size = Pt(9)
i += 1
continue

if in_code:
code_buf.append(line)
i += 1
continue

if line.startswith("#"):
level = len(re.match(r"^#+", line).group(0))
text = strip_md_inline(re.sub(r"^#+\s*", "", line))
if level == 1:
doc.add_heading(text, level=0)
else:
doc.add_heading(text, level=min(level, 3))
i += 1
continue

if line.strip().startswith("|") and i + 1 < len(lines) and re.match(
r"^\|[\s\-:|]+\|$", lines[i + 1].strip()
):
header = [strip_md_inline(c) for c in line.strip().strip("|").split("|")]
i += 2
rows: list[list[str]] = []
while i < len(lines) and lines[i].strip().startswith("|"):
rows.append(
[strip_md_inline(c) for c in lines[i].strip().strip("|").split("|")]
)
i += 1
table = doc.add_table(rows=1 + len(rows), cols=len(header))
table.style = "Table Grid"
for c, h in enumerate(header):
cell = table.rows[0].cells[c]
cell.text = h
for p in cell.paragraphs:
for r in p.runs:
r.bold = True
for r_idx, row in enumerate(rows):
for c, val in enumerate(row):
if c < len(header):
table.rows[r_idx + 1].cells[c].text = val
doc.add_paragraph()
continue

if line.strip().startswith("> "):
p = doc.add_paragraph()
p.paragraph_format.left_indent = Cm(0.5)
add_runs_with_code(p, strip_md_inline(line.strip()[2:]))
for r in p.runs:
r.italic = True
i += 1
continue

if re.match(r"^[-*]\s+", line.strip()):
text = strip_md_inline(re.sub(r"^[-*]\s+", "", line.strip()))
p = doc.add_paragraph(style="List Bullet")
add_runs_with_code(p, text)
i += 1
continue

if re.match(r"^\d+\.\s+", line.strip()):
text = strip_md_inline(re.sub(r"^\d+\.\s+", "", line.strip()))
p = doc.add_paragraph(style="List Number")
add_runs_with_code(p, text)
i += 1
continue

if line.strip() == "" or line.strip() == "---":
i += 1
continue

p = doc.add_paragraph()
add_runs_with_code(p, strip_md_inline(line))
i += 1


def write_xlsx(out_path: Path) -> None:
wb = Workbook()
# remove default later if we create named sheets first
header_fill = PatternFill("solid", fgColor="D9D9D9")
header_font = Font(bold=True, name="Calibri", size=11)
thin = Border(
left=Side(style="thin", color="B0B0B0"),
right=Side(style="thin", color="B0B0B0"),
top=Side(style="thin", color="B0B0B0"),
bottom=Side(style="thin", color="B0B0B0"),
)
wrap = Alignment(wrap_text=True, vertical="center")

def style_sheet(ws, headers: list[str], rows: list[list[str]], title: str) -> None:
ws["A1"] = title
ws["A1"].font = Font(bold=True, size=14, name="Calibri")
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=max(len(headers), 1))
ws["A2"] = f"Exported {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}"
ws["A2"].font = Font(italic=True, color="666666", size=9)

start = 4
for c, h in enumerate(headers, 1):
cell = ws.cell(start, c, h)
cell.fill = header_fill
cell.font = header_font
cell.border = thin
cell.alignment = Alignment(wrap_text=True, vertical="center", horizontal="center")
for r_idx, row in enumerate(rows, start + 1):
for c, val in enumerate(row, 1):
cell = ws.cell(r_idx, c, val)
cell.border = thin
cell.alignment = wrap
for c in range(1, len(headers) + 1):
maxlen = len(headers[c - 1])
for row in rows:
if c - 1 < len(row):
maxlen = max(maxlen, len(row[c - 1]))
ws.column_dimensions[get_column_letter(c)].width = min(max(12, maxlen + 2), 48)

# Collect tables from generated + handbook
sheets_spec: list[tuple[str, Path]] = [
("ItemType_Sync", ITEM_TYPE_MD),
("StSearch", STSEARCH_MD),
("Handbook_Tables", HANDBOOK),
]

first = True
for sheet_name, md_path in sheets_spec:
if not md_path.is_file():
continue
tables = parse_md_tables(md_path.read_text(encoding="utf-8"))
if sheet_name == "Handbook_Tables":
# one sheet per handbook table (limited name length)
for idx, (sec, headers, rows) in enumerate(tables, 1):
name = f"H{idx}_{sec[:20]}" if sec else f"H{idx}"
name = re.sub(r"[\\/*?:\[\]]", "_", name)[:31]
ws = wb.active if first else wb.create_sheet(name)
if first:
ws.title = name
first = False
style_sheet(ws, headers, rows, f"{sec or 'Table'} (from handbook)")
continue

# For generated files: put Sync mapping as main sheet; other tables as extra sheets
if not tables:
continue
if first:
ws = wb.active
ws.title = sheet_name[:31]
first = False
else:
ws = wb.create_sheet(sheet_name[:31])

# Prefer table titled Sync mapping / first table
main = next((t for t in tables if "Sync" in t[0] or "mapping" in t[0].lower()), tables[0])
style_sheet(ws, main[1], main[2], main[0] or sheet_name)

for sec, headers, rows in tables:
if (sec, headers, rows) == main:
continue
extra_name = re.sub(r"[\\/*?:\[\]]", "_", f"{sheet_name[:8]}_{sec}")[:31]
ws2 = wb.create_sheet(extra_name)
style_sheet(ws2, headers, rows, sec or extra_name)

# Readme sheet
ws = wb.create_sheet("README", 0)
ws["A1"] = "MTMS (FPSMS) ↔ M18 資料對照 — Excel 匯出"
ws["A1"].font = Font(bold=True, size=14)
ws["A3"] = "來源"
ws["B3"] = "docs/MTMS_M18_DATA_MAPPING.md + docs/generated/*.md"
ws["A4"] = "重新產生 Markdown 表"
ws["B4"] = "python scripts/generate_m18_mapping_docs.py"
ws["A5"] = "重新匯出 Word/Excel"
ws["B5"] = "python scripts/export_m18_mapping_office.py"
ws["A7"] = "說明"
ws["B7"] = (
"對照表以 sheet 分開;完整敘述請看 Word 檔 MTMS_M18_DATA_MAPPING.docx。"
"已知陷阱:M18 udfProducttype=CMB 會落到 items.type=mat(原料)。"
)
ws.column_dimensions["A"].width = 28
ws.column_dimensions["B"].width = 80
for r in range(3, 8):
ws.cell(r, 2).alignment = wrap

out_path.parent.mkdir(parents=True, exist_ok=True)
wb.save(out_path)
print(f"Wrote {out_path.relative_to(ROOT)}")


def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
md_to_docx(
HANDBOOK,
OUT / "MTMS_M18_DATA_MAPPING.docx",
extra_md_files=[ITEM_TYPE_MD, STSEARCH_MD],
)
write_xlsx(OUT / "MTMS_M18_DATA_MAPPING.xlsx")


if __name__ == "__main__":
main()

+ 193
- 0
scripts/export_user_guide_office.py 파일 보기

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Export user-facing guides (Markdown) to Word (.docx).

Usage (from repo root):
pip install python-docx
python scripts/export_user_guide_office.py

Outputs under docs/exports/
"""

from __future__ import annotations

import re
from datetime import datetime, timezone
from pathlib import Path

from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.shared import Cm, Pt

ROOT = Path(__file__).resolve().parents[1]
GUIDE_DIR = ROOT / "docs" / "user-guides"
OUT_DIR = ROOT / "docs" / "exports"

GUIDES = [
# (markdown path, output docx filename — English names avoid Windows garbling)
(
GUIDE_DIR / "MTMS_排程與工單_使用說明.md",
"MTMS_Schedule_JobOrder_UserGuide.docx",
),
(
GUIDE_DIR / "MTMS_BOM_使用說明.md",
"MTMS_BOM_UserGuide.docx",
),
(
GUIDE_DIR / "MTMS_工單提料報工上架_使用說明.md",
"MTMS_JO_Pick_Production_PutAway_UserGuide.docx",
),
(
GUIDE_DIR / "MTMS_送貨訂單與出貨_使用說明.md",
"MTMS_DO_Shipping_UserGuide.docx",
),
]


def strip_md_inline(s: str) -> str:
s = s.strip()
s = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", s)
s = s.replace("**", "").replace("`", "").replace("*", "")
return s.strip()


def add_runs(paragraph, text: str) -> None:
parts = re.split(r"(「[^」]+」)", text)
for part in parts:
if not part:
continue
run = paragraph.add_run(part)
run.font.name = "Calibri"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei")
if part.startswith("「") and part.endswith("」"):
run.bold = True
run.font.color.rgb = None


def md_to_docx(md_path: Path, out_path: Path) -> None:
doc = Document()
section = doc.sections[0]
section.top_margin = Cm(2)
section.bottom_margin = Cm(2)
section.left_margin = Cm(2.2)
section.right_margin = Cm(2.2)

style = doc.styles["Normal"]
style.font.name = "Calibri"
style.font.size = Pt(11)
style._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei")

lines = md_path.read_text(encoding="utf-8").splitlines()
i = 0
in_code = False
code_buf: list[str] = []

while i < len(lines):
line = lines[i]

if line.strip().startswith("```"):
if not in_code:
in_code = True
code_buf = []
else:
in_code = False
p = doc.add_paragraph()
run = p.add_run("\n".join(code_buf))
run.font.name = "Consolas"
run.font.size = Pt(9)
i += 1
continue

if in_code:
code_buf.append(line)
i += 1
continue

if line.startswith("#"):
level = len(re.match(r"^#+", line).group(0))
text = strip_md_inline(re.sub(r"^#+\s*", "", line))
doc.add_heading(text, level=0 if level == 1 else min(level, 3))
i += 1
continue

if line.strip().startswith("|") and i + 1 < len(lines) and re.match(
r"^\|[\s\-:|]+\|$", lines[i + 1].strip()
):
header = [strip_md_inline(c) for c in line.strip().strip("|").split("|")]
i += 2
rows: list[list[str]] = []
while i < len(lines) and lines[i].strip().startswith("|"):
rows.append(
[strip_md_inline(c) for c in lines[i].strip().strip("|").split("|")]
)
i += 1
table = doc.add_table(rows=1 + len(rows), cols=len(header))
table.style = "Table Grid"
for c, h in enumerate(header):
cell = table.rows[0].cells[c]
cell.text = h
for p in cell.paragraphs:
for r in p.runs:
r.bold = True
for r_idx, row in enumerate(rows):
for c, val in enumerate(row):
if c < len(header):
table.rows[r_idx + 1].cells[c].text = val
doc.add_paragraph()
continue

if line.strip().startswith("> "):
p = doc.add_paragraph()
p.paragraph_format.left_indent = Cm(0.4)
add_runs(p, strip_md_inline(line.strip()[2:]))
for r in p.runs:
r.italic = True
i += 1
continue

if re.match(r"^[-*]\s+", line.strip()):
p = doc.add_paragraph(style="List Bullet")
add_runs(p, strip_md_inline(re.sub(r"^[-*]\s+", "", line.strip())))
i += 1
continue

if re.match(r"^\d+\.\s+", line.strip()):
p = doc.add_paragraph(style="List Number")
add_runs(p, strip_md_inline(re.sub(r"^\d+\.\s+", "", line.strip())))
i += 1
continue

if line.strip() in ("", "---"):
i += 1
continue

p = doc.add_paragraph()
add_runs(p, strip_md_inline(line))
i += 1

footer = doc.sections[0].footer.paragraphs[0]
footer.text = (
f"MTMS 使用說明 · {md_path.name} · "
f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}"
)
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER

out_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(out_path)
print(f"Wrote {out_path.relative_to(ROOT)}")


def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
for guide, out_name in GUIDES:
if not guide.is_file():
print(f"Skip missing: {guide}")
continue
out = OUT_DIR / out_name
md_to_docx(guide, out)


if __name__ == "__main__":
main()

+ 505
- 0
scripts/generate_commit_test_plans_docx.py 파일 보기

@@ -0,0 +1,505 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Auto-generate a Word (.docx) test plan for each commit in a git range.

Usage (from repo root):
pip install python-docx
python scripts/generate_commit_test_plans_docx.py HEAD~10..HEAD
python scripts/generate_commit_test_plans_docx.py abc123..def456 --out-dir docs/deploy/commit-plans
python scripts/generate_commit_test_plans_docx.py HEAD~5..HEAD --also-md

Output folder (default): docs/deploy/commit-plans/
<date>_<shortsha>_<slug>.docx — one file per commit
_index.md — list of generated plans

Notes:
- Plans are heuristic from commit message + touched paths (good starting point for QA).
- For critical releases, ask the agent to refine steps against the actual diff.
"""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path

from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor

ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUT = ROOT / "docs" / "deploy" / "commit-plans"

# (path substring, area label, suggested steps, expected results)
AREA_RULES: list[tuple[str, str, list[tuple[str, str]]]] = [
(
"deliveryOrder",
"送貨訂單 / 成品出倉",
[
("「送貨訂單」依預計送貨日搜索相關單,必要時「批量放單」或詳情「放單」。", "放單成功;產生提料票,無未預期 500。"),
("「成品出倉」`/doworkbench`:撳單 → 掃碼提料 → 填箱數列印。", "票可撳、可提、可完成;狀態「待撳單」→「提貨中」→「已完成」。"),
("若涉及加單/車線-X:開「加單」分頁與「車線-X」對照。", "票出現在正確樓層/車線;指派篩選與顯示一致。"),
],
),
(
"pickOrder",
"提料單 / Workbench",
[
("「成品出倉」撳單並完成一張提料票(測試庫)。", "掃碼/提交正常;完成後記錄頁可見。"),
("「查看提貨情況」核對該票狀態。", "狀態與負責人符合操作。"),
],
),
(
"SuggestedPickLot",
"建議批號 / UOM",
[
("出倉或提料時掃建議批號;刻意掃 UOM 不符批號。", "不符 UOM 被擋並有明確提示;相符批號可提交。"),
("標籤列印/批號列表只應出現同 UOM 選項(若本次改動涵蓋)。", "列表無錯誤 UOM 批號。"),
],
),
(
"job_order",
"工單",
[
("「搜索工單/ 建立工單」建立或搜索受影響工單。", "列表/詳情資料正確。"),
("依狀態走「放單」→「工單提料」或「工單生產流程」。", "狀態轉換符合預期,無阻塞錯誤。"),
],
),
(
"JobOrder",
"工單",
[
("「搜索工單/ 建立工單」驗證建立/搜索/放單。", "成功提示;狀態正確。"),
],
),
(
"modules/bom",
"BOM",
[
("「設定」→「匯入 BOM」/「BOM 明細」搜索受影響成品。", "可載入明細;啟用狀態正確。"),
("「建立工單」下拉是否仍能選該 BOM。", "啟用可選、停用不可選。"),
],
),
(
"production_schedule",
"排程",
[
("「排程」→「生產排程」搜索相關生產日 →「詳細」。", "明細數量合理。"),
("(測試庫)「自動生成工單」。", "產生工單或明確錯誤訊息。"),
],
),
(
"ProductionSchedule",
"排程",
[
("「排程」頁驗證預測/搜索/詳細。", "畫面與 API 正常。"),
],
),
(
"stock",
"庫存 / 上架 / 出入倉",
[
("依改動點進「上架掃碼」或相關庫存查詢頁。", "掃碼/查詢結果與庫存數量合理。"),
("做一筆小量入/出/調撥(測試庫)。", "成功;庫存異動可查。"),
],
),
(
"StockIn",
"來貨 / 品檢",
[
("「工單生產流程」→「品檢」或待 QC 列表。", "可開品檢;確定後狀態更新。"),
],
),
(
"putAway",
"上架",
[
("「上架掃碼」:貨品 QR → 倉庫 QR →「確定及上架貨物」。", "上架成功;待上架數量減少。"),
],
),
(
"m18",
"M18 同步",
[
("對受影響主檔執行同步或查看最近同步結果(測試環境)。", "對應欄位寫入 MTMS;錯誤有日誌。"),
],
),
(
"onpack2030",
"標籤 / OnPack",
[
("對新增/修改的貨品編號列印標籤(測試機)。", "圖檔/job 正確;可印出。"),
],
),
(
"db/changelog",
"資料庫變更",
[
("部署後確認 Liquibase/changelog 已套用(或啟動 log 無 changeset 失敗)。", "DB schema/資料符合 changeset。"),
("用相關畫面或 SQL 抽樣驗證新欄位/約束。", "讀寫正常,無缺欄錯誤。"),
],
),
(
"modules/master",
"主檔 (Item/Shop 等)",
[
("主檔搜索受影響編號,核對顯示欄位。", "名稱/單位/類型等與預期一致。"),
],
),
(
"Inventory",
"庫存查詢",
[
("庫存搜索頁用受影響貨品/倉位查詢。", "批號、數量、單位正確。"),
],
),
]

KEYWORD_HINTS: list[tuple[re.Pattern[str], tuple[str, str]]] = [
(
re.compile(r"uom|單位", re.I),
("掃一筆 UOM 不符的批號/物料。", "系統拒絕或明確提示;相符者可過。"),
),
(
re.compile(r"isextra|etra|加單", re.I),
("「成品出倉」→「加單」檢視該日票。", "加單票可見且可撳單。"),
),
(
re.compile(r"truck\s*x|車線-?x|車線x", re.I),
("核對「車線-X」票在 2/F/4/F 顯示。", "出現在正確樓層區塊,可指派。"),
),
(
re.compile(r"label|print|列印|標籤|onpack", re.I),
("選打印機後列印標籤/送貨單標籤。", "成功列印;內容正確。"),
),
(
re.compile(r"fix|bug|repair|修正|修復", re.I),
("重現原問題步驟一次。", "問題不再出現;無新副作用。"),
),
]


@dataclass
class CommitPlan:
sha: str
short: str
subject: str
body: str
author: str
date: str
files: list[str] = field(default_factory=list)
areas: list[str] = field(default_factory=list)
tests: list[tuple[str, str]] = field(default_factory=list)


def run(cmd: list[str]) -> str:
r = subprocess.run(
cmd,
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if r.returncode != 0:
raise SystemExit((r.stderr or r.stdout or f"failed: {cmd}").strip())
return r.stdout.strip()


def list_commits(rev_range: str) -> list[str]:
out = run(["git", "log", "--reverse", "--format=%H", rev_range])
return [ln.strip() for ln in out.splitlines() if ln.strip()]


def load_commit(sha: str) -> CommitPlan:
subject = run(["git", "log", "-1", "--format=%s", sha])
body = run(["git", "log", "-1", "--format=%b", sha])
author = run(["git", "log", "-1", "--format=%an", sha])
date = run(["git", "log", "-1", "--format=%cs", sha])
files_raw = run(["git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha])
files = [f for f in files_raw.splitlines() if f.strip()]
plan = CommitPlan(
sha=sha,
short=sha[:8],
subject=subject or "(no message)",
body=(body or "").strip(),
author=author,
date=date,
files=files,
)
build_tests(plan)
return plan


def build_tests(plan: CommitPlan) -> None:
seen_areas: set[str] = set()
seen_steps: set[str] = set()
tests: list[tuple[str, str]] = []

def add(step: str, expected: str) -> None:
key = step.strip()
if key in seen_steps:
return
seen_steps.add(key)
tests.append((step, expected))

blob = " ".join(plan.files) + "\n" + plan.subject + "\n" + plan.body

for path_key, area, pairs in AREA_RULES:
if any(path_key in f.replace("\\", "/") for f in plan.files) or path_key.lower() in blob.lower():
if area not in seen_areas:
seen_areas.add(area)
plan.areas.append(area)
for step, exp in pairs:
add(step, exp)

for pat, pair in KEYWORD_HINTS:
if pat.search(blob):
add(pair[0], pair[1])

# Generic fallbacks
if not tests:
if plan.files:
add(
f"依 commit 變更檔抽樣驗證(共 {len(plan.files)} 個檔)。主要檔:{plan.files[0]}",
"相關 API/畫面無 500;行為符合 commit 說明。",
)
else:
add("確認此 commit 無業務檔變更(empty / merge)。", "無需功能測試或僅煙霧測試。")

add(
"部署後煙霧:登入系統,開側欄主要入口一次(排程/工單/送貨訂單/成品出倉)。",
"頁面可開、無全域錯誤橫幅。",
)
add(
"(回歸)與本改動相鄰但未改的主流程走一輪 Happy path。",
"無明顯回退。",
)

if not plan.subject or plan.subject.strip().lower() in ("no message", "(no message)"):
add(
"向作者確認此 commit 的實際意圖(訊息為 empty/no message)。",
"補上說明後再簽核上線。",
)

plan.tests = tests


def slugify(text: str, max_len: int = 40) -> str:
"""ASCII-only filename slug (avoids Windows console / zip garbling)."""
tokens = re.findall(r"[a-z0-9]+", text.lower())
seen: set[str] = set()
ordered: list[str] = []
for t in tokens:
if t in seen or len(t) < 2:
continue
seen.add(t)
ordered.append(t)
s = "_".join(ordered)[:max_len].strip("_")
return s or "commit"


def set_run_font(run, *, east_asia: str = "Microsoft JhengHei", ascii_font: str = "Calibri", size: Pt | None = None) -> None:
run.font.name = ascii_font
run._element.rPr.rFonts.set(qn("w:eastAsia"), east_asia)
if size is not None:
run.font.size = size


def add_para(doc: Document, text: str, *, bold: bool = False, italic: bool = False, size: Pt | None = None) -> None:
p = doc.add_paragraph()
run = p.add_run(text)
set_run_font(run, size=size or Pt(11))
run.bold = bold
run.italic = italic


def write_docx(plan: CommitPlan, out_path: Path) -> None:
doc = Document()
section = doc.sections[0]
section.top_margin = Cm(1.8)
section.bottom_margin = Cm(1.8)
section.left_margin = Cm(2)
section.right_margin = Cm(2)

style = doc.styles["Normal"]
style.font.name = "Calibri"
style.font.size = Pt(11)
style._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei")

title = doc.add_heading(level=0)
tr = title.add_run(f"Test plan — {plan.short}")
set_run_font(tr, size=Pt(18))

add_para(doc, plan.subject, bold=True, size=Pt(12))
meta = (
f"Commit: {plan.sha}\n"
f"Date: {plan.date} | Author: {plan.author}\n"
f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}"
)
add_para(doc, meta, italic=True, size=Pt(9))

doc.add_heading("Summary", level=1)
add_para(doc, plan.subject)
if plan.body:
add_para(doc, plan.body)

doc.add_heading("Areas (auto-detected)", level=1)
if plan.areas:
for a in plan.areas:
doc.add_paragraph(a, style="List Bullet")
else:
add_para(doc, "(No area rule matched — generic steps only)")

doc.add_heading("Files touched", level=1)
for f in plan.files[:40]:
doc.add_paragraph(f, style="List Bullet")
if len(plan.files) > 40:
add_para(doc, f"… and {len(plan.files) - 40} more files")

doc.add_heading("Test plan", level=1)
note = doc.add_paragraph()
nr = note.add_run(
"Auto-generated from paths + commit message. Refine before sign-off on critical deploys."
)
set_run_font(nr, size=Pt(9))
nr.italic = True
nr.font.color.rgb = RGBColor(0x66, 0x66, 0x66)

table = doc.add_table(rows=1 + len(plan.tests), cols=3)
table.style = "Table Grid"
headers = ["#", "Steps", "Expected result"]
for i, h in enumerate(headers):
cell = table.rows[0].cells[i]
cell.text = h
for p in cell.paragraphs:
for r in p.runs:
r.bold = True
set_run_font(r)

for idx, (step, exp) in enumerate(plan.tests, start=1):
row = table.rows[idx]
row.cells[0].text = str(idx)
row.cells[1].text = step
row.cells[2].text = exp
for c in row.cells:
for p in c.paragraphs:
for r in p.runs:
set_run_font(r, size=Pt(10))

doc.add_heading("Out of scope / notes", level=1)
add_para(
doc,
"Frontend-only changes may live in FPSMS-frontend — verify paired repo if UI behavior is expected.",
)
add_para(doc, "Rollback: revert this commit or redeploy previous backend build.")

footer = doc.sections[0].footer.paragraphs[0]
footer.text = f"MTMS commit test plan · {plan.short} · {plan.date}"
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER

out_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(out_path)


def write_md(plan: CommitPlan, out_path: Path) -> None:
lines = [
f"# Test plan — {plan.short}",
"",
f"**{plan.subject}**",
"",
f"- Commit: `{plan.sha}`",
f"- Date: {plan.date}",
f"- Author: {plan.author}",
"",
"## Areas",
"",
]
for a in plan.areas or ["(generic)"]:
lines.append(f"- {a}")
lines += ["", "## Files", ""]
for f in plan.files:
lines.append(f"- `{f}`")
lines += ["", "## Test plan", "", "| # | Steps | Expected result |", "|---|--------|-----------------|"]
for i, (s, e) in enumerate(plan.tests, 1):
lines.append(f"| {i} | {s.replace('|', '/')} | {e.replace('|', '/')} |")
lines += ["", "## Rollback", "", f"- Revert `{plan.short}` / previous build", ""]
out_path.write_text("\n".join(lines), encoding="utf-8")


def write_index(out_dir: Path, plans: list[CommitPlan], files: list[str]) -> None:
lines = [
"# Commit test plans (auto-generated)",
"",
f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
"",
"| Date | SHA | Subject | Word |",
"|------|-----|---------|------|",
]
for plan, name in zip(plans, files):
subj = plan.subject.replace("|", "/")
lines.append(f"| {plan.date} | `{plan.short}` | {subj} | [{name}]({name}) |")
lines.append("")
(out_dir / "_index.md").write_text("\n".join(lines), encoding="utf-8")


def main() -> None:
ap = argparse.ArgumentParser(description="Generate Word test plans per commit")
ap.add_argument(
"range",
help="Git revision range, e.g. HEAD~10..HEAD (use A..B exclusive-start semantics)",
)
ap.add_argument(
"--out-dir",
type=Path,
default=DEFAULT_OUT,
help=f"Output folder (default: {DEFAULT_OUT.relative_to(ROOT)})",
)
ap.add_argument("--also-md", action="store_true", help="Also write .md next to each .docx")
ap.add_argument(
"--limit",
type=int,
default=0,
help="Max commits to process (0 = all)",
)
args = ap.parse_args()

out_dir = args.out_dir if args.out_dir.is_absolute() else ROOT / args.out_dir
out_dir.mkdir(parents=True, exist_ok=True)

shas = list_commits(args.range)
if args.limit and args.limit > 0:
shas = shas[-args.limit :]
if not shas:
print("No commits in range.", file=sys.stderr)
sys.exit(1)

plans: list[CommitPlan] = []
names: list[str] = []
for sha in shas:
plan = load_commit(sha)
plans.append(plan)
# Stable name per commit (re-run overwrites same SHA)
fname = f"{plan.date}_{plan.short}_{slugify(plan.subject)}.docx"
target = out_dir / fname
write_docx(plan, target)
names.append(target.name)
print(f"Wrote {target.relative_to(ROOT)}")
if args.also_md:
md_path = target.with_suffix(".md")
write_md(plan, md_path)
print(f"Wrote {md_path.relative_to(ROOT)}")

write_index(out_dir, plans, names)
print(f"Wrote { (out_dir / '_index.md').relative_to(ROOT) }")
print(f"Done: {len(plans)} plan(s) → {out_dir.relative_to(ROOT)}")


if __name__ == "__main__":
main()

+ 120
- 0
scripts/generate_deploy_test_plan.py 파일 보기

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Scaffold a deploy / QA note from a git commit range.

Usage (from repo root):
python scripts/generate_deploy_test_plan.py HEAD~5..HEAD
python scripts/generate_deploy_test_plan.py abc1234..def5678 --out docs/deploy/20260801_example.md

The script lists commits and touched files. You (or the agent) still fill
test steps and expected results after reading the diffs.
"""

from __future__ import annotations

import argparse
import subprocess
import sys
from datetime import date
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def run(args: list[str]) -> str:
r = subprocess.run(
args,
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if r.returncode != 0:
raise SystemExit(r.stderr or r.stdout or f"command failed: {args}")
return r.stdout.strip()


def main() -> None:
p = argparse.ArgumentParser(description="Scaffold deploy test plan from git range")
p.add_argument(
"range",
help="Git revision range, e.g. HEAD~5..HEAD or origin/production..HEAD",
)
p.add_argument(
"--out",
type=Path,
default=None,
help="Optional output path under docs/deploy/",
)
p.add_argument("--title", default="Deploy note (draft)", help="Document title")
args = p.parse_args()

log = run(["git", "log", "--oneline", args.range])
if not log:
print("No commits in range.", file=sys.stderr)
sys.exit(1)

stat = run(["git", "diff", "--stat", args.range])
name_status = run(["git", "diff", "--name-status", args.range])

commits = [ln for ln in log.splitlines() if ln.strip()]
commit_bullets = "\n".join(f"- `{c[:7]}` — {c[8:]}" for c in commits)

body = f"""# {args.title}
Date: {date.today().isoformat()}
Branch / build: (fill)
Range: `{args.range}`
Author: (fill)

> Auto-scaffolded from git. **Replace the Test plan with real steps** after reviewing the diff.

## Summary
- (TODO: 1–3 bullets — user-facing impact)

## Scope
- Backend: (see files below)
- Frontend: (check paired repo if UI)
- DB / Liquibase: (none / list changelog files)
- Config / ops: (none / list)

## Commits
{commit_bullets}

## Files touched
```
{name_status}
```

### Diffstat
```
{stat}
```

## Test plan
| # | Steps (who / where / data) | Expected result |
|---|----------------------------|-----------------|
| 1 | TODO — happy path | TODO |
| 2 | TODO — edge / failure case | TODO |
| 3 | TODO — regression on related screen | TODO |

## Out of scope / not tested
- (TODO)

## Rollback
- Revert range `{args.range}` / redeploy previous build
"""

if args.out:
out = args.out if args.out.is_absolute() else ROOT / args.out
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(body, encoding="utf-8")
print(f"Wrote {out.relative_to(ROOT)}")
else:
sys.stdout.reconfigure(encoding="utf-8")
print(body)


if __name__ == "__main__":
main()

+ 239
- 0
scripts/generate_m18_mapping_docs.py 파일 보기

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Generate MTMS ↔ M18 mapping snippets from Kotlin source of truth.

Usage (from repo root):
python scripts/generate_m18_mapping_docs.py

Outputs:
docs/generated/m18-item-type-mapping.md
docs/generated/m18-stsearch-types.md

Re-run after changing:
- modules/master/web/models/NewItemRequest.kt (ItemType / M18ItemType)
- m18/model/M18MasterDataRequest.kt (StSearchType)
- m18/service/M18MasterDataService.kt (udfProducttype when-branches)
"""

from __future__ import annotations

import re
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "docs" / "generated"

NEW_ITEM_REQUEST = (
ROOT
/ "src/main/java/com/ffii/fpsms/modules/master/web/models/NewItemRequest.kt"
)
MASTER_DATA_REQUEST = (
ROOT / "src/main/java/com/ffii/fpsms/m18/model/M18MasterDataRequest.kt"
)
MASTER_DATA_SERVICE = (
ROOT / "src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt"
)
INVENTORY_I18N = (
ROOT.parent / "FPSMS-frontend" / "src" / "i18n" / "zh" / "inventory.json"
)

# UI labels when inventory.json is unavailable (fallback)
FALLBACK_UI = {
"mat": "原料",
"consumables": "消耗品",
"non-consumables": "非消耗品",
"fg": "成品",
"sfg": "半成品",
"item": "貨品",
"cmb": "消耗品",
"wip": "半成品",
"nm": "雜項及非消耗品",
}

# Known M18 udfProducttype values seen in the wild that are NOT in M18ItemType
# (documented as gaps so ops/dev notice).
KNOWN_UNMAPPED_M18_VALUES = [
("CMB", "Seen on M18 pro.udfProducttype (e.g. MG1852). Falls through to mat."),
]


def parse_kotlin_string_enum(text: str, enum_name: str) -> list[tuple[str, str]]:
"""Parse active (non-commented) `enum class Foo(...) { NAME("x"), ... }`."""
# Only match enum declarations that start a line (optional indent), not //enum
m = re.search(
rf"(?m)^[ \t]*enum class {re.escape(enum_name)}\([^)]*\)\s*\{{(.*?)^[ \t]*\}}",
text,
re.DOTALL,
)
if not m:
raise SystemExit(f"Could not find enum class {enum_name}")
body = m.group(1)
return re.findall(r"(\w+)\s*\(\s*\"([^\"]+)\"\s*\)", body)


def parse_producttype_when_branches(service_text: str) -> list[tuple[str, str]]:
"""
Extract first `when (pro.udfProducttype) { M18ItemType.X.type -> ItemType.Y.type ... }`
Returns list of (M18ItemTypeConst, ItemTypeConst).
"""
m = re.search(
r"when\s*\(\s*pro\.udfProducttype\s*\)\s*\{(.*?)else\s*->\s*ItemType\.(\w+)\.type",
service_text,
re.DOTALL,
)
if not m:
raise SystemExit("Could not find udfProducttype when-branch in M18MasterDataService")
body, else_item = m.group(1), m.group(2)
pairs = re.findall(
r"M18ItemType\.(\w+)\.type\s*->\s*ItemType\.(\w+)\.type",
body,
)
return pairs + [("__else__", else_item)]


def load_ui_labels() -> dict[str, str]:
labels = dict(FALLBACK_UI)
if not INVENTORY_I18N.is_file():
return labels
# Minimal JSON-ish extract of "key": "value" string pairs
text = INVENTORY_I18N.read_text(encoding="utf-8")
for k, v in re.findall(r'"([^"]+)"\s*:\s*"([^"]*)"', text):
labels[k] = v
return labels


def write_item_type_doc(
item_types: list[tuple[str, str]],
m18_types: list[tuple[str, str]],
when_pairs: list[tuple[str, str]],
ui: dict[str, str],
) -> None:
item_by_const = {c: v for c, v in item_types}
m18_by_const = {c: v for c, v in m18_types}
mapped_m18_consts = {a for a, b in when_pairs if a != "__else__"}

lines: list[str] = []
lines.append("<!-- AUTO-GENERATED by scripts/generate_m18_mapping_docs.py — do not edit by hand -->")
lines.append("")
lines.append("# M18 `udfProducttype` → MTMS `items.type`")
lines.append("")
lines.append(f"_Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}_")
lines.append("")
lines.append("**Source of truth**")
lines.append("")
lines.append("- Enums: `NewItemRequest.kt` → `ItemType`, `M18ItemType`")
lines.append("- Sync: `M18MasterDataService.saveProduct` / `saveProducts` (`when (pro.udfProducttype)`)")
lines.append("- UI labels (inventory): `FPSMS-frontend/src/i18n/zh/inventory.json`")
lines.append("")
lines.append("## Sync mapping")
lines.append("")
lines.append("| M18 `udfProducttype` (exact string) | `M18ItemType` | MTMS `items.type` | `ItemType` | Inventory UI (zh) |")
lines.append("|---|---|---|---|---|")

for m18_const, item_const in when_pairs:
if m18_const == "__else__":
mtms_val = item_by_const.get(item_const, "?")
lines.append(
f"| *(any other value / empty)* | — | `{mtms_val}` | `{item_const}` | {ui.get(mtms_val, '—')} |"
)
continue
m18_val = m18_by_const.get(m18_const, "?")
mtms_val = item_by_const.get(item_const, "?")
lines.append(
f"| `{m18_val}` | `{m18_const}` | `{mtms_val}` | `{item_const}` | {ui.get(mtms_val, '—')} |"
)

lines.append("")
lines.append("## Enum inventories")
lines.append("")
lines.append("### `M18ItemType`")
lines.append("")
lines.append("| Constant | String value | Used in sync `when`? |")
lines.append("|---|---|---|")
for c, v in m18_types:
used = "yes" if c in mapped_m18_consts else "**no**"
lines.append(f"| `{c}` | `{v}` | {used} |")

lines.append("")
lines.append("### `ItemType` (MTMS stored values)")
lines.append("")
lines.append("| Constant | `items.type` | Inventory UI (zh) |")
lines.append("|---|---|---|")
for c, v in item_types:
lines.append(f"| `{c}` | `{v}` | {ui.get(v, '—')} |")

lines.append("")
lines.append("## Known gaps (not auto-mapped)")
lines.append("")
lines.append("| M18 value seen | Effect | Notes |")
lines.append("|---|---|---|")
for val, note in KNOWN_UNMAPPED_M18_VALUES:
lines.append(f"| `{val}` | → `mat` (else) | {note} |")
lines.append("")
lines.append("Frontend Settings → Items edit also offers `cmb` / `wip` / `nm` as local types;")
lines.append("those are **not** written by the current M18 `udfProducttype` mapper.")
lines.append("")
lines.append("## Regenerate")
lines.append("")
lines.append("```bash")
lines.append("python scripts/generate_m18_mapping_docs.py")
lines.append("```")
lines.append("")

OUT_DIR.mkdir(parents=True, exist_ok=True)
path = OUT_DIR / "m18-item-type-mapping.md"
path.write_text("\n".join(lines), encoding="utf-8")
print(f"Wrote {path.relative_to(ROOT)}")


def write_stsearch_doc(st_types: list[tuple[str, str]]) -> None:
lines: list[str] = []
lines.append("<!-- AUTO-GENERATED by scripts/generate_m18_mapping_docs.py — do not edit by hand -->")
lines.append("")
lines.append("# M18 `StSearchType` (master list APIs)")
lines.append("")
lines.append(f"_Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}_")
lines.append("")
lines.append("**Source:** `m18/model/M18MasterDataRequest.kt`")
lines.append("")
lines.append("| Constant | `stSearch` value | Typical MTMS sync target |")
lines.append("|---|---|---|")
hints = {
"PRODUCT": "items (+ item_uom via prices)",
"VENDOR": "shop (`type=supplier`)",
"CUSTOMER": "(enum present; sync usage varies)",
"UNIT": "uom_conversion (+ m18 cunit)",
"CURRENCY": "currency",
"BOM": "bom / bom_material (udfbomforshop)",
"BUSINESS_UNIT": "shop (`type=shop`)",
}
for c, v in st_types:
lines.append(f"| `{c}` | `{v}` | {hints.get(c, '—')} |")
lines.append("")

path = OUT_DIR / "m18-stsearch-types.md"
path.write_text("\n".join(lines), encoding="utf-8")
print(f"Wrote {path.relative_to(ROOT)}")


def main() -> None:
new_item = NEW_ITEM_REQUEST.read_text(encoding="utf-8")
master_req = MASTER_DATA_REQUEST.read_text(encoding="utf-8")
service = MASTER_DATA_SERVICE.read_text(encoding="utf-8")

item_types = parse_kotlin_string_enum(new_item, "ItemType")
m18_types = parse_kotlin_string_enum(new_item, "M18ItemType")
st_types = parse_kotlin_string_enum(master_req, "StSearchType")
# StSearchType uses `value` not always matching parse — enum uses (val value: String)
# Our regex still works for NAME("x")
when_pairs = parse_producttype_when_branches(service)
ui = load_ui_labels()

write_item_type_doc(item_types, m18_types, when_pairs, ui)
write_stsearch_doc(st_types)


if __name__ == "__main__":
main()

불러오는 중...
취소
저장