新增功能指南

本指南涵蓋三種新增功能的場景:

  1. 設定驅動文件 — 標準列表/詳情畫面(~50 行,10 分鐘)
  2. 自訂功能 — 具有自訂畫面的複雜 UI(完整實作)
  3. 企業自訂視窗 — 不修改核心程式碼即可新增設定

場景一:設定驅動文件(推薦)

大多數 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.arbapp_en.arb
設定已註冊 standard_configs.dart(或 custom_configs.dart
本地化已產生 flutter gen-l10n 已執行
靜態分析通過 flutter analyze 無錯誤
裝置測試通過 列表載入、詳情開啟、動作運作正常
🌐 English Version

Three Scenarios for Adding Features

  1. Config-driven document — standard list/detail screen (~50 lines, 10 minutes)
  2. Custom feature — complex UI with its own screens (full implementation)
  3. 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

  1. Find the AD_Window_ID and table name in iDempiere
  2. Understand the data structure (key columns, FKs, status field, line items)
  3. Create lib/features/<name>/<name>_config.dart with a DocumentConfig
  4. Add localization keys to ARB files and run flutter gen-l10n
  5. 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, Voided
  • StatusFilter.docStatusWithIP — All, Draft, In Progress, Complete, Voided
  • StatusFilter.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.

  1. Create directory under lib/features/my_feature/
  2. Implement repository → notifier → screens → routes
  3. Register routes in app_router.dart
  4. Register in enhanced_module_registry.dart if 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-l10n run
  • flutter analyze passes
  • Tested on device