HR 拿著一份人員增補申請表走進來,說:「我們需要一個簽核流程。」
工程師接過表單,看了三秒,知道這不只是「建一個 Workflow」。在 iDempiere 的世界裡,每一個欄位都需要名分,每一扇窗口都需要身份,每一條審批鏈都需要從頭蓋起。這不是設定,這是建城。
以下是建城的完整過程。
這次蓋了什麼
先看全局,再進細節。這份建置單是整座城的設計圖:
| 物件 | 名稱 / 數量 |
|---|---|
AD_Table | HR_HeadcountRequest |
AD_Column | 21 欄(含 DocStatus、DocAction、業務欄位) |
AD_Window | Headcount Request(英文主記錄)+ zh_TW Trl = 人員增補申請 |
AD_Tab | Headcount Request(英文主記錄)+ zh_TW Trl = 人員增補申請 |
AD_Field | 21 個 |
AD_Menu | Headcount Request(英文主記錄)+ zh_TW Trl = 人員增補申請 |
AD_Window_Access | System Admin (0)、GardenWorld Admin (102)、GardenWorld Admin Not Advanced (200001) |
AD_Workflow | HR_HeadcountRequest |
AD_WF_Node | 9 個(4 系統節點 + 5 審批節點) |
AD_WF_NodeNext | 12 條(3 系統 + 9 審批含駁回) |
Workflow 簽核路徑:
(Start)→(DocPrepare)→(DocComplete)→(DocAuto)
↓
申請人 → 申請人主管 → 部門主管 → 總經理 → HR主管(CO)
↑______駁回______↑__________↑__________↑__________↑第一關:文件齊了才開工
每個工程師都有過這種經歷:做到一半,發現需求文件是上個版本的,或者表單樣板根本不存在。人員增補流程不能這樣開始。
所以第一步不是打開資料庫,是打開清單。
開工前需要確認兩類文件:
- Form Template(表單樣板):Word 或 Excel 格式,定義申請人要填什麼欄位。這份文件決定了後面要建哪些 AD_Column。沒有它,你不知道表單長什麼樣,建出來的窗口就是猜的。
- Workflow 說明文件:描述審批流程的文件,通常是 Word 或 Excel,說明有幾個關卡、每關由誰審、核准條件是什麼、駁回後走哪條路。這份文件決定了後面 AD_WF_Node 和 AD_WF_NodeNext 怎麼建。
兩份文件缺一不可。Form Template 缺了,你不知道要建什麼;Workflow 說明文件缺了,你不知道節點怎麼連。沒有文件就動手,等於在黑暗中蓋房子——蓋完可能沒有門。
補充邏輯:文件確認後,還要核對以下幾點,才能確保後續順利:
- 審批層級是否與 HR 或業務單位確認過(不能只看文件,文件可能是舊的)
- 是否有代理核准的需求(主管出差時由誰代簽)
- 駁回後是否需要重新走全程,還是只退回特定關卡
- DB 開發環境帳密是否可用,測試環境與正式環境是否分開
第一點五關:授權儀式
文件齊了,不代表可以開工。還有一個儀式要走完:確認你有進門的鑰匙。
這把鑰匙叫做 DB 連線。在 iDempiere 本機開發環境,資料庫是 PostgreSQL,帳密是 dev / dev,資料庫名稱是 dev,跑在 localhost。聽起來很簡單,但每次換機器、換環境、換 Docker volume,這一步都可能讓你卡半小時。先確認,再動手。
連線確認後,還要驗證 nextid 函式存在且可用。這個函式是 iDempiere 的 PostgreSQL stored function,定義在 adempiere schema 下。它不是標準 SQL,是 iDempiere 自己的東西。如果 schema 沒有 import 完整,或者你連錯了資料庫,這一步就會噴錯,後面所有的 INSERT 全部白費。
授權儀式的三個動作:連線、驗證 nextid、確認目標 Schema。三個都過,才算拿到鑰匙。
— 1. 確認 DB 連線(psql 指令)
— psql -h localhost -U dev -d dev -c “SELECT current_database(), current_user, version();”
— 2. 確認 nextid 函式存在
SELECT routine_name, routine_schema
FROM information_schema.routines
WHERE routine_name = ‘nextid’
AND routine_schema = ‘adempiere’;
— 3. 驗證 nextid 可正常取號(不會實際消耗序列,僅測試)
SELECT nextid(‘AD_Element’, ‘N’) AS test_id;
— 4. 確認 adempiere schema 下有 AD_Element 表
SELECT table_name
FROM information_schema.tables
WHERE table_schema = ‘adempiere’
AND table_name = ‘ad_element’;
第二關:先取號碼牌,再進場
iDempiere 的世界裡,沒有 ID 就沒有身份。每建一筆 AD_* 記錄,都要先跟序列號要一個 Primary Key。這個函式叫 nextid,第一個參數是 Table Name 字串,第二個參數傳 'N'(Client 端序列)或 'Y'(System 序列)。
用法很簡單:直接嵌進 INSERT 的 VALUES 裡。不用先 SELECT,不用存變數,一行搞定。
— nextid 函式用法(tablename 版本)
— nextid(‘TableName’, ‘N’) 直接嵌入 INSERT VALUES
— 第一參數:Table 名稱(對應 AD_Sequence.Name)
— 第二參數:’N’ = Client 端序列(一般用途);’Y’ = System 序列
— 使用範例:
INSERT INTO AD_Element
(AD_Element_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
ColumnName, Name, PrintName)
VALUES
(nextid(‘AD_Element’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘HR_HeadcountRequest_ID’, ‘Headcount Request’, ‘Headcount Request’);
第三關:命名儀式
在 iDempiere,每個欄位都要有三個名字:資料庫欄位名(DB Column Name)、系統顯示名(Name)、列印標籤(Print Text)。這不是繁瑣,這是規矩。三個名字各司其職,缺一不可。
建立順序有依賴關係,不能亂序:Element → Table → Column → Window → Tab → Field → Menu。就像蓋房子:先有磚頭,才有牆,才有門。
有一條命名規矩必須說清楚:Window、Tab、Menu 的主記錄(AD_Window、AD_Tab、AD_Menu)Name 欄位要填英文。中文顯示名另外插入對應的 _Trl 翻譯表(AD_Window_Trl、AD_Tab_Trl、AD_Menu_Trl,AD_Language='zh_TW')。直接用中文當主記錄名不符 iDempiere 慣例,日後 Synchronize Terminology 可能覆蓋。
還有一步容易被忽略:Menu 建好後,必須在 AD_TreeNodeMM 插入一筆,指定所屬的 Menu Tree(Tree_ID=10)和上層目錄節點(Parent_ID)。沒有這筆,Menu 在資料庫裡存在,但導覽列上永遠看不到它。
以下是 AD_Element 的命名格式範例,三欄缺一不可:
| DB Column Name | Name | Print Text |
|---|---|---|
| QuotationOrder_ID | Quotation | Quotation |
| ReceiveOrderReply | Receive Order Reply | Order reply |
| Ref_OrderLine_ID | Referenced Order Line | Ref Order Line |
| Ref_Order_ID | Referenced Order | Ref Order |
| Search_Order_ID | Search Order | Search Order |
| SendOrder | Send Order | Send Order |
| SumQtyOrdered | Sum Qty Ordered | Sum Qty Ordered |
| WebOrderEMail | Web Order EMail | Web Order EMail |
— 1. AD_Element(欄位元素定義)
— 格式參考 element_naming.jpg:DB Column Name / Name / Print Text
INSERT INTO AD_Element
(AD_Element_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
ColumnName, Name, PrintName, Description)
VALUES
(nextid(‘AD_Element’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘HR_HeadcountRequest_ID’, ‘Headcount Request’, ‘Headcount Request’, ‘Headcount Request Document’);
— 2. AD_Table
INSERT INTO AD_Table
(AD_Table_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, TableName, IsView, AccessLevel, IsDeleteable, IsHighVolume, IsSecurityEnabled,
IsChangeLog, ReplicationType, AD_Window_ID)
VALUES
(nextid(‘AD_Table’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘HR Headcount Request’, ‘HR_HeadcountRequest’, ‘N’, ‘3’, ‘Y’, ‘N’, ‘N’, ‘Y’, ‘L’, NULL);
— 3. AD_Column(主鍵欄位)
— 需先取得上方建立的 AD_Table_ID 與 AD_Element_ID
INSERT INTO AD_Column
(AD_Column_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, AD_Table_ID, AD_Element_ID, ColumnName, FieldLength, IsKey, IsParent,
IsMandatory, IsTranslated, IsIdentifier, IsEncrypted, AD_Reference_ID, SeqNo, Version)
VALUES
(nextid(‘AD_Column’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘Headcount Request’,
‘HR_HeadcountRequest_ID’, 10, ‘Y’, ‘N’, ‘Y’, ‘N’, ‘Y’, ‘N’, 13, 1, 0);
— ⚠️ Version NOT NULL — 必須填 0,原生 iDempiere schema 不允許 NULL
— 4. AD_Window
— ⚠️ 主記錄 Name 必須用英文,中文顯示靠 AD_Window_Trl
INSERT INTO adempiere.AD_Window
(AD_Window_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, WindowType, IsSOTrx)
VALUES
(nextid(‘AD_Window’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘Headcount Request’, ‘M’, ‘Y’);
— 4a. AD_Window_Trl(中文譯名)
INSERT INTO adempiere.AD_Window_Trl
(AD_Window_ID, AD_Language, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, IsTranslated)
VALUES
(
— 5. AD_Tab
— ⚠️ 同樣:主記錄英文,中文靠 AD_Tab_Trl
INSERT INTO adempiere.AD_Tab
(AD_Tab_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, AD_Window_ID, AD_Table_ID, SeqNo, TabLevel, IsSingleRow, IsReadOnly, HasTree)
VALUES
(nextid(‘AD_Tab’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘Headcount Request’,
— 5a. AD_Tab_Trl(中文譯名)
INSERT INTO adempiere.AD_Tab_Trl
(AD_Tab_ID, AD_Language, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, IsTranslated)
SELECT AD_Tab_ID, ‘zh_TW’, 0, 0, ‘Y’, now(), 100, now(), 100, ‘人員增補申請’, ‘Y’
FROM adempiere.AD_Tab WHERE AD_Window_ID =
— 6. AD_Field(主鍵欄位)
INSERT INTO adempiere.AD_Field
(AD_Field_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, AD_Tab_ID, AD_Column_ID, SeqNo, DisplayLength, IsSameLine, IsHeading,
IsFieldOnly, IsEncrypted, IsDisplayed, IsMandatory)
VALUES
(nextid(‘AD_Field’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘Headcount Request’,
— 7. AD_Menu
— ⚠️ 主記錄英文,中文靠 AD_Menu_Trl
INSERT INTO adempiere.AD_Menu
(AD_Menu_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, IsSOTrx, IsSummary, Action, AD_Window_ID)
VALUES
(nextid(‘AD_Menu’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100,
‘Headcount Request’, ‘Y’, ‘N’, ‘W’,
— 7a. AD_Menu_Trl(中文譯名)
INSERT INTO adempiere.AD_Menu_Trl
(AD_Menu_ID, AD_Language, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, IsTranslated)
VALUES
(
— 7b. AD_TreeNodeMM(Menu 必須掛入 Tree,否則不出現在導覽列)
— ⚠️ 少了這步,Menu 建好了但導覽列看不到
— Tree_ID=10 為 Menu Tree;Parent_ID 為上層目錄節點(HR 目錄)
INSERT INTO adempiere.AD_TreeNodeMM
(AD_Tree_ID, Node_ID, Parent_ID, SeqNo, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy)
VALUES
(10,
第四關:給 Admin 一把鑰匙
門蓋好了,窗口設好了,但如果沒有存取權限,所有東西都是不存在的。在 iDempiere,Window 建好後要明確授權給 Role,Admin 才能看到它、操作它。
這裡有個容易踩的坑:只授權 System Admin(Role_ID=0)是不夠的。System Admin 是 Client_ID=0 層級的超級角色,但一般使用者是用 Client 層的帳號登入(例如 GardenWorld Client=11)。如果沒有同時授權目標 Client 的 Admin Role,那些使用者登入後根本看不到這個 Window。授權時要一次給齊:System Admin、目標 Client 的 Admin、以及任何需要存取的角色。
— AD_Window_Access:授予多個 Role 存取 Headcount Request Window
— ⚠️ AD_Window_Access 使用 (AD_Window_ID, AD_Role_ID) 複合主鍵,無獨立 ID 欄位
— ⚠️ 只給 Role_ID=0 不夠!Client 層的使用者看不到 Window,必須補 Client Admin Role
INSERT INTO adempiere.AD_Window_Access
(AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_Window_ID, AD_Role_ID, IsReadWrite)
VALUES
(0, 0, ‘Y’, now(), 100, now(), 100,
(11, 0, ‘Y’, now(), 100, now(), 100,
(11, 0, ‘Y’, now(), 100, now(), 100,
— 依實際 Client 調整 AD_Client_ID 與 AD_Role_ID
第五關:Workflow 的四根柱子(System)
每一份 iDempiere 文件 Workflow 都建在同樣的骨架上:Start、DocPrepare、DocComplete、DocAuto。這四個節點定義了文件的生命週期,是 System 層級的結構,不屬於任何 Client。
先建骨架,再掛業務邏輯。這是順序,也是哲學。
— ⚠️ 實際表名:AD_WF_Node(不是 AD_Node),AD_WF_NodeNext(不是 AD_NodeNext)
— ⚠️ 使用 DO $$ 變數 block:節點建立後取回 ID 再做 NodeNext,避免 subquery 返回多筆
DO $$
DECLARE
v_wf_id NUMERIC;
v_start NUMERIC;
v_prep NUMERIC;
v_comp NUMERIC;
v_auto NUMERIC;
BEGIN
— AD_Workflow(Document Workflow)
— ⚠️ AD_Workflow 有多個 NOT NULL 欄位:Cost, PublishStatus, Author, Version, WorkingTime, WaitingTime, IsBetaFunctionality, IsValid
v_wf_id := nextid(‘AD_Workflow’, ‘N’);
INSERT INTO adempiere.AD_Workflow
(AD_Workflow_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AccessLevel, WorkflowType, Duration, DurationUnit,
IsDefault, ValidFrom,
Cost, PublishStatus, Author, Version, WorkingTime, WaitingTime,
IsBetaFunctionality, IsValid)
VALUES
(v_wf_id, 0, 0, ‘Y’, now(), 100, now(), 100,
‘HR Headcount Request’, ‘HR_HeadcountRequest’,
‘3’, ‘D’, 0, ‘D’, ‘Y’, now(),
0, ‘U’, ‘Admin’, 0, 0, 0, ‘N’, ‘N’);
— ⚠️ AD_WF_Node NOT NULL 欄位:Value, JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail
— Action DB 值:B=Start Process, D=Document Action
— DocAction DB 值:PR=Prepare, CO=Complete, –=None
— Node 1: Start
v_start := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES
(v_start, 0, 0, ‘Y’, now(), 100, now(), 100,
‘(Start)’, ‘HHR_Start’, v_wf_id, ‘B’, ‘–‘, 10, 10,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— Node 2: DocPrepare
v_prep := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES
(v_prep, 0, 0, ‘Y’, now(), 100, now(), 100,
‘(DocPrepare)’, ‘HHR_Prepare’, v_wf_id, ‘D’, ‘PR’, 10, 80,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— Node 3: DocComplete
v_comp := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES
(v_comp, 0, 0, ‘Y’, now(), 100, now(), 100,
‘(DocComplete)’, ‘HHR_Complete’, v_wf_id, ‘D’, ‘CO’, 10, 150,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— Node 4: DocAuto
v_auto := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES
(v_auto, 0, 0, ‘Y’, now(), 100, now(), 100,
‘(DocAuto)’, ‘HHR_Auto’, v_wf_id, ‘D’, ‘–‘, 10, 220,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— AD_WF_NodeNext:Transition 連結(所有節點建完後執行)
— Start → DocPrepare
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100, v_start, v_prep, 10, ‘N’);
— DocPrepare → DocComplete
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100, v_prep, v_comp, 10, ‘N’);
— DocComplete → DocAuto
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100, v_comp, v_auto, 10, ‘N’);
END $$;
第六關:五關審批(Client=11)
骨架建好了,現在換業務邏輯上場。人員增補申請有五個審批關卡,每一關都是一個承諾:申請人起單,主管確認,部門主管背書,總經理拍板,HR 執行。
這五個節點建在 Client_ID=11(Demo),掛在同一個 AD_Workflow 下。每個節點需要兩條 Transition:核准走下一關,駁回退回申請人。
— ⚠️ 使用 DO $$ 變數 block,避免 subquery 多筆問題
— ⚠️ 實際表名:AD_WF_Node(非 AD_Node),AD_WF_NodeNext(非 AD_NodeNext)
— ⚠️ AD_WF_Node 有多個 NOT NULL 欄位:Value, JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail
— ⚠️ “limit” 是 PostgreSQL 保留字,INSERT 欄位列表中須加雙引號
DO $$
DECLARE
v_wf_id NUMERIC;
v_auto NUMERIC; — 取得 System 層 DocAuto 節點 ID(BUILD-WORKFLOW-SYSTEM 已建)
v_app NUMERIC; — 申請人
v_mgr NUMERIC; — 申請人主管
v_dept NUMERIC; — 部門主管
v_gm NUMERIC; — 總經理
v_hr NUMERIC; — HR主管
BEGIN
— 取得已建立的 Workflow 和 DocAuto 節點 ID
SELECT AD_Workflow_ID INTO v_wf_id FROM adempiere.AD_Workflow WHERE Value=’HR_HeadcountRequest’;
SELECT AD_WF_Node_ID INTO v_auto FROM adempiere.AD_WF_Node WHERE Value=’HHR_Auto’ AND AD_Workflow_ID=v_wf_id;
— 五個審批節點(Client_ID=11)
— UserDefined Logic 語法:@ColumnName@ 取欄位值,& = AND,| = OR
— Node 5: 申請人(起單)
v_app := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES (v_app, 11, 0, ‘Y’, now(), 100, now(), 100,
‘申請人’, ‘HHR_Applicant’, v_wf_id, ‘D’, ‘–‘, 200, 10,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— Node 6: 申請人主管
v_mgr := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES (v_mgr, 11, 0, ‘Y’, now(), 100, now(), 100,
‘申請人主管’, ‘HHR_Manager’, v_wf_id, ‘D’, ‘–‘, 200, 80,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— Node 7: 部門主管
v_dept := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES (v_dept, 11, 0, ‘Y’, now(), 100, now(), 100,
‘部門主管’, ‘HHR_DeptHead’, v_wf_id, ‘D’, ‘–‘, 200, 150,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— Node 8: 總經理
v_gm := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES (v_gm, 11, 0, ‘Y’, now(), 100, now(), 100,
‘總經理’, ‘HHR_GM’, v_wf_id, ‘D’, ‘–‘, 200, 220,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— Node 9: HR主管(CO = 核准完成文件)
v_hr := nextid(‘AD_WF_Node’, ‘N’);
INSERT INTO adempiere.AD_WF_Node
(AD_WF_Node_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
Name, Value, AD_Workflow_ID, Action, DocAction, XPosition, YPosition,
JoinElement, SplitElement, “limit”, Duration, Cost, WaitingTime, IsAttachedDocumentToEmail)
VALUES (v_hr, 11, 0, ‘Y’, now(), 100, now(), 100,
‘HR主管’, ‘HHR_HR’, v_wf_id, ‘D’, ‘CO’, 200, 290,
‘X’, ‘X’, 0, 0, 0, 0, ‘N’);
— AD_WF_NodeNext Transitions(所有節點建完後執行)
— DocAuto → 申請人(System 銜接 Client)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 0, 0, ‘Y’, now(), 100, now(), 100, v_auto, v_app, 10, ‘Y’);
— 申請人 → 申請人主管(核准)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_app, v_mgr, 10, ‘Y’);
— 申請人主管 → 部門主管(核准)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_mgr, v_dept, 10, ‘Y’);
— 申請人主管 → 申請人(駁回)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow, EntityType)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_mgr, v_app, 20, ‘N’, ‘U’);
— 部門主管 → 總經理(核准)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_dept, v_gm, 10, ‘Y’);
— 部門主管 → 申請人(駁回)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow, EntityType)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_dept, v_app, 20, ‘N’, ‘U’);
— 總經理 → HR主管(核准)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_gm, v_hr, 10, ‘Y’);
— 總經理 → 申請人(駁回)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow, EntityType)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_gm, v_app, 20, ‘N’, ‘U’);
— HR主管 → 申請人(駁回,最終關卡仍可退件)
INSERT INTO adempiere.AD_WF_NodeNext
(AD_WF_NodeNext_ID, AD_Client_ID, AD_Org_ID, IsActive, Created, CreatedBy, Updated, UpdatedBy,
AD_WF_Node_ID, AD_Next_WF_Node_ID, SeqNo, IsStdUserWorkflow, EntityType)
VALUES (nextid(‘AD_WF_NodeNext’,’N’), 11, 0, ‘Y’, now(), 100, now(), 100, v_hr, v_app, 20, ‘N’, ‘U’);
END $$;
⚠️ 踩坑記錄:這些錯誤執行時才會爆
以下是照著本文建置時實際踩到的坑,每一個都是 SQL 報錯換來的教訓。
- AD_Column 需要
Version欄位(NOT NULL)
原始範例未包含,執行時會報null value in column "version"。必須加入Version = 0。 - AD_Window_Access 沒有
AD_Window_Access_ID欄位
這個 table 用(AD_Window_ID, AD_Role_ID)複合主鍵,不是獨立序列 ID。不要加nextid()。 - Workflow 節點表是
AD_WF_Node,不是AD_Node
連結表是AD_WF_NodeNext,不是AD_NodeNext。打錯表名,INSERT 直接失敗。 AD_WF_Node有多個 NOT NULL 欄位須一次填齊
Value,JoinElement,SplitElement,"limit",Duration,Cost,WaitingTime,IsAttachedDocumentToEmail——缺一即報錯。AD_Workflow也有多個 NOT NULL 欄位
Cost,PublishStatus,Author,Version,WorkingTime,WaitingTime,IsBetaFunctionality,IsValid——原範例只填了一半。- 使用
DO $$變數 block 是最佳實踐
節點建立後取回 ID 再做 NodeNext,避免 subquery 返回多筆的問題。本文已改用此寫法。 limit是 PostgreSQL 保留字
在 INSERT 欄位列表中必須加雙引號:"limit",否則 SQL 解析失敗。- Window / Tab / Menu 主記錄名必須用英文,中文靠
_Trl表
直接用中文當 Name 不符合 iDempiere 慣例。主記錄填英文,再分別 INSERTAD_Window_Trl、AD_Tab_Trl、AD_Menu_Trl,語言碼zh_TW,才能正確多語系顯示。 AD_Window_Access只給 System Admin (Role_ID=0) 不夠
Client 層登入的用戶看不到 Window。必須補上目標 Client 的 Admin Role(例如 GardenWorld Admin Role_ID=102、200001)。AD_Client_ID 也要對應 Client,不能全填 0。AD_Menu建好了但導覽列看不到——要插入AD_TreeNodeMM
Menu 記錄建立後,必須在AD_TreeNodeMM加一筆,指定 Tree_ID(Menu Tree=10)、Parent_ID(上層目錄節點)才會掛進導覽樹。漏了這步,Menu 在資料庫裡存在,但介面上永遠不會出現。
下一步
城蓋好了,但還沒完工。以下是後續需要處理的項目:
- 在 iDempiere UI 確認導覽列出現「人員增補申請」選單
- Synchronize Terminology:在 System Admin → General Rules → System → Synchronize Terminology 執行,讓系統重整欄位翻譯
- 建立
C_DocType文件類型並關聯此 Workflow - 設定各審批節點的
AD_WF_Responsible(指派實際簽核人) - 實作
MHeadcountRequest.java(Model class,可參考MAbsenceNote的寫法)
結:雷公曰
一張人員增補表,走過簽查、取名、授權、骨架、審批,終成一條有名有分的 Workflow。
建城不難,難在每一塊磚都要有位置,每一道門都要有鑰匙,每一個節點都要知道下一步去哪。
雷公曰:流程即承諾,程式碼即合約,SQL 即儀式。儀式完成,事成矣。
English
HR walked in with a headcount request form and said: “We need an approval workflow.” The engineer took the form, looked at it for three seconds, and understood: this wasn’t just “set up a workflow.” In iDempiere, every field needs an identity, every window needs a name, every approval chain needs to be built from scratch. This isn’t configuration. This is city-building.
What We Built
| Object | Name / Count |
|---|---|
AD_Table | HR_HeadcountRequest |
AD_Column | 21 columns (incl. DocStatus, DocAction, business fields) |
AD_Window | Headcount Request (English) + zh_TW Trl |
AD_Tab | Headcount Request (English) + zh_TW Trl |
AD_Field | 21 fields |
AD_Menu | Headcount Request (English) + zh_TW Trl |
AD_Window_Access | System Admin (0), GardenWorld Admin (102), GardenWorld Admin Not Advanced (200001) |
AD_Workflow | HR_HeadcountRequest |
AD_WF_Node | 9 nodes (4 system + 5 approval) |
AD_WF_NodeNext | 12 transitions (3 system + 9 approval incl. reject paths) |
Approval flow:
(Start)→(DocPrepare)→(DocComplete)→(DocAuto)
↓
Applicant → Direct Mgr → Dept Head → GM → HR Head (CO)
↑__________reject_______↑__________↑_______↑_______↑Gate 1: Documents First
Every engineer has been there: halfway through the build, you discover the requirements doc is from last quarter, or the form template doesn’t exist. A headcount request workflow can’t start that way. The first step isn’t opening the database — it’s opening a checklist.
Gate 2: Get Your Number First
In iDempiere, no ID means no identity. Before inserting any AD_* record, you need a Primary Key from the sequence. The function is called nextid — first parameter is the table name string (e.g. 'AD_Element'), second is 'N' for client-side sequence or 'Y' for system sequence. Usage: embed it directly in the INSERT VALUES. No SELECT first, no variable needed.
Gate 3: The Naming Ceremony
Every field in iDempiere needs three names: DB Column Name, system display Name, and Print Text. The build order has dependencies and cannot be shuffled: Element → Table → Column → Window → Tab → Field → Menu. Like building a house: bricks first, then walls, then doors.
One naming rule must be stated clearly: the primary records for Window, Tab, and Menu (AD_Window, AD_Tab, AD_Menu) must use English in the Name field. Chinese display names go into the corresponding translation tables (AD_Window_Trl, AD_Tab_Trl, AD_Menu_Trl) with AD_Language='zh_TW'. Using Chinese as the primary name is against iDempiere convention and may be overwritten by Synchronize Terminology.
One more step that’s easy to miss: after creating the Menu, you must insert a row into AD_TreeNodeMM with the correct Menu Tree ID (Tree_ID=10) and parent folder node (Parent_ID). Without this row, the menu exists in the database but never appears in the navigation bar.
Gate 4: Give Admin a Key
The window is built, but without access permissions it doesn’t exist. After creating the Window, you must explicitly grant role access. Without it, Admin can’t see it or use it.
A common trap: granting access only to System Admin (Role_ID=0) is not enough. System Admin operates at the Client_ID=0 level. Regular users log in under a specific Client (e.g. GardenWorld Client=11). If you don’t also grant access to that Client’s Admin Role, those users will not see the Window at all. Grant all needed roles in one go: System Admin, the target Client’s Admin role, and any other roles that need access.
Gate 5: The Four Pillars (System)
Every iDempiere document workflow is built on the same skeleton: Start, DocPrepare, DocComplete, DocAuto. These four nodes define the document lifecycle at the System level (Client_ID=0). Build the skeleton first, hang the business logic after.
Gate 6: The Five-Gate Approval Chain (Client=11)
The skeleton is ready. Now comes the business logic. The headcount request has five approval gates: Applicant submits, direct manager confirms, department head endorses, GM approves, HR executes. These five nodes are built in Client_ID=11 on the same AD_Workflow. Each node needs two transitions: approve → next gate, reject → back to applicant.
⚠️ Gotchas: Errors You’ll Only Hit at Runtime
These are the actual SQL errors encountered during the build described in this post. Each one cost at least one failed INSERT.
AD_Columnrequires aVersionfield (NOT NULL)
Missing from the original example. Will thrownull value in column "version". Must addVersion = 0.AD_Window_Accesshas noAD_Window_Access_IDcolumn
This table uses a composite primary key(AD_Window_ID, AD_Role_ID). Do not add anextid()call for it.- Workflow node table is
AD_WF_Node, notAD_Node
And the transition table isAD_WF_NodeNext, notAD_NodeNext. Wrong table name = immediate INSERT failure. AD_WF_Nodehas multiple NOT NULL fields that must all be provided
Value,JoinElement,SplitElement,"limit",Duration,Cost,WaitingTime,IsAttachedDocumentToEmail— omit any one and the INSERT fails.AD_Workflowalso has multiple hidden NOT NULL fields
Cost,PublishStatus,Author,Version,WorkingTime,WaitingTime,IsBetaFunctionality,IsValid— the original example only filled half of them.- Use
DO $$variable blocks for workflow node inserts
Capture node IDs into variables after INSERT, then use them in NodeNext inserts. Avoids subquery-returns-multiple-rows errors. limitis a reserved word in PostgreSQL
In INSERT column lists it must be double-quoted:"limit". Otherwise the SQL parser fails.- Window / Tab / Menu primary records must use English names — Chinese goes in
_Trltables
iDempiere convention: the main record (AD_Window,AD_Tab,AD_Menu) stores the English name. Chinese display is handled byAD_Window_Trl,AD_Tab_Trl, andAD_Menu_TrlwithAD_Language='zh_TW'. Skipping the Trl insert means the UI shows English regardless of locale. - Granting
AD_Window_Accessto System Admin (Role_ID=0) only is not enough
Users logging in under a Client won’t see the Window. You must also grant access to the Client-level Admin roles (e.g. GardenWorld Admin Role_ID=102, 200001). TheAD_Client_IDin those rows must match the target Client, not 0. - After creating
AD_Menu, you must insert a row intoAD_TreeNodeMMor it won’t appear in the navigation bar
The Menu record alone is invisible in the UI. Insert a row intoAD_TreeNodeMMwith the correctAD_Tree_ID(Menu Tree = 10),Node_ID(your Menu ID), andParent_ID(the parent folder node). Without this, the menu exists in the database but never appears on screen.
What’s Next
- Verify the “Headcount Request” menu item appears in the iDempiere navigation bar
- Synchronize Terminology: run via System Admin → General Rules → System → Synchronize Terminology to rebuild field translations
- Create a
C_DocTypedocument type and link it to this Workflow - Configure
AD_WF_Responsiblefor each approval node (assign the actual approvers) - Implement
MHeadcountRequest.java(Model class — seeMAbsenceNoteas a reference)
雷公曰
One headcount request form, passed through document check, naming ceremony, access grant, workflow skeleton, and approval chain — finally becoming a Workflow with a name and a place.
Building the city was not the hard part. The hard part was making sure every brick had a position, every door had a key, every node knew where to go next.
Ray says: workflow is promise, code is contract, SQL is ritual. When the ritual is complete, the work is done.
日本語
HR が人員増補申請書を持ってきて言いました:「承認ワークフローが必要です。」エンジニアはその書類を受け取り、3秒見て理解しました。これは単に「ワークフローを設定する」話ではない。iDempiere の世界では、すべてのフィールドに名前が必要で、すべてのウィンドウに身分が必要で、すべての承認チェーンをゼロから構築する必要があります。これは設定ではなく、都市建設です。
今回作ったもの
| オブジェクト | 名前 / 数 |
|---|---|
AD_Table | HR_HeadcountRequest |
AD_Column | 21カラム(DocStatus、DocAction、業務フィールド含む) |
AD_Window | Headcount Request(英語)+ zh_TW Trl = 人員増補申請 |
AD_Tab | Headcount Request(英語)+ zh_TW Trl = 人員増補申請 |
AD_Field | 21フィールド |
AD_Menu | Headcount Request(英語)+ zh_TW Trl = 人員増補申請 |
AD_Window_Access | System Admin (0)、GardenWorld Admin (102)、GardenWorld Admin Not Advanced (200001) |
AD_Workflow | HR_HeadcountRequest |
AD_WF_Node | 9ノード(システム4 + 承認5) |
AD_WF_NodeNext | 12トランジション(システム3 + 承認9(却下パス含む)) |
承認フロー:
(Start)→(DocPrepare)→(DocComplete)→(DocAuto)
↓
申請者 → 直属上司 → 部門長 → GM → HR長(CO)
↑_______却下______↑_________↑_____↑_____↑第一関:書類から始める
すべてのエンジニアが経験したことがあるはずです:作業の途中で、要件書が古いバージョンだったり、フォームテンプレートが存在しなかったりすることを発見する。人員増補ワークフローはそのような始め方ができません。最初のステップはデータベースを開くことではなく、チェックリストを開くことです。
第二関:番号札を先に取る
iDempiere では、ID がなければ身分がありません。AD_* レコードを挿入する前に、シーケンスから Primary Key を取得する必要があります。関数は nextid と呼ばれ、第一引数は テーブル名の文字列(例:'AD_Element')、第二引数は 'N'(クライアント側シーケンス)または 'Y'(システムシーケンス)です。使い方:INSERT VALUES に直接埋め込みます。
第三関:命名の儀式
iDempiere のすべてのフィールドには三つの名前が必要です:DB カラム名、システム表示名、印刷テキスト。構築順序には依存関係があり、順番を変えることはできません:Element → Table → Column → Window → Tab → Field → Menu。
命名のルールで必ず押さえておくべき点:Window・Tab・Menu の主レコード(AD_Window・AD_Tab・AD_Menu)の Name カラムには英語名を入れること。各言語の表示名は対応する翻訳テーブル(AD_Window_Trl・AD_Tab_Trl・AD_Menu_Trl、AD_Language='zh_TW')に登録します。主レコードに中国語を直接入れるのは iDempiere の慣例に反しており、Synchronize Terminology で上書きされる可能性があります。
見落としやすい手順がもう一つ:Menu を作成した後、AD_TreeNodeMM に行を挿入する必要があります。正しい Menu Tree ID(Tree_ID=10)と親フォルダのノード ID(Parent_ID)を指定します。この行がないと、データベース上には Menu が存在していてもナビゲーションバーには一切表示されません。
第四関:Admin に鍵を渡す
ウィンドウは構築されましたが、アクセス権限がなければ存在しないも同然です。Window を作成した後、Role にアクセス権を明示的に付与する必要があります。
よくある落とし穴:System Admin(Role_ID=0)のみに権限を付与しても不十分です。System Admin は Client_ID=0 レベルで動作します。一般ユーザーは特定の Client(例:GardenWorld Client=11)配下でログインします。その Client の Admin Role にも権限を付与しないと、そのユーザーは Window を一切見ることができません。必要なロールをまとめて付与してください:System Admin、対象 Client の Admin Role、その他アクセスが必要なロール。
第五関:四本の柱(System)
すべての iDempiere 文書ワークフローは同じ骨格の上に構築されます:Start、DocPrepare、DocComplete、DocAuto。これら四つのノードは System レベル(Client_ID=0)で文書のライフサイクルを定義します。
第六関:五段階承認チェーン(Client=11)
骨格の準備ができました。今度はビジネスロジックの番です。人員増補申請には五つの承認ゲートがあります:申請者が起票し、直属上司が確認し、部門長が承認し、GM が最終承認し、HR が実行します。各ノードには二つのトランジションが必要です:承認→次のゲート、却下→申請者に差し戻し。
⚠️ ハマりポイント:実行して初めて分かるエラー
以下は実際の建置作業中に踏んだ罠です。いずれも SQL エラーという形でツケが回ってきました。
AD_ColumnにVersionフィールドが必要(NOT NULL)
元のサンプルに含まれておらず、null value in column "version"エラーが発生します。Version = 0を必ず追加してください。AD_Window_AccessにAD_Window_Access_IDカラムは存在しない
このテーブルは(AD_Window_ID, AD_Role_ID)の複合主キーを使用します。nextid()は不要です。- ワークフローノードのテーブルは
AD_WF_Node(AD_Nodeではない)
トランジションテーブルはAD_WF_NodeNext(AD_NodeNextではない)。テーブル名を間違えると INSERT が即座に失敗します。 AD_WF_Nodeには複数の NOT NULL フィールドを一度に指定する必要がある
Value、JoinElement、SplitElement、"limit"、Duration、Cost、WaitingTime、IsAttachedDocumentToEmail— 一つでも欠けると INSERT 失敗。AD_Workflowにも複数の隠れた NOT NULL フィールドがある
Cost、PublishStatus、Author、Version、WorkingTime、WaitingTime、IsBetaFunctionality、IsValid— 元のサンプルはその半分しか含んでいません。- ワークフローノードの INSERT には
DO $$変数ブロックを使う
INSERT 後にノード ID を変数に取得してから NodeNext で使用。サブクエリが複数行を返す問題を回避できます。 limitは PostgreSQL の予約語
INSERT のカラムリストでは二重引用符が必要:"limit"。付けないと SQL パーサーが失敗します。- Window / Tab / Menu の主レコード名は英語にする — 日本語・中国語は
_Trlテーブルへ
iDempiere の慣例として、AD_Window・AD_Tab・AD_Menuの主レコードには英語名を入れます。各言語の表示名はAD_Window_Trl・AD_Tab_Trl・AD_Menu_TrlにAD_Language='zh_TW'などで登録します。Trl INSERT を省略すると、ロケール設定に関わらず UI は英語表示のままになります。 AD_Window_Accessを System Admin (Role_ID=0) のみに付与しても不十分
Client 配下のユーザーには Window が見えません。対象 Client の Admin Role(例:GardenWorld Admin Role_ID=102、200001)にも付与が必要です。その行のAD_Client_IDは対象 Client の値(0 ではなく)を設定してください。AD_Menuを作成してもAD_TreeNodeMMに行を挿入しないとナビゲーションバーに表示されない
Menu レコードだけでは UI に表示されません。AD_TreeNodeMMに正しいAD_Tree_ID(Menu Tree = 10)・Node_ID(作成した Menu ID)・Parent_ID(親フォルダのノード ID)を持つ行を INSERT する必要があります。この手順を省くと、データベース上には存在するのに画面には永遠に表示されません。
次のステップ
- iDempiere のナビゲーションバーに「人員増補申請」メニューが表示されていることを確認
- Synchronize Terminology:System Admin → General Rules → System → Synchronize Terminology を実行してフィールド翻訳を再構築
C_DocType文書タイプを作成し、このワークフローに関連付ける- 各承認ノードの
AD_WF_Responsibleを設定(実際の承認者を割り当て) MHeadcountRequest.javaを実装(Model class —MAbsenceNoteを参考に)
雷公曰
一枚の人員増補申請書が、書類確認、命名の儀式、アクセス付与、ワークフロー骨格、承認チェーンを経て、ついに名前と場所を持つワークフローになりました。
雷公曰:ワークフローは約束であり、コードは契約であり、SQL は儀式です。儀式が完了すれば、仕事は成し遂げられます。
