本指南涵蓋三種新增功能的場景:
- 設定驅動文件 — 標準列表/詳情畫面(~50 行,10 分鐘)
- 自訂功能 — 具有自訂畫面的複雜 UI(完整實作)
- 企業自訂視窗 — 不修改核心程式碼即可新增設定
場景一:設定驅動文件(推薦)
大多數 iDempiere 視窗遵循 列表 → 詳情 → 動作 的模式。這些只需一個設定檔即可新增。
步驟 1:辨識視窗
在 iDempiere 中找到 AD_Window_ID 和表格名稱:
SELECT AD_Window_ID, Name FROM AD_Window WHERE Name LIKE '%Your Window%';
-- 範例: AD_Window_ID=143, Name='Sales Order'
-- 找到主表格:
SELECT t.TableName FROM AD_Tab tab
JOIN AD_Table t ON tab.AD_Table_ID = t.AD_Table_ID
WHERE tab.AD_Window_ID = 143 AND tab.SeqNo = 10;
-- 範例: C_Order
步驟 2:了解資料結構
開啟 iDempiere REST API explorer 或查看表格結構:
- 關鍵欄位 — 列表卡片要顯示什麼(DocumentNo、Name 等)
- 外鍵 — 以
_ID結尾、需要$expand的欄位(C_BPartner_ID 等) - 狀態欄位 — 文件用
DocStatus,主資料用IsActive - 明細行 — 子表格名稱和父外鍵欄位
- 排序欄位 — 預設排序方式
步驟 3:建立設定檔
建立 lib/features/<feature_name>/<feature_name>_config.dart:
範例 A:交易文件(銷售訂單模式)
import 'package:flutter/material.dart';
import '../../core/document_framework/config/document_config.dart';
import '../../core/document_framework/config/field_config.dart';
import '../../core/document_framework/config/filter_config.dart';
final myFeatureConfig = DocumentConfig(
// --- 識別 ---
targetId: 143, // AD_Window_ID
tableName: 'C_Order', // 主表格
title: (l10n) => l10n.myFeatureTitle, // 本地化標題
icon: Icons.shopping_cart,
color: Colors.blue,
category: 'sales', // 儀表板循環頁籤
// --- 列表畫面 ---
baseFilter: "IsSOTrx eq 'Y'", // 永久篩選(選用)
defaultOrderBy: 'DateOrdered desc',
expandFields: 'C_BPartner_ID,C_DocType_ID', // 列表的外鍵展開
searchFields: const ['DocumentNo', 'Description'],
// 狀態篩選標籤
statusFilters: StatusFilter.docStatusWithIP,
// 卡片佈局
cardConfig: const CardConfig(
titleField: 'DocumentNo',
subtitleFields: [
DisplayField.lookup('C_BPartner_ID'), // 顯示展開外鍵的 "identifier"
],
trailingField: DisplayField.currency('GrandTotal'),
statusField: 'DocStatus',
dateField: 'DateOrdered',
),
// --- 詳情畫面 ---
detailConfig: const DetailConfig(
detailExpand: 'C_BPartner_ID,C_DocType_ID,M_Warehouse_ID',
headerFields: [
DetailField.lookup('C_BPartner_ID', label: 'Business Partner'),
DetailField.date('DateOrdered', label: 'Date Ordered'),
DetailField.lookup('C_DocType_ID', label: 'Doc Type'),
DetailField.currency('GrandTotal', label: 'Grand Total'),
],
// 明細行(選用)
linesTable: 'C_OrderLine',
linesParentField: 'C_Order_ID',
linesExpand: 'M_Product_ID',
linesFields: [
LineField.lookup('M_Product_ID'),
LineField.number('QtyOrdered', label: 'Qty'),
LineField.currency('LineNetAmt', label: 'Amount'),
],
// 彙總合計(選用)
summaryFields: [
SummaryField.currency('GrandTotal', label: 'Grand Total'),
],
),
// --- 文件動作 ---
actions: const [DocAction.complete, DocAction.void_],
actionCondition: (record) => record['DocStatus'] == 'DR',
// --- 路由 ---
listRoute: '/dashboard/my-feature',
detailRoute: '/dashboard/my-feature/:id',
);
範例 B:主資料(產品模式)
final myMasterConfig = DocumentConfig(
targetId: 140,
tableName: 'M_Product',
title: (l10n) => l10n.productTitle,
icon: Icons.inventory_2,
color: Colors.blue,
category: 'master',
baseFilter: 'IsSummary eq false', // Boolean 篩選(不加引號!)
defaultOrderBy: 'Value',
searchFields: const ['Value', 'Name', 'Description'],
// 使用 IsActive 篩選(而非 DocStatus)
statusFilterField: 'IsActive',
statusFilters: StatusFilter.isActiveFilters,
cardConfig: const CardConfig(
titleField: 'Value',
subtitleFields: [DisplayField.raw('Name')],
// 無 statusField, trailingField, dateField
),
detailConfig: const DetailConfig(
detailExpand: 'M_Product_Category_ID,C_UOM_ID',
headerFields: [
DetailField('Value', label: 'Value'),
DetailField('Name', label: 'Name'),
DetailField.lookup('M_Product_Category_ID', label: 'Category'),
DetailField.lookup('C_UOM_ID', label: 'UOM'),
],
// 無明細行、無彙總、無動作
),
listRoute: '/dashboard/product',
detailRoute: '/dashboard/product/:id',
);
範例 C:含額外篩選的文件(付款模式)
final paymentConfig = DocumentConfig(
targetId: 195,
tableName: 'C_Payment',
title: (l10n) => l10n.paymentTitle,
icon: Icons.payments,
color: Colors.green,
category: 'financing',
defaultOrderBy: 'DateTrx desc',
expandFields: 'C_BPartner_ID,C_DocType_ID,C_Currency_ID',
searchFields: const ['DocumentNo', 'Description'],
statusFilters: StatusFilter.docStatusFilters,
// 額外篩選:收款 vs 付款切換
extraFilters: const [
ExtraFilter(
field: 'IsReceipt',
label: 'Type',
options: [
FilterOption("'Y'", 'Receipt'),
FilterOption("'N'", 'Payment'),
],
),
],
cardConfig: const CardConfig(
titleField: 'DocumentNo',
subtitleFields: [DisplayField.lookup('C_BPartner_ID')],
trailingField: DisplayField.currency('PayAmt'),
statusField: 'DocStatus',
dateField: 'DateTrx',
),
// ... detailConfig, actions, routes
);
步驟 4:新增本地化 Key
新增到 lib/l10n/app_zh_TW.arb(範本):
"myFeatureTitle": "我的功能"
新增到 lib/l10n/app_en.arb:
"myFeatureTitle": "My Feature"
執行:
flutter gen-l10n
步驟 5:註冊設定
在 lib/core/document_framework/config/standard_configs.dart 中:
import '../../../features/my_feature/my_feature_config.dart';
void registerStandardConfigs() {
DocumentRegistry.registerAll([
// ... 現有的 configs ...
myFeatureConfig, // 在此新增
]);
}
完成!
框架會自動:
- 透過
generateDocumentRoutes()建立列表和詳情路由 - 透過
DocumentRegistry.find(action, targetId)對應選單項目 - 渲染含搜尋、篩選標籤、分頁的列表畫面
- 渲染含標頭欄位、明細行、文件動作的詳情畫面
- 自動處理 OData 篩選建構、Boolean vs String 欄位類型
驗證
flutter analyze # 無錯誤
flutter run # 在裝置上測試 — 找到選單項目
欄位類型參考
DisplayField 建構子
| 建構子 | 用途 | 渲染結果 |
|---|---|---|
DisplayField.raw('Name') |
純文字 | record['Name'].toString() |
DisplayField.lookup('C_BPartner_ID') |
外鍵 | record['C_BPartner_ID']['identifier'] |
DisplayField.currency('GrandTotal') |
金額 | "1234.56"(2 位小數) |
DisplayField.date('DateOrdered') |
日期 | "2026-02-15"(去除時間) |
DisplayField.boolean('IsActive') |
是/否 | "Yes" 或 "No" |
DisplayField.custom('Field', formatter: ...) |
自訂邏輯 | 自訂函式的回傳值 |
欄位類別階層
DisplayField ← 基礎類別(用於 CardConfig.subtitleFields)
├── DetailField ← 用於 DetailConfig.headerFields
├── LineField ← 用於 DetailConfig.linesFields
└── SummaryField ← 用於 DetailConfig.summaryFields
所有子類別共用相同的建構子(.raw()、.lookup()、.currency()、.date())。請根據上下文使用正確的類別。
CardConfig 欄位
CardConfig(
titleField: 'DocumentNo', // 主標題(從記錄取得的原始字串)
subtitleFields: [...], // 標題下方(DisplayField 列表)
trailingField: DisplayField..., // 右側(通常為金額)
statusField: 'DocStatus', // 狀態標籤顏色 + 文字
dateField: 'DateOrdered', // 狀態列中顯示的日期
)
篩選預設值
| 預設值 | 適用場景 | 選項 |
|---|---|---|
StatusFilter.docStatusFilters |
標準文件 | 全部、草稿、已完成、已作廢 |
StatusFilter.docStatusWithIP |
含進行中的文件 | 全部、草稿、進行中、已完成、已作廢 |
StatusFilter.isActiveFilters |
主資料 | 全部、啟用中、已停用 |
自訂篩選
extraFilters: const [
ExtraFilter(
field: 'IsSOTrx', // OData 欄位名稱
label: 'Transaction Type',
options: [
FilterOption("'Y'", 'Sales'), // 值必須是有效的 OData
FilterOption("'N'", 'Purchase'),
],
),
],
文件動作
actions: const [DocAction.complete, DocAction.void_],
actionCondition: (record) => record['DocStatus'] == 'DR',
| 動作 | 代碼 | 顯示條件 |
|---|---|---|
DocAction.complete |
CO | actionCondition 回傳 true |
DocAction.void_ |
VO | actionCondition 回傳 true |
DocAction.close |
CL | actionCondition 回傳 true |
DocAction.reverse |
RC | actionCondition 回傳 true |
DocAction.prepare |
PR | actionCondition 回傳 true |
場景二:自訂功能(複雜 UI)
當功能需要超越列表/詳情的互動 — 表單、日曆、儀表板、多步驟工作流程 — 請建立完整的自訂功能。
目錄結構
lib/features/my_feature/
├── data/
│ └── my_feature_repository.dart # API 呼叫
├── domain/
│ └── my_feature_notifier.dart # 狀態管理
├── presentation/
│ ├── my_feature_screen.dart # 主畫面
│ ├── my_feature_form_screen.dart # 表單畫面(選用)
│ └── widgets/ # 功能專屬小工具
└── my_feature_routes.dart # GoRoute 定義
Repository 模式
final myFeatureRepositoryProvider = Provider<MyFeatureRepository>((ref) {
return MyFeatureRepository(ref.watch(apiClientProvider));
});
class MyFeatureRepository {
final ApiClient _api;
MyFeatureRepository(this._api);
Future<List<Map<String, dynamic>>> getRecords() async {
final data = await _api.getRecords('My_Table',
filter: 'IsActive eq true',
orderBy: 'Name',
expand: 'C_BPartner_ID',
);
return (data['records'] as List?)?.cast<Map<String, dynamic>>() ?? [];
}
}
註冊路由
在 lib/core/router/app_router.dart 中新增:
import '../../features/my_feature/my_feature_routes.dart';
// 在 dashboard ShellRoute 的 routes 列表中:
...myFeatureRoutes,
註冊到 Enhanced Module Registry(如果對應到 iDempiere 選單)
在 lib/core/config/enhanced_module_registry.dart 中:
// 對於 AD_Window,使用 action 'W':
'W:YOUR_WINDOW_ID': const EnhancedModule(
action: 'W',
targetId: YOUR_WINDOW_ID,
route: '/dashboard/my-feature',
icon: Icons.your_icon,
color: Colors.yourColor,
category: 'your_cycle',
),
// 對於 AD_Form,使用 action 'X':
'X:YOUR_FORM_ID': const EnhancedModule(
action: 'X',
targetId: YOUR_FORM_ID,
route: '/dashboard/my-feature',
...
),
場景三:企業自訂視窗
企業部署可以在不修改核心程式碼的情況下新增自訂 DocumentConfig。
建立自訂設定目錄
lib/custom/
└── custom_configs.dart
註冊自訂設定
// lib/custom/custom_configs.dart
import '../core/document_framework/config/document_config.dart';
import '../core/document_framework/config/document_registry.dart';
import '../core/document_framework/config/field_config.dart';
import '../core/document_framework/config/filter_config.dart';
import 'package:flutter/material.dart';
/// 註冊企業專屬的增強畫面。
/// 在 main.dart 的 registerStandardConfigs() 之後呼叫。
void registerCustomConfigs() {
DocumentRegistry.registerAll([
// 在此放入您的自訂 iDempiere 視窗
DocumentConfig(
targetId: 1000001, // 您的自訂 AD_Window_ID
tableName: 'XX_MyTable',
title: (l10n) => 'My Custom Window',
icon: Icons.extension,
color: Colors.purple,
category: 'custom',
// ... card, detail, filters, routes
cardConfig: const CardConfig(titleField: 'Name'),
listRoute: '/dashboard/xx-my-table',
detailRoute: '/dashboard/xx-my-table/:id',
),
]);
}
整合到 main.dart
import 'custom/custom_configs.dart';
void main() async {
// ...
registerStandardConfigs();
registerCustomConfigs(); // 在標準設定之後新增
// ...
}
重要事項
- 企業自訂設定在標準設定之後註冊
- 無需修改核心程式碼 — 只需在
lib/custom/下新增檔案 - 權限仍由伺服器驅動(使用者必須有該視窗的 AD_Window_Access)
- 建置並發布包含自訂設定的專屬 App 版本
提交前檢查清單
| 項目 | 說明 |
|---|---|
| 設定檔已建立 | lib/features/<name>/<name>_config.dart |
| 本地化 Key 已新增 | app_zh_TW.arb 和 app_en.arb |
| 設定已註冊 | standard_configs.dart(或 custom_configs.dart) |
| 本地化已產生 | flutter gen-l10n 已執行 |
| 靜態分析通過 | flutter analyze 無錯誤 |
| 裝置測試通過 | 列表載入、詳情開啟、動作運作正常 |
🌐 English Version
Three Scenarios for Adding Features
- Config-driven document — standard list/detail screen (~50 lines, 10 minutes)
- Custom feature — complex UI with its own screens (full implementation)
- Enterprise custom window — adding a config without modifying core code
Scenario 1: Config-Driven Document (Recommended)
Most iDempiere windows follow a list → detail → action pattern. These can be added with a single config file.
Steps
- Find the
AD_Window_IDand table name in iDempiere - Understand the data structure (key columns, FKs, status field, line items)
- Create
lib/features/<name>/<name>_config.dartwith aDocumentConfig - Add localization keys to ARB files and run
flutter gen-l10n - Register in
standard_configs.dart
The framework automatically creates routes, maps menu items, renders list/detail screens with search, filters, pagination, and doc actions.
Field Types
| Constructor | Use For | Renders As |
|---|---|---|
DisplayField.raw() |
Plain text | record[field].toString() |
DisplayField.lookup() |
Foreign key | record[field]['identifier'] |
DisplayField.currency() |
Money | 2-decimal formatted number |
DisplayField.date() |
Date | Date without time |
DisplayField.boolean() |
Yes/No | “Yes” or “No” |
Filter Presets
StatusFilter.docStatusFilters— All, Draft, Complete, VoidedStatusFilter.docStatusWithIP— All, Draft, In Progress, Complete, VoidedStatusFilter.isActiveFilters— All, Active, Inactive
Scenario 2: Custom Feature (Complex UI)
Use the full data/domain/presentation/ structure when you need forms, calendars, dashboards, or multi-step workflows.
- Create directory under
lib/features/my_feature/ - Implement repository → notifier → screens → routes
- Register routes in
app_router.dart - Register in
enhanced_module_registry.dartif mapped to iDempiere menu
Scenario 3: Enterprise Custom Window
Add custom configs in lib/custom/custom_configs.dart without modifying core code. Register after registerStandardConfigs() in main.dart.
Checklist
- Config file created
- Localization keys added
- Config registered
flutter gen-l10nrunflutter analyzepasses- Tested on device