|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- #!/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()
|