Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

generate_m18_mapping_docs.py 8.5 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Generate MTMS ↔ M18 mapping snippets from Kotlin source of truth.
  5. Usage (from repo root):
  6. python scripts/generate_m18_mapping_docs.py
  7. Outputs:
  8. docs/generated/m18-item-type-mapping.md
  9. docs/generated/m18-stsearch-types.md
  10. Re-run after changing:
  11. - modules/master/web/models/NewItemRequest.kt (ItemType / M18ItemType)
  12. - m18/model/M18MasterDataRequest.kt (StSearchType)
  13. - m18/service/M18MasterDataService.kt (udfProducttype when-branches)
  14. """
  15. from __future__ import annotations
  16. import re
  17. from datetime import datetime, timezone
  18. from pathlib import Path
  19. ROOT = Path(__file__).resolve().parents[1]
  20. OUT_DIR = ROOT / "docs" / "generated"
  21. NEW_ITEM_REQUEST = (
  22. ROOT
  23. / "src/main/java/com/ffii/fpsms/modules/master/web/models/NewItemRequest.kt"
  24. )
  25. MASTER_DATA_REQUEST = (
  26. ROOT / "src/main/java/com/ffii/fpsms/m18/model/M18MasterDataRequest.kt"
  27. )
  28. MASTER_DATA_SERVICE = (
  29. ROOT / "src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt"
  30. )
  31. INVENTORY_I18N = (
  32. ROOT.parent / "FPSMS-frontend" / "src" / "i18n" / "zh" / "inventory.json"
  33. )
  34. # UI labels when inventory.json is unavailable (fallback)
  35. FALLBACK_UI = {
  36. "mat": "原料",
  37. "consumables": "消耗品",
  38. "non-consumables": "非消耗品",
  39. "fg": "成品",
  40. "sfg": "半成品",
  41. "item": "貨品",
  42. "cmb": "消耗品",
  43. "wip": "半成品",
  44. "nm": "雜項及非消耗品",
  45. }
  46. # Known M18 udfProducttype values seen in the wild that are NOT in M18ItemType
  47. # (documented as gaps so ops/dev notice).
  48. KNOWN_UNMAPPED_M18_VALUES = [
  49. ("CMB", "Seen on M18 pro.udfProducttype (e.g. MG1852). Falls through to mat."),
  50. ]
  51. def parse_kotlin_string_enum(text: str, enum_name: str) -> list[tuple[str, str]]:
  52. """Parse active (non-commented) `enum class Foo(...) { NAME("x"), ... }`."""
  53. # Only match enum declarations that start a line (optional indent), not //enum
  54. m = re.search(
  55. rf"(?m)^[ \t]*enum class {re.escape(enum_name)}\([^)]*\)\s*\{{(.*?)^[ \t]*\}}",
  56. text,
  57. re.DOTALL,
  58. )
  59. if not m:
  60. raise SystemExit(f"Could not find enum class {enum_name}")
  61. body = m.group(1)
  62. return re.findall(r"(\w+)\s*\(\s*\"([^\"]+)\"\s*\)", body)
  63. def parse_producttype_when_branches(service_text: str) -> list[tuple[str, str]]:
  64. """
  65. Extract first `when (pro.udfProducttype) { M18ItemType.X.type -> ItemType.Y.type ... }`
  66. Returns list of (M18ItemTypeConst, ItemTypeConst).
  67. """
  68. m = re.search(
  69. r"when\s*\(\s*pro\.udfProducttype\s*\)\s*\{(.*?)else\s*->\s*ItemType\.(\w+)\.type",
  70. service_text,
  71. re.DOTALL,
  72. )
  73. if not m:
  74. raise SystemExit("Could not find udfProducttype when-branch in M18MasterDataService")
  75. body, else_item = m.group(1), m.group(2)
  76. pairs = re.findall(
  77. r"M18ItemType\.(\w+)\.type\s*->\s*ItemType\.(\w+)\.type",
  78. body,
  79. )
  80. return pairs + [("__else__", else_item)]
  81. def load_ui_labels() -> dict[str, str]:
  82. labels = dict(FALLBACK_UI)
  83. if not INVENTORY_I18N.is_file():
  84. return labels
  85. # Minimal JSON-ish extract of "key": "value" string pairs
  86. text = INVENTORY_I18N.read_text(encoding="utf-8")
  87. for k, v in re.findall(r'"([^"]+)"\s*:\s*"([^"]*)"', text):
  88. labels[k] = v
  89. return labels
  90. def write_item_type_doc(
  91. item_types: list[tuple[str, str]],
  92. m18_types: list[tuple[str, str]],
  93. when_pairs: list[tuple[str, str]],
  94. ui: dict[str, str],
  95. ) -> None:
  96. item_by_const = {c: v for c, v in item_types}
  97. m18_by_const = {c: v for c, v in m18_types}
  98. mapped_m18_consts = {a for a, b in when_pairs if a != "__else__"}
  99. lines: list[str] = []
  100. lines.append("<!-- AUTO-GENERATED by scripts/generate_m18_mapping_docs.py — do not edit by hand -->")
  101. lines.append("")
  102. lines.append("# M18 `udfProducttype` → MTMS `items.type`")
  103. lines.append("")
  104. lines.append(f"_Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}_")
  105. lines.append("")
  106. lines.append("**Source of truth**")
  107. lines.append("")
  108. lines.append("- Enums: `NewItemRequest.kt` → `ItemType`, `M18ItemType`")
  109. lines.append("- Sync: `M18MasterDataService.saveProduct` / `saveProducts` (`when (pro.udfProducttype)`)")
  110. lines.append("- UI labels (inventory): `FPSMS-frontend/src/i18n/zh/inventory.json`")
  111. lines.append("")
  112. lines.append("## Sync mapping")
  113. lines.append("")
  114. lines.append("| M18 `udfProducttype` (exact string) | `M18ItemType` | MTMS `items.type` | `ItemType` | Inventory UI (zh) |")
  115. lines.append("|---|---|---|---|---|")
  116. for m18_const, item_const in when_pairs:
  117. if m18_const == "__else__":
  118. mtms_val = item_by_const.get(item_const, "?")
  119. lines.append(
  120. f"| *(any other value / empty)* | — | `{mtms_val}` | `{item_const}` | {ui.get(mtms_val, '—')} |"
  121. )
  122. continue
  123. m18_val = m18_by_const.get(m18_const, "?")
  124. mtms_val = item_by_const.get(item_const, "?")
  125. lines.append(
  126. f"| `{m18_val}` | `{m18_const}` | `{mtms_val}` | `{item_const}` | {ui.get(mtms_val, '—')} |"
  127. )
  128. lines.append("")
  129. lines.append("## Enum inventories")
  130. lines.append("")
  131. lines.append("### `M18ItemType`")
  132. lines.append("")
  133. lines.append("| Constant | String value | Used in sync `when`? |")
  134. lines.append("|---|---|---|")
  135. for c, v in m18_types:
  136. used = "yes" if c in mapped_m18_consts else "**no**"
  137. lines.append(f"| `{c}` | `{v}` | {used} |")
  138. lines.append("")
  139. lines.append("### `ItemType` (MTMS stored values)")
  140. lines.append("")
  141. lines.append("| Constant | `items.type` | Inventory UI (zh) |")
  142. lines.append("|---|---|---|")
  143. for c, v in item_types:
  144. lines.append(f"| `{c}` | `{v}` | {ui.get(v, '—')} |")
  145. lines.append("")
  146. lines.append("## Known gaps (not auto-mapped)")
  147. lines.append("")
  148. lines.append("| M18 value seen | Effect | Notes |")
  149. lines.append("|---|---|---|")
  150. for val, note in KNOWN_UNMAPPED_M18_VALUES:
  151. lines.append(f"| `{val}` | → `mat` (else) | {note} |")
  152. lines.append("")
  153. lines.append("Frontend Settings → Items edit also offers `cmb` / `wip` / `nm` as local types;")
  154. lines.append("those are **not** written by the current M18 `udfProducttype` mapper.")
  155. lines.append("")
  156. lines.append("## Regenerate")
  157. lines.append("")
  158. lines.append("```bash")
  159. lines.append("python scripts/generate_m18_mapping_docs.py")
  160. lines.append("```")
  161. lines.append("")
  162. OUT_DIR.mkdir(parents=True, exist_ok=True)
  163. path = OUT_DIR / "m18-item-type-mapping.md"
  164. path.write_text("\n".join(lines), encoding="utf-8")
  165. print(f"Wrote {path.relative_to(ROOT)}")
  166. def write_stsearch_doc(st_types: list[tuple[str, str]]) -> None:
  167. lines: list[str] = []
  168. lines.append("<!-- AUTO-GENERATED by scripts/generate_m18_mapping_docs.py — do not edit by hand -->")
  169. lines.append("")
  170. lines.append("# M18 `StSearchType` (master list APIs)")
  171. lines.append("")
  172. lines.append(f"_Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}_")
  173. lines.append("")
  174. lines.append("**Source:** `m18/model/M18MasterDataRequest.kt`")
  175. lines.append("")
  176. lines.append("| Constant | `stSearch` value | Typical MTMS sync target |")
  177. lines.append("|---|---|---|")
  178. hints = {
  179. "PRODUCT": "items (+ item_uom via prices)",
  180. "VENDOR": "shop (`type=supplier`)",
  181. "CUSTOMER": "(enum present; sync usage varies)",
  182. "UNIT": "uom_conversion (+ m18 cunit)",
  183. "CURRENCY": "currency",
  184. "BOM": "bom / bom_material (udfbomforshop)",
  185. "BUSINESS_UNIT": "shop (`type=shop`)",
  186. }
  187. for c, v in st_types:
  188. lines.append(f"| `{c}` | `{v}` | {hints.get(c, '—')} |")
  189. lines.append("")
  190. path = OUT_DIR / "m18-stsearch-types.md"
  191. path.write_text("\n".join(lines), encoding="utf-8")
  192. print(f"Wrote {path.relative_to(ROOT)}")
  193. def main() -> None:
  194. new_item = NEW_ITEM_REQUEST.read_text(encoding="utf-8")
  195. master_req = MASTER_DATA_REQUEST.read_text(encoding="utf-8")
  196. service = MASTER_DATA_SERVICE.read_text(encoding="utf-8")
  197. item_types = parse_kotlin_string_enum(new_item, "ItemType")
  198. m18_types = parse_kotlin_string_enum(new_item, "M18ItemType")
  199. st_types = parse_kotlin_string_enum(master_req, "StSearchType")
  200. # StSearchType uses `value` not always matching parse — enum uses (val value: String)
  201. # Our regex still works for NAME("x")
  202. when_pairs = parse_producttype_when_branches(service)
  203. ui = load_ui_labels()
  204. write_item_type_doc(item_types, m18_types, when_pairs, ui)
  205. write_stsearch_doc(st_types)
  206. if __name__ == "__main__":
  207. main()