ABAP 圖書館 · S/4HANA

總覽:如何使用這座圖書館

這是把 Dropbox\09_SAP_AB_ABAP編程 多年累積的教材,整理成可以隨時查閱的 ABAP 線上圖書館。所有程式碼都已改寫為 SAP S/4HANA 1809 以後可直接使用的現代 ABAP 寫法(內嵌宣告、@ 逸出主機變數、建構式運算、CL_SALV_TABLE、class-based exception)。

資料來源00_SAP_DOC 十六本自編教材(ABAP_01_1 ~ ABAP_04_4,2509 版)
語法基準ABAP 7.52 / S/4HANA 1809 以上,並標註 7.54~7.57 才有的語法
閱讀方式左側選章 → 右側內文;上方搜尋框可全文搜尋;每段程式碼右上可一鍵複製

1. 四部曲學習路線

主題學完可以做什麼章節
第一部資料字典:讓模組顧問自由自在的飛看得懂 SAP 資料結構、會建 Z 表與維護畫面、不寫程式也能出報表1-1 ~ 1-4
第二部內表與 SQL:模組顧問進化成 ABAPER寫得出可上線的 ALV 報表,會用 7.40+ 新語法與效能寫法2-1 ~ 2-4
第三部視窗報表與增強:顧問會程式等於空間無限設計選擇畫面、OOALV、看得懂並寫得出 User Exit / BAdI3-1 ~ 3-4
第四部進階技巧:目標成為專職的 ABAPER介面(RFC/BAPI)、對話式程式、表單、批次匯入與排程4-1 ~ 4-4
建議路線:先把 2-2 內表與 ABAP SQL 讀熟(那是整座圖書館的地基),再回頭補 1-2 資料字典,接著 2-3 ALV 就能產出第一支能交付的報表。

2. 這份文件的體例

  • SE38 這樣的標記代表交易碼,可直接在 SAP 命令列輸入。
  • 藍底方塊是補充說明、綠底方塊是實務技巧或老師口訣、紅底方塊是 S/4HANA 注意事項或常見錯誤,紅色的請務必看。
  • (依講義步驟整理) 代表原講義該處只有截圖,內文是依 SAP 標準流程補寫成可操作的文字步驟。
  • 每章最後都有「練習與自我檢核」,題目沿用原教材編號,方便對照舊講義。

3. 開發前的三個提醒

一、HANA 沒有預設排序。同一支 SELECT 不加 ORDER BY,在 HANA 上兩次執行可能回傳不同順序。任何「取第一筆」的邏輯(SELECT SINGLEUP TO 1 ROWSREAD TABLE ... INDEX 1)都必須先明確排序,否則結果會時對時錯。實務上就發生過成本評價同時存在 item category「H」與 cost component「E」兩種角度、程式沒排序而一下抓到 H、一下抓到 E,導致成本忽高忽低的案例。
二、條件用「列舉」比用「區間」安全。選擇條件用區間(BT)時,比較的是字典序而非業務語意,號碼段不連續、有 ALPHA 前導零、編碼規則改過的時候就會漏抓或多抓;改成逐項列舉(多筆 EQ)結果才正確。詳見 3-1 選擇畫面
三、先找標準,再想客製。順序永遠是:標準設定 → BAdI/Enhancement Spot → Enhancement Point → 修改標準(Modification)。每往右一步,升級時的維護成本就高一個量級。

4. 最常用的交易碼

類別交易碼用途
開發SE38 / SE80 / SE24 / SE37 / SE11 / SE51 / SE41 / SE93程式、物件瀏覽器、類別、函數、字典、畫面、選單、交易碼
資料SE16N / SE16H / SM30 / SM34 / SE54 / SE14查表、維護表、維護視圖與表群組、資料庫工具
除錯/h、SAT、ST05、SE30、ST22、SM21、SDBE除錯、執行時間分析、SQL 追蹤、短傾印、系統日誌
增強CMOD / SMOD / SE18 / SE19 / SE84客戶出口專案、BAdI 定義與實作、物件導覽
介面SM59 / SM58 / SMQ1 / SMQ2 / BAPI / SOAMANAGERRFC 目的地、tRFC/qRFC 佇列、BAPI 瀏覽、Web Service
批次SHDB / SM35 / SM36 / SM37 / SP01錄製、批次輸入、排程作業、Spool
表單SMARTFORMS / SMARTSTYLES / SE71 / SFP / SE78 / NACESmartForms、樣式、SAPscript、Adobe Form、圖檔、輸出決定
傳輸SE09 / SE10 / SE01 / STMS / SCC1傳輸請求與傳輸管理

5. 最常用的資料表(S/4HANA 版)

模組資料表S/4HANA 變動
MM 物料MARA / MARC / MARD / MBEW / MAKT / MCHBMARD、MBEW 等庫存與評價表在 S/4 為相容視圖(NSDM),實體資料在 MATDOC 聚合
MM 採購EKKO / EKPO / EKET / EKBE / EBAN大致不變
MM 異動MATDOC取代 MKPF / MSEG,後兩者保留為相容視圖
SDVBAK / VBAP / LIKP / LIPS / VBRK / VBRP / PRCD_ELEMENTSVBUK / VBUP 已併入 VBAK / VBAP;KONV 改名 PRCD_ELEMENTS
FI / COACDOCA / BKPF / BSEG / SKA1 / SKB1ACDOCA 為總帳與明細的單一真相來源,BSIS/BSAS/COEP/GLT0 等已成視圖
PPAUFK / AFKO / AFPO / AFVC / RESB / STPO / MAST大致不變
主檔KNA1 / LFA1 / BUT000 / BUT020客戶與供應商統一走 Business Partner(BP),XD01 / XK01 停用
組織T001 / T001W / T001L / T001K / TCURR大致不變

6. 這份圖書館還可以怎麼長大

同目錄下的 CLAUDE.mdSKILL.md 已定義好擴充規則。之後想加東西時,只要把新資料放進 Dropbox 對應資料夾,然後告訴 Claude「照 SKILL.md 把 XXX 加進 ABAP 圖書館」,就會依同樣的體例、同樣的 S/4HANA 語法基準續寫,章節編號與側欄目錄會自動接上。可以優先補的方向:

  1. 09_ABAP二次開發 的 SD / MM / PP / FI / CO / QM 實務案例(ZMMI0001、ZPPB0002 缺料表、DBCO 連 SQL Server 等)。
  2. 15_累計心得 的常用日期函數、HR 表、ACDOCA 筆記、透過欄位查表名。
  3. L36 的 136 個 ABAP OOP 範例、L31 的 36 套源碼。
  4. 原廠 BC 教材(BC400 / BC401 / BC402 / BC405 / BC410 / BC414 / BC425 / BC430 / BC470 / BC490)的重點摘要與對照。
  5. CDS View、RAP、ABAP Cloud 這一塊全新的內容,目前教材尚未涵蓋。

第 1-1 章 編寫第一支程式:SE38 / SQL / 內表 / WRITE / Package / Request / STMS

這一章把 ABAP 開發者第一週要會的事一次講完:認得住十個核心 TCODE、在 SE38 建出第一支 Z 開頭的報表、寫出第一句 ABAP SQL、把資料放進內表再輸出、把程式歸進 Package 並掛到傳輸請求,最後沿著 DEV → QAS → PRD 傳出去。學完之後,你能獨立完成「查資料 → 寫程式 → 存檔激活 → 執行 → 傳輸」的完整迴圈,並且知道哪些是講義裡的舊寫法、在 S/4HANA 該怎麼改。

核心 TCODESE38SE11SE16NSE16HSE80SE21SE09SE10STMSSE93SA38SE37SE14SM30SU53SCIABAPDOCUSPROVA03FB03CO03ME23NKSB5
關鍵物件VBAK、VBAP、LIKP、LIPS、MARA、MAKT、MARC、MARD、BKPF、BSEG、ACDOCA、MATDOC、T002、TRDIR、TADIR、E070/E071、CL_SALV_TABLE

1. 認識十個核心 TCODE(先會查,才會寫)

老師的第一堂課不是教語法,而是要你先「看得懂 SAP 在做什麼」。寫 ABAP 的人如果不會用標準交易查資料,寫出來的程式一定跟業務對不上。下表是講義列出的十個 TCODE,建議背下來當日常工具。

TCODE用途備註/對應資料表
SE11ABAP Dictionary 資料字典看 Table/View/Data Element/Domain 的定義與外鍵
SE38ABAP Editor 建立與執行程式本章主戰場
SPRO後台配置 IMG能配置解決的,就不要寫程式
SE16N維護與瀏覽資料(含技術欄位、可組合條件)可以拿來「學寫 SQL」,見本章第 4 節
SE16瀏覽資料(較陽春的資料瀏覽器)S/4HANA 亦可用 SE16N 取代
VA03瀏覽銷售訂單VBAK(主檔)/VBAP(明細)
FB03瀏覽 FI 會計傳票BKPF(表頭)/BSEG(行項目);S/4 讀 ACDOCA
CO03瀏覽生產工單AUFK/AFKO/AFPO/AFVC
ME23N瀏覽採購單EKKO(表頭)/EKPO(明細)/EKBE(歷史)
KSB5瀏覽 CO 管理會計傳票COEP/COBK;S/4 亦落在 ACDOCA
口訣:SAP 幾乎所有業務單據都是「主檔+子檔」成對出現。VBAK/VBAP(訂單)、LIKP/LIPS(交貨)、EKKO/EKPO(採購)、BKPF/BSEG(會計)、AUFK/AFPO(工單)、VBRK/VBRP(發票)。抓到這個規律,第一次面對陌生模組也不會慌。
作業 1-01:找出 FI、CO、SD、MM、PP 五個模組的「重大雙檔」(主檔/子檔配對)。
參考答案方向:FI=BKPF/BSEG(S/4 為 ACDOCA);CO=COBK/COEP;SD=VBAK/VBAP、LIKP/LIPS、VBRK/VBRP;MM=EKKO/EKPO、MKPF/MSEG(S/4 為 MATDOC);PP=AUFK+AFKO/AFPO。

2. 開發環境:SE38、SE80 與 ADT(Eclipse)

ABAP 有三種主要的開發入口,功能有重疊但定位不同:

  • SE38(ABAP Editor):最直接的入口,適合寫可執行報表(Type 1)。初學者從這裡開始。
  • SE80(Object Navigator 物件瀏覽器):以「Package/程式/Function Group/Class/Screen」為樹狀結構管理,可以一次看到一個開發物件底下的所有 Include、畫面、文字元素。建 Function Group、Module Pool、Class 建議走 SE80。
  • ADT(ABAP Development Tools for Eclipse):S/4HANA 時代的正式開發工具。CDS View、AMDP、RAP(Restful ABAP Programming Model)、ABAP Unit 的完整體驗只有 ADT 有,SE80 在 S/4 已被 SAP 標示為「不再增強」。
S/4HANA 注意:CDS View(DDLS)、Behavior Definition、Service Binding 等物件無法在 SE80 建立,必須用 Eclipse + ADT。SE80 仍可用來看舊物件,但新開發建議一律 ADT。SE80 在 SAP BTP ABAP Environment(雲端)更是完全不存在。
GUI 端仍然需要:Debug、SM30 維護、SE93 建 TCODE、STMS 傳輸這些工作在 GUI 做比較順。實務上是「Eclipse 寫程式 + GUI 做運維」兩邊並用。

3. 寫程式的四步驟與命名規則(Z/Y 命名空間)

講義把建立第一支程式拆成四個動作,這四步在 S/4HANA 依然完全一樣:

  1. 程式名稱一定要 Z 開頭(或 Y 開頭)。
  2. 設定屬性:Title、Type、Status、Application。
  3. 指定 Package:練習環境可選 Local Object($TMP);正式開發要放進自己的 Package(例如講義用的 Z600_2409_ERP000)。
  4. 執行程式:F8,或用 SA38/SE93 建成 TCODE 執行。

命名空間規則:SAP 保留了所有不以 Z/Y 開頭的名稱給標準物件。客戶自建物件只能用 Z*Y*;若公司有向 SAP 申請的專屬命名空間,則是 /ABCDE/ 這種斜線包起來的形式。表格、資料元素、Domain、Class、Function Module 全部適用同一規則。

Program Status意義使用時機
PSAP Standard Production Program(標準生產程式)SAP 原廠用,客戶不選
KCustomer Production Program(客戶生產程式)預設選這一項,正式上線的客製程式
SSystem Program(系統程式)系統層級工具
TTest Program(測試程式)臨時測試、不打算上線的
實務命名建議:Z + 模組 + 用途 + 序號,例如 ZSD_ORDER_LIST01ZFI_ACDOCA_RPT。看名字就知道歸誰維護。講義的課堂命名是 Z2202_00_01 這種「課程代號_學號_序號」,方便老師點名批改。
$TMP(Local Object)的物件永遠無法傳輸到 QAS/PRD。練習可以用,但只要這支程式將來要上線,一開始就放進正式 Package,不然事後搬家(SE03 → Change Object Directory Entry)很麻煩。

4. 用 SE16N/SE16H 學寫 SQL

老師的心法:不會寫 SQL 就先去 SE16N 點一遍。SE16N 的選擇畫面本質上就是一句 SELECT,把條件填一填、按執行,你要的 WHERE 條件就成形了。

  • SE16N 進入後在指令列輸入 &SAP_EDIT 可開啟編輯模式(正式機嚴禁使用,會繞過所有業務邏輯與變更文件)。
  • SE16N 支援「技術欄位名稱/描述」切換、可指定輸出欄位、可設定筆數上限。
  • SE16H(HANA 版)更強:可直接做 Group By、Sum、Count、以及多表 JOIN,等於在畫面上做出一份聚合報表,很適合先驗證邏輯再寫成 ABAP。
作業 2-01:用 SE16H 把作業 1-01 找到的 FI、CO、SD、MM、PP 重大雙檔做成關聯(在 SE16H 的 Table Join 區塊填入第二張表與關聯欄位,例如 VBAK-VBELN = VBAP-VBELN)。
SE16H 的 Group By/Sum 功能是「不寫程式就能出報表」的最快路徑,很多臨時性的查數需求根本不必開發。這也呼應本章最後一節「解決企業問題的手段與順序」。

5. 第一支 SELECT:從講義的舊寫法到 S/4HANA 現代寫法

講義的第一支程式 Z2202_00_01(取出料號為 DXTR1000 的訂單明細)原本是這樣寫的(此處刻意保留作為反例):

" ===== 反例:講義原始寫法,S/4HANA 請勿再這樣寫 =====
REPORT z2202_00_01.
TABLES: vbap.                          " 宣告了整張表當共用工作區
SELECT * INTO CORRESPONDING FIELDS OF vbap
  FROM vbap
  WHERE matnr = 'DXTR1000'.            " 主機變數沒有 @、寫死常數
  WRITE: / vbap-vbeln, vbap-posnr, vbap-matnr,
           vbap-meins, vbap-kwmeng.
ENDSELECT.                             " 一筆一筆丟回 AP Server,效能最差
上面這段有四個 S/4HANA 的地雷:
1. TABLES: 會產生全域工作區,在 OO 環境與現代 ABAP 中已被視為 obsolete(ABAP Objects 語法檢查會直接報錯)。
2. SELECT ... ENDSELECT 是逐筆迴圈,每一圈都在 DB 與 AP Server 間往返,違反 S/4HANA「Code Push-Down」原則。
3. SELECT * 在 HANA 的欄式儲存上代價極高——只取 5 個欄位卻讀了 VBAP 全部兩百多欄。
4. WHERE 條件的主機變數沒有逸出字元 @,在嚴格語法模式下無法通過。

改寫成 S/4HANA 1809 之後的現代 ABAP:

REPORT z2202_00_01.

" 第一支 SELECT 的現代寫法:明列欄位 + 內嵌宣告 + 一次抓進內表
SELECT vbeln, posnr, matnr, meins, kwmeng
  FROM vbap
  WHERE matnr = 'DXTR1000'          " 常數不需要 @;只有主機變數才要
  INTO TABLE @DATA(lt_vbap).

IF lt_vbap IS INITIAL.
  MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'W'.
  RETURN.
ENDIF.

LOOP AT lt_vbap ASSIGNING FIELD-SYMBOL(<ls_vbap>).
  WRITE: / <ls_vbap>-vbeln,
           <ls_vbap>-posnr,
           <ls_vbap>-matnr,
           <ls_vbap>-meins,
           <ls_vbap>-kwmeng.
ENDLOOP.
講義標註的「進化三段」正是這支程式的學習路徑:1. UP TO n ROWS(限制筆數) → 2. Field List(明列欄位) → 3. WHERE(加條件)。開發時養成習慣:先 UP TO 10 ROWS 試跑,確認欄位與資料對了,再拿掉限制。
" 開發階段的安全寫法:先限制筆數再放大
SELECT vbeln, posnr, matnr, meins, kwmeng
  FROM vbap
  WHERE matnr = 'DXTR1000'
  ORDER BY vbeln, posnr                 " 沒有 ORDER BY 就沒有順序保證
  INTO TABLE @DATA(lt_vbap)
  UP TO 10 ROWS.
HANA 沒有「預設排序」這回事。舊 DB 常常剛好照主鍵回傳,HANA 是平行處理,不下 ORDER BY 的結果順序完全不保證,同一支程式跑兩次可能不同。凡是「抓第一筆」「比對前後筆」的邏輯,一定要明確 ORDER BY。SELECT SINGLE 若 WHERE 不是完整主鍵,回傳的是任意一筆,不是「第一筆」。

6. 選擇畫面 PARAMETERS 與型態宣告(Search Help 的關鍵)

講義 Z2202_00_02 讓使用者自己輸入料號;Z2202_00_03 則刻意示範「宣告方式不同,會不會有下拉選單(Search Help)」的差別。這是初學者最容易踩的坑。

REPORT z2202_00_02.

" 寫法 A:直接用 DDIC 欄位型態 —— 自動繼承 Search Help、轉換常式、欄位說明
PARAMETERS p_matnr TYPE vbap-matnr.

SELECT vbeln, posnr, matnr, meins, kwmeng
  FROM vbap
  WHERE matnr = @p_matnr               " 主機變數一律加 @
  INTO TABLE @DATA(lt_vbap).

LOOP AT lt_vbap INTO DATA(ls_vbap).
  WRITE: / ls_vbap-vbeln, ls_vbap-posnr, ls_vbap-matnr,
           ls_vbap-meins, ls_vbap-kwmeng.
ENDLOOP.
REPORT z2202_00_03.

" 寫法 B:自訂型態 —— 只有長度、沒有語意,畫面上「沒有下拉選單」
TYPES: ty_matnr(40) TYPE c.
PARAMETERS p_bad TYPE ty_matnr.        " 反例:F4 出不來

" 寫法 C:型態指向 Data Element —— Search Help 會回來
TYPES: ty_matnr_dd TYPE matnr.         " matnr 是 Data Element
PARAMETERS p_ok TYPE ty_matnr_dd.

" 寫法 D:最直接、實務最常用
PARAMETERS p_best TYPE mara-matnr.
口訣:「型態要接到 DDIC,F4 才會來」
TYPE c LENGTH 40 → 沒有 Search Help;
TYPE matnr(Data Element)→ 有 Search Help(Data Element 掛了 Search Help 或 Domain 有 Value Table);
TYPE mara-matnr(表-欄位)→ 有 Search Help,且欄位說明、外鍵檢查一起繼承。
關聯鏈要記住:VBAP-MATNR → Data Element MATNR → Domain MATNR → Value Table MARA。Search Help 與外鍵檢查就是沿著這條鏈走出來的。
S/4HANA 的 MATNR 長度已由 18 碼延伸為 40 碼(Extended Material Number)。所有自訂欄位、介面結構請一律 TYPE matnr,不要自己寫 CHAR18,否則在 S/4 上會被截斷。講義寫 ty_matnr(40) 正是為了配合 S/4 的長度,但仍建議直接用 DDIC 型態。

7. 工作區與內表:從 TYPES 到 Internal Table

講義 Z2202_00_04 的重點在於建立四層概念:TYPES(型態) → STRUCTURE(結構) → WORK AREA(工作區) → INTERNAL TABLE(內表)。這是所有 ABAP 程式的骨架。

REPORT z2202_00_04.

" ① 定義結構型態(只是藍圖,不佔記憶體)
TYPES: BEGIN OF ty_vbap,
         vbeln  TYPE vbap-vbeln,
         posnr  TYPE vbap-posnr,
         matnr  TYPE vbap-matnr,
         meins  TYPE vbap-meins,
         kwmeng TYPE vbap-kwmeng,
       END OF ty_vbap.

" ② 定義內表型態(表格藍圖)
TYPES: ty_t_vbap TYPE STANDARD TABLE OF ty_vbap WITH EMPTY KEY.

" ③ 真正配置記憶體:工作區(一列)與內表(多列)
DATA: ls_vbap TYPE ty_vbap,           " Work Area = 一列
      lt_vbap TYPE ty_t_vbap.         " Internal Table = 多列

" ④ 建構式寫法:直接生出一列 / 一張表,不必逐欄位搬
ls_vbap = VALUE #( vbeln = '0000000001'
                   posnr = '000010'
                   matnr = 'DXTR1000'
                   meins = 'PC'
                   kwmeng = 10 ).

lt_vbap = VALUE #( ( vbeln = '0000000001' posnr = '000010' matnr = 'DXTR1000' )
                   ( vbeln = '0000000001' posnr = '000020' matnr = 'DXTR2000' ) ).
ABAP 的基本資料型態(講義列出的):C(字元)、D(日期 YYYYMMDD)、P(壓縮十進位,金額數量用)、N(數字字串)、I(整數)、F(浮點)、STRING(變長字串)。另外還有 X(十六進位)、T(時間 HHMMSS)、XSTRING、以及 S/4 新增的 INT8(8-byte 整數)與 DECFLOAT16/34
金額欄位一律用 CURR(DDIC)/P DECIMALS 2(ABAP),絕不要用 F 浮點數算錢
內表三種型態的選擇:STANDARD TABLE(順序存取,APPEND 快,READ 需線性掃描)、SORTED TABLE(自動維持排序,二分搜尋)、HASHED TABLE(唯一鍵,READ 為 O(1),不能 APPEND 只能 INSERT)。做「大表對照查值」時用 HASHED,效能差距是數量級的。

8. 關鍵案例:用內表取代 ENDSELECT 提升效率

講義 Z2202_00_05 是整章最重要的觀念題:把 SELECT ... ENDSELECT 換成「一次抓進內表 + LOOP」。老師把它拆成十個編號步驟,建議直接背成模板。

REPORT z2202_00_05.

"=== 01 定義結構 =====================================
TYPES: BEGIN OF ty_vbap,
         vbeln  TYPE vbap-vbeln,
         posnr  TYPE vbap-posnr,
         matnr  TYPE vbap-matnr,
         meins  TYPE vbap-meins,
         kwmeng TYPE vbap-kwmeng,
       END OF ty_vbap.

"=== 02 定義內表(工作區改用 LOOP 內嵌宣告,不必另外開)====
DATA lt_vbap TYPE STANDARD TABLE OF ty_vbap WITH EMPTY KEY.

"=== 03 選擇畫面:型態接 DDIC,才有下拉選單 =============
PARAMETERS p_matnr TYPE mara-matnr OBLIGATORY.

"=== 04 一次讀進內表(只跟 DB 往返一次)=================
START-OF-SELECTION.
  SELECT vbeln, posnr, matnr, meins, kwmeng
    FROM vbap
    WHERE matnr = @p_matnr
    ORDER BY vbeln, posnr
    INTO TABLE @lt_vbap.

  IF sy-subrc <> 0.
    MESSAGE |料號 { p_matnr ALPHA = OUT } 查無訂單明細| TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

"=== 05 迴圈輸出 =====================================
  LOOP AT lt_vbap ASSIGNING FIELD-SYMBOL(<ls>).
    WRITE: / <ls>-vbeln, <ls>-posnr, <ls>-matnr,
             <ls>-meins, <ls>-kwmeng.
  ENDLOOP.

  WRITE: / |共 { lines( lt_vbap ) } 筆|.
作業 3-01(講義解答 Z2202_00_06):請找出多餘的程式碼並刪除,並讓選擇螢幕的料號可下拉選單。
要刪的是 TABLES: VBAP.(用不到了)、TYPES: TY_MATNR(40) TYPE C.DATA: W_MATNR(沒被用到);要改的是把 PARAMETERS p_matnr TYPE ty_matnr 改成 TYPE mara-matnr,Search Help 就出來了。
老師說這支「可以用背的,久了就成習慣」。實務上 90% 的報表都是這個骨架:選擇畫面 → 一次 SELECT 進內表 → 加工 → 輸出。差別只在中間加工多複雜、輸出用 WRITE 還是 ALV。
效能觀念:SELECT ... ENDSELECT 每圈一次 DB 往返;一萬筆就是一萬次網路來回。改成 INTO TABLE 只有一次。這在傳統 DB 上就已經是常識,在 S/4HANA(DB 與 AP 分離、且 HANA 擅長批次回傳)差距更大。另外 INTO CORRESPONDING FIELDS OF TABLE 是「依名稱對應」,會多一層搬移成本且容易因欄位改名而靜默出錯——明列欄位 + INTO TABLE 才是首選。

9. 傳票案例:主子檔 JOIN 與 S/4HANA 的 ACDOCA

講義第 4 節用 FI 傳票示範「怎麼把主檔與子檔關聯起來」。原始寫法是兩支 SELECT ... ENDSELECT 各印各的,再進化成一句 JOIN。以下是現代化改寫。

REPORT z20211109_ncu.

" 舊 ECC 觀念:BKPF(表頭)與 BSEG(行項目)用 BUKRS + BELNR + GJAHR 關聯
PARAMETERS: p_gjahr TYPE bkpf-gjahr DEFAULT '2021',
            p_budat TYPE bkpf-budat DEFAULT '20211109'.

START-OF-SELECTION.
  SELECT h~gjahr, h~budat, h~cputm, h~usnam, h~belnr,
         h~tcode, h~blart,
         d~buzei, d~koart, d~valut, d~prctr, d~dmbtr,
         d~shkzg, d~pargb, d~bschl, d~kokrs, d~kostl,
         d~menge, d~werks
    FROM bkpf AS h
    INNER JOIN bseg AS d
      ON  h~bukrs = d~bukrs        " 三個鍵都要接,少一個就是笛卡兒乘積
      AND h~belnr = d~belnr
      AND h~gjahr = d~gjahr
    WHERE h~gjahr = @p_gjahr
      AND h~budat = @p_budat
    ORDER BY h~usnam, h~belnr, d~buzei
    INTO TABLE @DATA(lt_accts).

  LOOP AT lt_accts ASSIGNING FIELD-SYMBOL(<ls>).
    WRITE: / <ls>-gjahr, <ls>-budat, <ls>-cputm, <ls>-usnam,
             <ls>-belnr, <ls>-buzei, <ls>-tcode, <ls>-blart,
             <ls>-bschl, <ls>-dmbtr, <ls>-werks.
  ENDLOOP.
初學者最常見的語法錯誤:ABAP 每一句敘述都以「句點 .」結束,不是分號;冒號「:」是「鏈式敘述(chained statement)」的縮寫符號,例如 WRITE: a, b, c. 等同三句 WRITE。JOIN 時三個關聯鍵(BUKRS、BELNR、GJAHR)少接一個,就會變成笛卡兒乘積,筆數暴增且金額全錯——這是 FI 報表最常見的災難。
S/4HANA 重大變更:BSEG 在 S/4 仍存在但已非唯一真相來源,總帳的完整明細落在 ACDOCA(Universal Journal)
・BSEG 是 Cluster 概念遺留、欄位受限、且不能當作報表主表(無法有效索引)。
・FI + CO + AA + ML 已合併為單一 ACDOCA,不需要再 join COEP、FAGLFLEXA、ANEP。
・GLT0、FAGLFLEXT、BSIS/BSAS/BSID/BSAD/BSIK/BSAK 在 S/4 已改為 相容視圖(Compatibility View),可讀但效能差,新開發一律改讀 ACDOCA 或標準 CDS View(如 I_JournalEntryItem)。
" S/4HANA 建議寫法:直接讀 ACDOCA(Universal Journal)
SELECT rbukrs, gjahr, belnr, docln, budat, bldat, blart,
       racct, rcntr, prctr, segment,
       hsl, wsl, ksl, waers, drcrk, usnam
  FROM acdoca
  WHERE rldnr  = '0L'                 " 0L = Leading Ledger,一定要帶
    AND rbukrs = @p_bukrs
    AND gjahr  = @p_gjahr
    AND budat  = @p_budat
  ORDER BY rbukrs, gjahr, belnr, docln
  INTO TABLE @DATA(lt_acdoca).
在 SAP 環境中解決「雙檔關聯」的七種手段(講義原話):
1. SELECT 的 JOIN + WRITE / 2. SELECT 的 JOIN + ALV 報表 / 3. SQVI Query View / 4. SE11 建 View / 5. SE16H / 6. 轉出 Excel 再 VLOOKUP / 7. 轉出 Excel 再 Power BI。
S/4HANA 再加第 8 種:建 CDS View——它同時取代了 4(SE11 View)與 3(Query),而且能直接被 Fiori、Analytics、OData 消費。
作業 4-01:請指出 MARA、MARC、MARD、MAKT 的關聯性。
答案要點:MARA=物料主檔一般資料(鍵:MATNR);MARC=工廠層級(MATNR + WERKS);MARD=儲存地點層級庫存(MATNR + WERKS + LGORT);MAKT=物料多語系描述(MATNR + SPRAS)。層層往下加鍵,是 SAP 主資料的典型「一般→工廠→儲位」三層結構。

10. ACDOCA 與 MATDOC 重要欄位、SE37 建立 Function Group

講義第 5 節把 ACDOCA 的關鍵欄位整理成一張速查表,這是 S/4HANA FI/CO 開發的必備字典。

欄位意義欄位意義
MANDT集團代碼(Client)RLDNR分類帳碼(Ledger,0L=主帳)
GJAHR傳票年度BELNR / DOCLN傳票號碼/傳票項次
RBUKRS公司代碼RCNTR成本中心
PRCTR利潤中心RFAREA功能範圍
RACCT會計科目RBUSA業務範圍
KOKRS控制範圍SEGMENT財報段別
TSL交易幣別金額(Transaction)WSL交易幣別金額
HSL公司碼本位幣金額KSL集團幣別金額
POPER過帳期間BUDAT / BLDAT過帳日期/憑證日期
BLART文件類型BSCHL過帳代碼
USNAM輸入人員KTOSL交易代碼
LINETYPE行項目類別KTOPL科目表
EBELN採購編號KDAUF訂單編號
MATNR物料號碼WERKS工廠代碼
KUNNR客戶編號WWERT匯率轉換日期
KOART科目類別AFABE折舊範圍
ANLN1 / ANLN2主資產碼/次資產碼PRZNR企業流程
ERKRS營運考量(CO-PA)TAX_COUNTRY稅務國別
GKONT / GKOAR子科目碼/科目類別 D(客戶)K(供應商)DRCRK借貸方向 S/H
ACDOCA 的欄位數會隨 CO-PA 特性設定而變動,講義提到約在 470~520 欄之間。這也是為什麼在 ACDOCA 上絕對不能寫 SELECT *——一次讀五百欄的成本非常可觀。
MM 對應的變更:MKPF/MSEG 在 S/4HANA 已合併為 MATDOC(Material Document,含表頭與行項目),MKPF/MSEG 成為相容視圖。庫存彙總表 MARD-LABST、MCHB、MSKA 等在 S/4 是由 MATDOC 即時彙總而來(MATDOC_EXTRACT),因此 MB5B 這類期初期末庫存報表的邏輯已完全改寫。自建庫存報表請以 MATDOC 或標準 CDS(如 I_MaterialDocumentItem)為來源。

SE37 / SE80 建立 Function Group 與 Function Module

  • Function Group(程式類型 F)是 Function Module 的容器,一個 FG 底下可以有很多 FM,共用 FG 的全域資料。
  • 建立步驟:SE80 → 選 Function Group → 輸入名稱(例如講義的 Z600_ERP300)→ 存檔並指定 Package → 激活
  • 講義特別提醒(20250320):激活時要把 Include 程式(LZxxxTOP、LZxxxUXX)一併勾選激活,否則會失敗。這是新手最常卡住的地方。
  • SE11 產生表格維護畫面(Table Maintenance Generator)時也需要指定一個 Function Group,所以建議先把自己的 FG 建好。
" Function Module 的現代呼叫方式:例外用 class-based 或至少檢查 sy-subrc
DATA(lv_matnr_in) = CONV matnr( '000000000000001234' ).

CALL FUNCTION 'CONVERSION_EXIT_MATN1_OUTPUT'
  EXPORTING
    input  = lv_matnr_in
  IMPORTING
    output = DATA(lv_matnr_out).

" 更現代的等價寫法:字串範本的 ALPHA 選項,不必呼叫 FM
DATA(lv_disp) = |{ lv_matnr_in ALPHA = OUT }|.
作業 5-01:找出你認為最重要的 MATDOC 二十個欄位,並用第 9 節(原講義 4-1)所學寫成程式。
提示欄位:MBLNR、MJAHR、ZEILE、BUDAT、BLDAT、BWART、MATNR、WERKS、LGORT、CHARG、MENGE、MEINS、DMBTR、WAERS、SHKZG、LIFNR、KUNNR、EBELN、EBELP、AUFNR。

11. WRITE 清單輸出:格式、TOP-OF-PAGE、HIDE,以及 S/4 時代的定位

WRITE 是 ABAP 最古老的輸出方式,產生的是「Classical List(傳統清單)」。講義第 8 節用它示範了對齊、畫表格線、雙擊鑽取等技巧。

REPORT zdemo_write.

" ① 對齊:WRITE 後面的數字是「起始欄位(column)」
SELECT matnr, mtart FROM mara
  WHERE matnr BETWEEN 'P-100' AND 'P-200'
  ORDER BY matnr
  INTO TABLE @DATA(lt_mara)
  UP TO 10 ROWS.

TOP-OF-PAGE.
  " 每一頁的表頭:TEXT-001 等文字元素在 SE38 → 移至 → 文字元素 維護
  WRITE: /0 TEXT-001, 16 sy-vline,
          20 TEXT-002, 28 sy-vline,
          30 TEXT-003, 50 sy-vline.
  ULINE.

START-OF-SELECTION.
  LOOP AT lt_mara ASSIGNING FIELD-SYMBOL(<ls>).
    WRITE: /0  <ls>-matnr, 16 sy-vline,
            20 <ls>-mtart, 28 sy-vline,
            30 space,      50 sy-vline.
    ULINE.
  ENDLOOP.

  ULINE.
  WRITE: / |共 { lines( lt_mara ) } 筆|, 50 TEXT-001, sy-uname.
常用系統欄位:SY-VLINE(垂直線 │)、SY-ULINE(水平線 ─)、ULINE 敘述(畫整條線)、SY-UNAME(使用者)、SY-DATUM/SY-UZEIT(系統日期時間)、SY-SUBRC(回傳碼)、SY-TABIX(LOOP 內目前列號)、SY-INDEX(DO/WHILE 圈數)。
WRITE 的常用附加:NO-ZERO(去前導零)、NO-GAP(不留空白)、LEFT-JUSTIFIED / RIGHT-JUSTIFIEDUNDER f(對齊某欄)、DD/MM/YYYY(日期格式)、COLOR nDECIMALS n

HIDE 與 AT LINE-SELECTION:雙擊鑽取(Drill-down)——講義 08-6 示範顯示銷售訂單,雙擊某一行就顯示該料號的描述。

REPORT zdemo_hide.

DATA gt_makt TYPE SORTED TABLE OF makt WITH UNIQUE KEY matnr spras.
DATA gv_matnr TYPE vbap-matnr.

START-OF-SELECTION.
  SELECT vbeln, posnr, matnr FROM vbap
    WHERE matnr LIKE 'P-%'
    ORDER BY vbeln, posnr
    INTO TABLE @DATA(lt_vbap).

  SELECT matnr, spras, maktx FROM makt
    WHERE matnr LIKE 'P-%' AND spras = @sy-langu
    INTO CORRESPONDING FIELDS OF TABLE @gt_makt.

  LOOP AT lt_vbap ASSIGNING FIELD-SYMBOL(<ls>).
    gv_matnr = <ls>-matnr.
    WRITE: /10 <ls>-vbeln, 20 <ls>-posnr, 30 <ls>-matnr.
    HIDE gv_matnr.          " 把本行的料號藏進系統暫存區
  ENDLOOP.
  CLEAR gv_matnr.

AT LINE-SELECTION.
  " 雙擊時,系統自動把該行 HIDE 起來的值還原到 gv_matnr
  IF line_exists( gt_makt[ matnr = gv_matnr spras = sy-langu ] ).
    DATA(ls_makt) = gt_makt[ matnr = gv_matnr spras = sy-langu ].
    WRITE: / ls_makt-matnr, ls_makt-maktx.
  ELSE.
    MESSAGE |料號 { gv_matnr } 無描述| TYPE 'S'.
  ENDIF.
老師說:「這支程式可以當做簡單的追蹤問題樣版」。HIDE 的原理是把「目前這一行想帶到次清單的變數值」暫存在系統區,只有 AT LINE-SELECTION 被觸發時才依行號取回。除錯查資料時,比開 ALV 還快。
S/4HANA 時代 WRITE 的定位
・WRITE 清單沒有排序、篩選、匯出 Excel、版面配置(Layout)、列印預覽等功能,使用者一定會抱怨。
・Classical List 無法在 Fiori Launchpad/SAP GUI for HTML 以外的環境正常顯示,也不能被 OData 消費。
・因此 WRITE 現在的合理用途只剩三種:(1) 教學與觀念驗證、(2) 背景 Job 的執行 Log(Spool)、(3) 極簡的批次處理結果訊息
只要是給使用者看的報表,一律用 ALV:全螢幕/簡易報表用 CL_SALV_TABLE,需要編輯欄位、事件、細部控制才用 CL_GUI_ALV_GRID。舊的 REUSE_ALV_GRID_DISPLAY 仍能跑,但不建議新開發使用。
" 同一份資料,改用 ALV 只要三行 —— 這才是 S/4HANA 該交出去的東西
TRY.
    cl_salv_table=>factory(
      IMPORTING r_salv_table = DATA(lo_alv)
      CHANGING  t_table      = lt_vbap ).

    lo_alv->get_functions( )->set_all( abap_true ).   " 開啟工具列全部功能
    lo_alv->get_columns( )->set_optimize( abap_true ).
    lo_alv->get_display_settings( )->set_list_header( '銷售訂單明細' ).
    lo_alv->display( ).

  CATCH cx_salv_msg INTO DATA(lx_salv).
    MESSAGE lx_salv->get_text( ) TYPE 'E'.
ENDTRY.

12. JOIN 的差異、多語系 T002 與同名不同義的欄位

講義 08-2 用一支程式同時跑 INNER JOIN 與 LEFT JOIN,讓學生自己數筆數,體會兩者差別。老師的註解只有一句:「沒事別用 LEFT JOIN」

REPORT zdemo_join.

" ① INNER JOIN:兩邊都要有才出現。MAKT 沒有 E 語系的料號會消失
SELECT a~matnr, a~mtart, t~maktx
  FROM mara AS a
  INNER JOIN makt AS t ON a~matnr = t~matnr
  WHERE a~matnr BETWEEN 'P-100' AND 'P-200'
    AND t~spras = 'E'
  ORDER BY a~matnr
  INTO TABLE @DATA(lt_inner).

WRITE: / |INNER JOIN 筆數:{ lines( lt_inner ) }|.

" ② LEFT OUTER JOIN:MARA 全留,找不到描述的 MAKTX 為空
"    注意 SPRAS 條件必須寫在 ON 裡面,寫在 WHERE 會把 LEFT 打回 INNER
SELECT a~matnr, a~mtart, t~maktx
  FROM mara AS a
  LEFT OUTER JOIN makt AS t
    ON  a~matnr = t~matnr
    AND t~spras = 'E'
  WHERE a~matnr BETWEEN 'P-100' AND 'P-200'
  ORDER BY a~matnr
  INTO TABLE @DATA(lt_left).

WRITE: / |LEFT JOIN 筆數:{ lines( lt_left ) }|.

LOOP AT lt_left ASSIGNING FIELD-SYMBOL(<ls>).
  WRITE: / <ls>-matnr, <ls>-mtart, <ls>-maktx.
ENDLOOP.
LEFT JOIN 的兩個經典陷阱
1. 右表的過濾條件寫在 WHERE 就會退化成 INNER JOIN(因為 NULL 不滿足條件而被濾掉),必須寫在 ON 子句裡。
2. 若右表對同一鍵有多筆(例如 MAKT 沒限定 SPRAS),LEFT JOIN 會把左表的列數放大,金額類欄位再做 SUM 就會重複計算。這是報表金額對不起來最常見的原因。

語系 T002:SAP 用單碼語言鍵(SPRAS)。講義列出的常用對照:

語言SPRAS(單碼)ISO(雙碼)
簡體中文1ZH
繁體中文MZF
英文EEN
德文DDE
" 語系轉換:單碼 <-> ISO 雙碼
DATA lv_iso  TYPE laiso.
DATA lv_lang TYPE sy-langu.

" 單碼 -> ISO
CALL FUNCTION 'CONVERSION_EXIT_ISOLA_OUTPUT'
  EXPORTING input  = sy-langu
  IMPORTING output = lv_iso.

" ISO -> 單碼
CALL FUNCTION 'CONVERSION_EXIT_ISOLA_INPUT'
  EXPORTING input  = 'ZF'
  IMPORTING output = lv_lang.

" 最佳實務:不要寫死 'E',用 sy-langu,並在查不到時 fallback 到英文
SELECT SINGLE maktx FROM makt
  WHERE matnr = @lv_matnr AND spras = @sy-langu
  INTO @DATA(lv_maktx).
IF sy-subrc <> 0.
  SELECT SINGLE maktx FROM makt
    WHERE matnr = @lv_matnr AND spras = 'E'
    INTO @lv_maktx.
ENDIF.

同名不同義的欄位(講義 08-3):這是 SD 模組最容易搞混的地方。

欄位在訂單在交貨單
VBELNVBAK-VBELN/VBAP-VBELN=訂單編號LIKP-VBELN/LIPS-VBELN=DN 交貨單編號
POSNRVBAP-POSNR=訂單項次LIPS-POSNR=DN 項次
串接關係LIPS-VGBEL = VBAP-VBELN(來源訂單號)
LIPS-VGPOS = VBAP-POSNR(來源訂單項次)
SD 的文件流是 VBFA(Document Flow)在串:報價 → 訂單 → 交貨 → 發票。要追一張訂單出到哪,查 VBFA 的 VBELV(前導單)/VBELN(後續單)/VBTYP_N(單據類別)比自己 join 各表安全得多。

13. RANGES/SELECT-OPTIONS:區間與列舉

講義 08-8 標註「有點難,可半年後再學」,但它是 ABAP 選擇條件的底層原理,早懂早受益。

REPORT zrange.

" 現代寫法:用 RANGE OF 型態(不要再用 RANGES: 敘述,它會產生帶表頭列的舊物件)
DATA lt_carrid TYPE RANGE OF spfli-carrid.

" ① 區間(BT):low 到 high 之間
lt_carrid = VALUE #( ( sign = 'I' option = 'BT' low = 'AA' high = 'HH' ) ).

" ② 列舉(EQ):逐一列出,最精準
lt_carrid = VALUE #( sign = 'I' option = 'EQ'
                     ( low = 'AA' )
                     ( low = 'LH' )
                     ( low = 'UA' ) ).

SELECT carrid, connid, cityfrom, cityto
  FROM spfli
  WHERE carrid IN @lt_carrid          " IN 一律搭配 @
  ORDER BY carrid, connid
  INTO TABLE @DATA(lt_spfli).

LOOP AT lt_spfli ASSIGNING FIELD-SYMBOL(<ls>).
  WRITE: / <ls>-carrid, <ls>-connid, <ls>-cityfrom, <ls>-cityto.
ENDLOOP.

RANGE 表固定有四個欄位:

欄位型態意義
SIGNCHAR 1I=Include 包含(一般用這個);E=Exclude 排除
OPTIONCHAR 2EQ 等於、NE 不等、GT 大於、GE 大於等於、LT 小於、LE 小於等於、BT 區間、NB 非區間、CP 樣式包含(可用 * )、NP 樣式排除
LOW同來源欄位低值(單值時只用這個)
HIGH同來源欄位高值,只有 OPTION 為 BT/NB 時才參與運算
老師實戰血淚:條件用「區間」與用「列舉」答案會不一樣,用列舉的才正確。
原因有三:
1. BT 是字串比較,不是數值比較。CHAR 型欄位 '003' BT '007' 看似 003~007,但如果值域裡出現 '0035''01A' 這種長度或格式不一致的資料,會意外被包含或排除。
2. 值域不連續時,區間會把中間你不想要的代碼一併撈進來(例如成本要素區間中夾雜了不該加總的科目)。
3. 帶前導零的 NUMC/CHAR 欄位若未經 ALPHA 轉換,畫面輸入的 100 與資料庫的 0000000100 比較結果會出乎意料。
結論:涉及金額彙總、成本歸集這類「不能多也不能少」的邏輯,寧可用 EQ 逐一列舉,也不要圖方便用 BT 區間。
定義 Range 的兩種方式:
1. DATA lt_x TYPE RANGE OF <型態>.(型態可以是基本型態或表欄位)—— 建議用這種
2. RANGES: g_x FOR spfli-carrid.(此處的參考只能是表欄位,不能是 Data Element;且會產生帶表頭列的舊式物件)—— 已 obsolete。
另外 SELECT-OPTIONS s_matnr FOR mara-matnr. 本身就是一張 RANGE 表,加上選擇畫面,這是實務上最常用的做法。
" 實務標準骨架:SELECT-OPTIONS 直接餵進 WHERE ... IN
TABLES mara.   " 注意:SELECT-OPTIONS 的 FOR 需要一個宣告過的欄位;
               " 現代寫法可改為先 TYPES 再用 RANGE,或維持此處最小範圍的宣告

SELECT-OPTIONS: s_matnr FOR mara-matnr,
                s_mtart FOR mara-mtart.
PARAMETERS:     p_max   TYPE i DEFAULT 100.

START-OF-SELECTION.
  SELECT matnr, mtart, meins
    FROM mara
    WHERE matnr IN @s_matnr
      AND mtart IN @s_mtart
    ORDER BY matnr
    INTO TABLE @DATA(lt_out)
    UP TO @p_max ROWS.
若要完全避開 TABLES:,可以先在程式中宣告一個結構型態的變數,再對它 SELECT-OPTIONS ... FOR
DATA gs_mara TYPE mara.SELECT-OPTIONS s_matnr FOR gs_mara-matnr.
這樣既有選擇畫面,也不會產生 TABLES 的全域工作區。

14. Package、SE21、SE80 與程式類型

Package(舊稱 Development Class 開發類)是 ABAP 所有開發物件的歸屬單位。講義的定義:

  • 儲存 SAP 系統開發過程中的所有相關物件(程式、表結構定義、資料類型等),方便管理與查詢。
  • 透過 Package 可以方便地把它所包含的物件批量傳輸到不同伺服器。傳輸是透過請求號(Request)進行,請求號是一份文件,記錄所有物件的修改記錄。
  • 不屬於任何開發類的物件,可分配給本地開發類 $TMP;該類中的物件不能進行系統間傳輸,主要用於測試。

建立步驟(SE21 或 SE80)(依講義步驟整理)

  1. 執行 SE21(Package Builder),或 SE80 → 下拉選 Package
  2. 輸入 Package 名稱(必須 Z/Y 開頭,例如講義的 Z600_2409_ERP000),按 Create
  3. 填入 Short Description、Software Component(客戶物件選 HOME)、Transport Layer(選系統的標準傳輸層,例如 ZDEVSAP;此欄決定物件會被傳到哪條傳輸路徑)。
  4. Package Type 一般選 Not a Main Package(不是主套件)。
  5. 存檔 → 系統要求指定 Workbench Request → 建立或選一個既有的請求號 → 完成。
Transport Layer 若選錯(例如選成 SAP 或留空),物件可能無法沿著客戶的 DEV→QAS→PRD 路徑傳輸,或被歸為「不可傳輸」。建 Package 前先問 Basis 這套系統的客戶傳輸層代碼。

SE80 物件瀏覽器:同一個 Package 底下可組織多個物件——程式、Function Group、Class、Table、Screen、Transaction、Message Class 全都掛在樹上。開發時把 SE80 開著,比反覆切 SE38/SE11/SE37 快得多。

程式類型(Program Type,可由 TRDIR 資料表查詢)

Type名稱說明
1Executable Program 可執行程式可由 ABAP Editor 直接執行;processing blocks 依預先定義的順序執行;可使用標準 selection screen;主要用來顯示 List。報表都是這一型
MModule Pool 模組池必須至少定義一個 TCODE 指定 initial screen;畫面順序可由 screen attributes 靜態指定,或由程式碼動態指定。VA01/VA02/VA03 三個 TCODE 共用同一個 Module Pool 與畫面
FFunction Group 函數組包含 function modules、local data declarations 與 screens。
IInclude Program可包含任何 ABAP 敘述;不能單獨執行,是模組化的容器。
JInterface Pool 介面池包含 global interfaces 與 local data declarations。
KClass Pool 類別池包含 global classes 與 local data declarations。
SSubroutine Pool只放 FORM 的容器(已不建議新開發使用)。
Type I/F/J/K 這些程式不能直接執行,它們是 modularization units 的容器,被其他可執行程式呼叫。只要呼叫了其中任一 module,系統就會把整個主程式載入當前的 internal session。
查詢方式:SE16N → TRDIR → NAME = 'Z*' ,看 SUBC 欄位。
" 用程式查自己 Package 底下有哪些物件(TADIR = 開發物件目錄)
PARAMETERS p_devc TYPE tadir-devclass DEFAULT 'Z600_2409_ERP000'.

SELECT pgmid, object, obj_name, author, srcsystem
  FROM tadir
  WHERE devclass = @p_devc
  ORDER BY object, obj_name
  INTO TABLE @DATA(lt_tadir).

LOOP AT lt_tadir ASSIGNING FIELD-SYMBOL(<ls>).
  WRITE: / <ls>-pgmid, <ls>-object, <ls>-obj_name, <ls>-author.
ENDLOOP.

" 查程式類型(TRDIR-SUBC:1=可執行 M=Module Pool F=FuncGroup I=Include K=Class)
SELECT name, subc, cnam, unam, udat
  FROM trdir
  WHERE name LIKE 'Z%'
  ORDER BY name
  INTO TABLE @DATA(lt_trdir)
  UP TO 100 ROWS.
作業 7-01:核對自己的 SE21,確認是否有跟上進度。此檔視為點名檔及作業繳交記錄。

15. 傳輸請求 SE09/SE10 與 STMS 傳輸路徑

ABAP 的每一次修改都會被記錄在傳輸請求(Transport Request,TR)裡。這是 SAP 變更管理的核心。

兩種請求

  • Workbench Request(工作台請求):跨 Client 的開發物件——程式、Table、Class、Function、Screen。編號形式 DEVK900123
  • Customizing Request(客製化請求):Client 相關的設定資料(SPRO 的配置)。編號形式 DEVK900124

請求的結構:一個 Request(主請求)底下掛數個 Task(任務),每位開發者一個 Task,物件實際登記在 Task 上。

TCODE用途
SE09Transport Organizer(Workbench 請求,講義用這支查請求號)
SE10Transport Organizer(含 Customizing 請求,功能與 SE09 幾乎相同)
SE01Transport Organizer(擴充視圖,可看傳輸日誌、建立各類請求)
SE03Transport Organizer Tools(改物件的 Package、搜尋請求中的物件、鎖定/解鎖)
STMSTransport Management System,匯入佇列、執行傳輸

請求狀態(講義原文):Modifiable=可修改(還在開發中,物件被鎖住);Released=已釋放(已寫出檔案,可以被匯入下一個系統,內容不能再改)。

釋放與傳輸的完整流程(依講義步驟整理)

  1. 開發完成 → SE09 找到自己的 Request。
  2. 釋放 Task(子項):選中 Task → 按卡車圖示(Release Directly)。
  3. 釋放 Request(主項):同樣按卡車圖示。此時系統會做語法/一致性檢查,並把物件寫成傳輸檔案(cofiles + data files)。
  4. STMS → Import Overview → 進入 QAS 的 Import Queue → 找到剛才的請求號 → Import Request。
  5. 在 QAS 測試通過後,再從 STMS 的 PRD 佇列匯入。

STMS 的觀念(講義 10-5):標準三層架構是 DEV → QAS → PRD,「有時不只這三區」。常見的變化包括:

系統全名角色
DEVDevelopment唯一可以改程式的地方(其他系統通常鎖住)
QAS / QA / TSTQuality Assurance使用者測試 UAT、整合測試
PRD / PROProduction正式生產環境
SBXSandbox沙箱,通常不在傳輸路徑上
TRNTraining教育訓練機
PRE / PPDPre-Production上線前最後演練(含真實資料副本)
傳輸的三大鐵律
1. 傳輸順序不能亂。請求號是依釋放時間編號的,匯入時若跳號(先匯 900130 再匯 900125),舊的請求會覆蓋新的,造成「明明修好了卻又壞掉」。務必依序匯入或用 Import All。
2. 相依物件要同一批傳。Table 改了欄位、程式用了新欄位,兩者若分在不同 Request 且分批傳,QAS 會出現「欄位不存在」的短暫錯誤。
3. $TMP 的東西傳不出去。上線前用 SE03 → Change Object Directory Entry 把 Package 從 $TMP 改成正式 Package,系統會要求指定 Request。
查「這支程式是被哪個請求號傳出去的」:SE03 → Search for Objects in Requests/Tasks,輸入物件名稱即可。或直接查資料表 E070(請求表頭)、E071(請求內物件)、E07T(請求說明)。
" 查某個物件被登記在哪些傳輸請求裡(E071 = 傳輸請求物件明細)
PARAMETERS p_obj TYPE e071-obj_name DEFAULT 'Z2202_00_05'.

SELECT e071~trkorr, e071~pgmid, e071~object, e071~obj_name,
       e070~trstatus, e070~as4user, e070~as4date, e07t~as4text
  FROM e071
  INNER JOIN e070 ON e070~trkorr = e071~trkorr
  LEFT OUTER JOIN e07t ON  e07t~trkorr = e071~trkorr
                       AND e07t~langu  = @sy-langu
  WHERE e071~obj_name = @p_obj
  ORDER BY e070~as4date DESCENDING
  INTO TABLE @DATA(lt_tr).

LOOP AT lt_tr ASSIGNING FIELD-SYMBOL(<ls>).
  " TRSTATUS: D=Modifiable L=Modifiable(protected) R=Released O=Release started
  WRITE: / <ls>-trkorr, <ls>-trstatus, <ls>-as4user, <ls>-as4date, <ls>-as4text.
ENDLOOP.

16. 自建資料表練習(SE11/SE14/SM30)

講義第 9 節要每位學員建立四張自己的表,作為整個課程的練習資料庫。命名規則:Z_學號_表名(學號用二位數字),例如 Z2202_00_KNA1

表名欄位型態/長度
Z2202_00_KNA1(客戶)MANDT 用戶端CLNT 3
KUNNR 客戶編號CHAR 10
NAME 客戶名稱CHAR 40
Z2202_00_MARA(物料)MANDT 用戶端CLNT 3
MATNR 物料編號CHAR 40
NAME 物料名稱CHAR 40
Z2202_00_VBAK(訂單主檔)MANDT 用戶端CLNT 3
ZSDNO 銷售編號CHAR 10
ZCC 公司代碼CHAR 4
ZDT 銷售日期DATS 8
KUNNR 客戶編號CHAR 10
ZAMT 銷售金額CURR 15,2
ZCUR 交易貨幣CUKY 5
Z2202_00_VBAP(訂單明細)MANDT 用戶端CLNT 3
ZSDNO 銷售編號CHAR 10
MATNR 物料編號CHAR 40
ZQTY 銷售數量NUMC 8
ZPRI 物料單價CURR 15,2
ZAMT 銷售金額CURR 15,2
ZCUR 交易貨幣CUKY 5

建表的關鍵設定(老師特別強調的坑)

  • Delivery and Maintenance 頁籤,Data Browser/Table View Maint. 必須選 「X — Display/Maintenance Allowed」,否則 SE16N/SM30 都進不去。
  • 技術設定(Technical Settings)必修:Data Class(一般主檔選 APPL0,交易資料選 APPL1)、Size Category。沒設定就無法激活。
  • 表必須先激活,才能執行 Utilities → Table Maintenance Generator(產生維護畫面)。
  • Table Maintenance Generator 需要指定一個 Function Group——所以要先在 SE80/SE37 把自己的 FG 建好。
  • Recording Routine 要勾「no, or user, recording routine」,這樣在 SM30 編輯資料時才不會每次都要求輸入 Change Request 號碼。
  • 一旦產生了維護畫面(Maintenance Screen Number),SE16N 就變成只能瀏覽不能維護,資料只能在 SM30 維護
  • Primary Key 一旦建立很難修改,設計時務必想清楚。事後要調整可研究 SE14(Database Utility,可做 Activate and Adjust Database,注意會有資料轉換風險)。
DDIC 共有 37 種預設型態,常用的:
CLNT Client、CHAR 文字、CUKY 幣別鍵、CURR 金額(必須指定參考幣別欄位)、DATS 日期 YYYYMMDD、TIMS 時間 HHMMSS、INT2 整數 ±32767、INT4 整數、NUMC 數字型文字、QUAN 數量(必須指定參考單位欄位)、UNIT 單位鍵、DEC 十進位、LANG 語言、RAW/LRAW 二進位。
CURR 一定要配 CUKY,QUAN 一定要配 UNIT,這是 DDIC 的強制規則(在 Currency/Quantity Fields 頁籤設定),否則無法激活。
建第二張表時務必練習完整鏈路:建 Data Element → 建 Domain → 設 Value Table → 建 Foreign Key。這條鏈走過一次,就會理解為什麼 VBAK-MATNR 會自動有 F4 與輸入檢查。全部做完後回頭看 SE11 裡的標準表 MARA,就能理解 STRUCTURE(結構)與 Append Structure 的意義。
作業 9-01 / 10-01:總共要建立客戶、料號、訂單主檔、訂單明細四張表,並用 SE16N/SM30 建入測試資料。

17. 編輯器熱鍵、Pretty Printer、SE93/SA38 與其他工具

ABAP Editor 熱鍵(講義 08-1;若失效通常是被中文輸入法攔截)

功能熱鍵功能熱鍵
調整字體大小Ctrl + 滑鼠滾輪矩陣(區塊)點選Ctrl + ↑↓←→
加入註解Ctrl + ,(逗號)取消註解Ctrl + .(句號)
刪除一行Ctrl + Shift + X剪下(需反白)Ctrl + X
複製Ctrl + C貼上Ctrl + V
轉小寫Ctrl + L轉大寫Ctrl + U
大小寫互轉Ctrl + K原地複製一行Ctrl + D
取消Ctrl + Z重做Ctrl + Y
尋找Ctrl + F取代Ctrl + H
Pretty PrinterShift + F1語法檢查Ctrl + F2
激活Ctrl + F3執行F8
Where-Used ListCtrl + Shift + F3設中斷點Ctrl + Shift + F12
雙擊的威力:在編輯器裡對任何變數、型態、Function、Table 名稱雙擊,會直接跳到它的定義處(Forward Navigation)。老師特別提到「對變數雙擊,會往上跳到其原始出處」——這是理解別人程式最快的方法。跳過去之後按 F3 或綠色返回鍵回來。

其他必知工具

  • Pretty Printer(Shift+F1):自動縮排、統一大小寫。在 SE38 右下角的 Options 圖示裡可設定風格(關鍵字大寫/小寫、縮排寬度)——老師建議勿動預設值,團隊風格一致比較重要。
  • SA38:直接輸入程式名稱執行,不進編輯器。
  • SE93:把程式編譯成 TCODE。老師提醒:「其實很多時候,在學模組運用時,PROGRAM 就可以了」——不必急著建 TCODE。
    建立步驟:SE93 → 輸入 Z 開頭的 TCODE 名稱 → Create → 選 Program and selection screen (report transaction) → 填程式名稱 → 存檔並指定 Package/Request。
  • SU53:權限不足時的第一招。跳出 "No authorization" 之後立刻執行 SU53,會顯示最後一次失敗的權限檢查物件與欄位值,截圖給 Basis 就能開權限。
  • SCI(Code Inspector):靜態程式碼檢查,可檢查命名規則、效能問題、安全性問題。搭配 ATC(ABAP Test Cockpit)是 S/4HANA 的標準做法。
  • ABAPDOCU:SAP 內建的 ABAP 語法文件與範例程式庫(對應的範例程式是 DEMO* 開頭)。忘記語法時比 Google 快。
  • 個人清單(Favorites):SAP Easy Access 左側可把常用 TCODE 與程式加進「我的最愛」,還可以放網址與檔案捷徑。
  • 搜尋的萬用字元:SAP 各處搜尋一律用 *(多字元)與 +(單一字元);ABAP SQL 的 LIKE 則是 %_。兩套不要搞混。

TIME ZONE 的觀念(講義 10-10)

系統欄位意義
SY-DATUM系統(Application Server)日期
SY-UZEIT系統時間
SY-DATLO使用者本地日期(依 SU3 設定的時區換算)
SY-TIMLO使用者本地時間
SY-ZONLO使用者時區
跨國系統一定要分清楚 SY-DATUM 與 SY-DATLO。台灣使用者連到設在美國的 AP Server,SY-DATUM 可能還是昨天。使用者的時區在 SU3 → Defaults 頁籤設定。存進資料庫的時間戳記建議用 TIMESTAMP/TIMESTAMPL(UTC),顯示時再用 CONVERT TIME STAMP ... TIME ZONE 換算。
" 時區換算的現代寫法
DATA lv_ts TYPE timestampl.
GET TIME STAMP FIELD lv_ts.                  " 取得 UTC 時間戳記

CONVERT TIME STAMP lv_ts TIME ZONE sy-zonlo
        INTO DATE DATA(lv_date) TIME DATA(lv_time).

WRITE: / |UTC 時戳:{ lv_ts }|,
       / |使用者時區 { sy-zonlo }:{ lv_date DATE = USER } { lv_time TIME = USER }|,
       / |系統日期 SY-DATUM:{ sy-datum DATE = USER }|,
       / |本地日期 SY-DATLO:{ sy-datlo DATE = USER }|.

18. 除錯入門(Debug)

講義以截圖為主,這裡整理成可操作的文字步驟(依講義步驟整理)

進入 Debugger 的四種方法

  1. 設中斷點:在 SE38 編輯器中,游標停在該行 → 點左側「停止」圖示,或按 Ctrl+Shift+F12。執行到該行就會停。
  2. 指令列輸入 /h:在任何畫面的指令列輸入 /h 按 Enter,畫面顯示 "Debugging switched on",接著按下一個動作就會進入偵錯。這招對標準程式最好用
  3. 程式中寫死:加一行 BREAK-POINT.(所有人都會停,絕不能傳到正式機)或 BREAK <使用者名稱>.(只有指定使用者會停)。
  4. SE80/SAT:對背景 Job 用 SM37 → 選 Job → 指令列 JDBG 進入偵錯。

Debugger 的操作鍵

動作用途
F5Single Step(單步進入)會鑽進 FORM/FUNCTION/METHOD 內部
F6Execute(單步跳過)把整個子程式當一行執行完
F7Return(跳出)執行完目前子程式後回到呼叫端
F8Continue(繼續)跑到下一個中斷點或結束

常用技巧

  • Watchpoint(監看點):不知道變數在哪裡被改壞時,設 Watchpoint(新 Debugger 的 Break./Watchpoints 頁籤),變數值一變就停。這是抓「金額為什麼變成 0」這類問題的利器。
  • 看內表:在 Variables 區輸入內表名稱,雙擊即可展開所有列;可用上方的篩選功能找特定列。
  • 改值:在 Debugger 裡雙擊變數值 → 修改 → 按鉛筆/Change 圖示,可以即時改變執行結果,用來模擬各種情境(正式機請小心,這會真的影響過帳結果)。
  • Call Stack:看目前的呼叫堆疊,快速找出「是誰呼叫我的」。
  • SY-SUBRC 是第一嫌疑犯:每個 SELECT、READ TABLE、CALL FUNCTION 之後都先看 SY-SUBRC,多數 bug 在這裡就抓到了。
" 除錯友善的寫法:把關鍵判斷點寫成明確的訊息,而不是靜默失敗
SELECT SINGLE matnr, mtart, meins
  FROM mara
  WHERE matnr = @p_matnr
  INTO @DATA(ls_mara).

IF sy-subrc <> 0.
  " 不要只 RETURN,留下可追查的訊息
  MESSAGE |MARA 查無料號 { p_matnr ALPHA = OUT }(程式 { sy-repid } 第一段查詢)|
          TYPE 'E'.
ENDIF.

" 開發階段的條件式中斷點:只對自己停,不影響同事
IF sy-uname = 'CASPER'.
  BREAK-POINT.       " 上線前務必移除
ENDIF.
上線檢查清單:BREAK-POINT、寫死的使用者名稱、&SAP_EDIT 相關程式碼、暫時註解掉的授權檢查、寫死的 Client/公司代碼、寫死的日期——這六樣是 Code Review 一定要掃的東西。SCI/ATC 可以自動幫你抓 BREAK-POINT。

19. 解決企業問題的手段與順序

講義最後一節只有三句話,但它是整套課程的價值觀,也是資深顧問與新手最大的差別:

  1. 能配置的就用後台配置(SPRO)解決。
  2. 一定要改程式就用增強(Enhancement/BAdI/User Exit)。
  3. 非不得已才寫程式。
為什麼?因為維護成本。配置在升級時原封不動;增強有標準的接點,升級時 SAP 會提示;自建程式在每次升級(例如 ECC → S/4HANA,或 S/4 年度版本升級)都要重新測試、可能要重寫。
延伸順序(S/4HANA 版):標準功能 → 配置 → Fiori 標準 App → CDS View / Custom Field(Key User Extensibility) → BAdI / Enhancement → 自建 ABAP 程式 → Side-by-side(BTP)Clean Core 是 S/4HANA 的核心原則:核心越乾淨,升級越輕鬆。
S/4HANA 的「Clean Core」還有一個現實理由:SAP 已宣告修改核心的客製會影響雲端遷移路徑。若公司未來打算上 RISE with SAP 或 Public Cloud,所有非標準的核心修改都會變成阻礙。開發前先問:「這個需求標準有沒有?Fiori 有沒有?CDS 能不能做?」

練習與自我檢核

  1. 作業 1-01:找出 FI、CO、SD、MM、PP 五大模組的重大雙檔。
    要點:BKPF/BSEG(S/4→ACDOCA)、COBK/COEP、VBAK/VBAP、EKKO/EKPO、MKPF/MSEG(S/4→MATDOC)、AUFK+AFKO/AFPO。
  2. 作業 2-01:用 SE16H 把上題找到的雙檔做成關聯,並試著加上 Group By 與 Sum。
  3. 作業 3-01:把 Z2202_00_05 的多餘程式碼刪掉,並讓選擇螢幕的料號能下拉選單。
    要點:刪 TABLES: VBAP.、未使用的 TYPES 與 DATA;PARAMETERS p_matnr TYPE mara-matnr
  4. 作業 4-01:說明 MARA、MARC、MARD、MAKT 的關聯性。
    要點:MATNR → +WERKS → +LGORT 的三層結構,MAKT 是 MATNR+SPRAS 的多語系描述。
  5. 作業 5-01:列出你認為最重要的 MATDOC 二十個欄位,並照第 9 節寫成 JOIN 報表程式。
  6. 作業 7-01:檢查自己的 SE21,確認 Package 已建立、程式已歸屬、Request 已產生。
  7. 作業 8-01DESCRIBE TABLE 能算出內表筆數,那「通透表(Transparent Table,即資料庫表)」如何計算筆數?
    答案:用 SELECT COUNT(*) FROM mara INTO @DATA(lv_cnt).SELECT COUNT( * ) AS cnt ... INTO ...。切勿把整張表撈進內表再 lines( ),那會把 DB 拖垮。另外 DESCRIBE TABLE it LINES lv_n 現代等價寫法是 lv_n = lines( it )DESCRIBE TABLE it KIND lv_k 可取回內表種類(S 標準/O 排序/H 雜湊),OCCURS 可取回初始行大小。
  8. 作業 8-02:把第 11 節的 ZDEMO 改寫成「由畫面上決定語系」。
    要點:加 PARAMETERS p_spras TYPE sy-langu DEFAULT sy-langu.,WHERE 改成 spras = @p_spras。進階:用 SELECT-OPTIONS 支援多語系並在 ALV 中分欄顯示。
  9. 作業 9-01 / 10-01:建立客戶、料號、訂單主檔、訂單明細四張自建表,設定技術設定、產生 Table Maintenance、用 SM30 建資料。
  10. 自我檢核:不看講義,能否在 15 分鐘內完成——建 Package(SE21)→ 建程式(SE38)→ 寫出「選擇畫面 + SELECT 進內表 + ALV 輸出」→ 激活 → 執行 → 在 SE09 找到自己的 Request?做得到,這一章就過關了。
  11. 觀念題:為什麼在 S/4HANA 上 SELECT * 特別傷?
    要點:HANA 是欄式(Column Store)儲存,只讀需要的欄位成本極低;SELECT * 等於強迫掃描並重組所有欄位(ACDOCA 有 500 欄),且無法使用只掃描部分欄位的最佳化。
  12. 觀念題:為什麼老師說「條件用區間跟用列舉,答案會不同,用列舉的才正確」?
    要點:BT 是字串比較、值域可能不連續、前導零/ALPHA 轉換差異。金額彙總類邏輯務必用 EQ 逐一列舉。

第 1-2 章 ABAP 資料字典:資料表 / 視圖 / 結構 / 域 / 資料元素 / 類型組 / 搜尋說明 / 鎖物件

這一章是整個 ABAP 開發的地基。資料字典(ABAP Dictionary,DDIC)是 SAP 全系統唯一的「資料型別中央登記處」:資料表長什麼樣、欄位的合法值有哪些、兩張表怎麼關聯、F4 按下去跳出什麼、多人同時改一筆資料誰先誰後——全部在 SE11 裡決定,而不是在程式裡硬寫。學完這一章,你能自己設計出一組互相關聯的 Z 資料表(客戶/物料/訂單主檔/訂單明細),為它們建 Domain、Data Element、外來鍵、Text Table、四種 View、Search Help 與 Lock Object,並用 SE54 產生維護畫面、掛上 SM30 與自訂 TCODE,最後知道在 S/4HANA 時代哪些做法該改成 CDS View。

核心 TCODESE11SE14SE16SE16NSE16HSE54SM30SM31SM34SE80SE37SE93SE84SM12SE38ME23NVA03
關鍵物件MARA、MAKT、KNA1、VBAK、VBAP、T001W、T001L、TCURC、TCURT、T002、DD02L、DD03L、DD04T、DD07L/DD07T、DD08L、SYST、SLIS、F4IF_INT_TABLE_VALUE_REQUEST、F4IF_SHLP_EXIT_EXAMPLE、CL_SALV_TABLE

1. 資料字典是什麼:六種基本物件

ABAP 資料字典是定義與管理「資料元素」與「資料庫元素」的中心工具。系統所有的全域資料型別、資料表結構都要在字典裡建立與維護;字典保證了資料的完整性、一致性與安全性。老師特別交代:這張表要會默寫,才會記得 DD 的威力有多強大

物件說明要點
Table 資料表資料庫中真正存放資料的地方;字典裡定義的是「表的結構」,由列(記錄)與欄(欄位)組成表的欄位一律透過 Data Element 定義
View 視圖一張或多張資料表的邏輯呈現,本身不存資料共四種:Database/Projection/Maintenance/Help
Data Type 資料類型值的集合+定義在該值集上的一組操作三種:Data Element/Structure/Table Type
Domain 域指定欄位的技術屬性(型別、長度、小數位)與允許的值不能直接拿 Domain 定義表的欄位,一定要透過 Data Element
Search Help 搜尋說明F4 說明,提升使用者友善度可掛在 Table/Structure/Data Element/Screen/Parameters
Lock Object 鎖物件對資料存取做並行控制;鎖資訊放在記憶體的 Lock Table名稱一律以 E 開頭,建立後自動產生 ENQUEUE_/DEQUEUE_ 兩支函數
口訣:Domain 管「長什麼樣、能填什麼」,Data Element 管「叫什麼名字、F4 給什麼、說明文件怎麼寫」,Table 只是把一堆 Data Element 排起來。這是 SAP 與其他系統最大的差別——別的系統直接在欄位上寫 CHAR(10),SAP 是 Table → Data Element → Domain 三層。

四種 View 的差異,是本章最常被考的觀念:

View 類型資料來源連接條件能否維護資料
Database View一張或多張表自己定義;一律是 INNER JOIN單表視圖可維護(改視圖=改表);多表視圖只能顯示
Projection View只能一張表用來「遮罩欄位」保護資料,可維護
Maintenance View多張表不能自訂,必須來自既有外來鍵可維護;需用「表格維護產生器」產生對話框;所有主鍵都要放進視圖欄位
Help View多張表必須存在外來鍵(LEFT OUTER JOIN 語意)不可維護;專供 Search Help 的 Selection Method 使用
S/4HANA 提醒:新開發不要再建 Database View。DDIC 的 Database View 只是把 SQL 固化在字典裡,沒有註解(annotation)、不能做參數、不能被 Fiori/OData 直接消費。請改用 ABAP CDS View(見本章第 11 節)。Maintenance View + SE54 這條路則仍然有效,客製設定表(Z 開頭的 Customizing 表)到今天還是這樣做。

2. 用 SE11 建第一張 Z 資料表(含 Delivery Class 與 Maintenance)

講義的目標很明確:本節做完,要能用 SE16N 維護自己的資料。而且提醒「實作之前,請先學會建立 function group」,因為後面 SE54 產生維護畫面時一定會用到。

SE11 建表的標準步驟:(依講義步驟整理)

  1. SE11 → 選 Database table → 輸入 ZERP300_KNA1 → Create。
  2. Delivery and Maintenance 頁籤:
    • Delivery ClassA(Application table,主資料與交易資料)。老師強調:這個畫面一定要選 A,其他的不要選;C 雖然是客製,但那是 SPRO 的客製。
    • Data Browser/Table View Maint.Display/Maintenance Allowed,否則 SE16N / SM30 進不去、也產不出維護畫面。
  3. Fields 頁籤:第一個欄位一律是 MANDT(Data Element MANDT,Domain MANDT,CLNT 3),並勾 Key,代表這是 client-dependent 的表。
  4. 逐欄輸入欄位名與 Data Element,勾選主鍵(Key)與 Initial Values。
  5. 幣別/數量欄位要設參考欄位:CURR 型別要指定 Reference table + Reference field(幣別欄位 CUKY),QUAN 型別要指定單位欄位(UNIT)。這是講義中「處理數量 QUAN 與金額 CURR 欄位」的重點。
  6. Extras → Enhancement Category 設為「Can Be Enhanced (Deep)」,否則激活時會一直跳警告。
  7. Technical Settings(技術設定)填 Data Class / Size Category(見第 3 節)。
  8. 存檔 → 指定 Package 與 Request → Activate。老師提醒:要 ACTIVE 才可被 SE16N 使用。

講義示範的客戶資料表 ZERP300_KNA1

欄位Data Element型別參考說明/對應標準欄位
MANDTMANDTCLNT 3用戶端(Key)
KUNNRKUNAGCHAR 10客戶編碼(Key);對應 VBAK-KUNNR
NAME1NAME1_GPCHAR客戶名稱;對應 KNA1-NAME1
ZWERTDZWERTCURRVBAP-WAERK金額(需指定幣別參考欄位)
ZMENGDZMENGQUANVBAP-ZIEME數量(需指定單位參考欄位)
ZIEMEDZIEMEUNIT單位

建好、激活好之後,先用最單純的方式把資料撈出來驗證。以下是 S/4HANA 的現代寫法:

REPORT zerp300_kna1_list.

" 選擇畫面:以客戶編號區間查詢自建客戶表
SELECT-OPTIONS: s_kunnr FOR zerp300_kna1-kunnr.

START-OF-SELECTION.

  " 一律明列欄位;主機變數用逸出字元 @
  SELECT kunnr,
         name1,
         zwert,
         zmeng,
         zieme
    FROM zerp300_kna1
    WHERE kunnr IN @s_kunnr
    ORDER BY kunnr             " SELECT 沒有 ORDER BY 就沒有排序保證
    INTO TABLE @DATA(lt_kna1).

  IF lt_kna1 IS INITIAL.
    MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  TRY.
      cl_salv_table=>factory(
        IMPORTING r_salv_table = DATA(lo_alv)
        CHANGING  t_table      = lt_kna1 ).

      lo_alv->get_functions( )->set_all( abap_true ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->display( ).

    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.
SE16N 是「簡易維護資料」的入口,能在裡面新增出資料才算建表成功。S/4HANA 上另有 SE16H(HANA 版資料瀏覽器,支援彙總、Group By、Outer Join),查資料時比 SE16N 更好用。SE16N 的 &SAP_EDIT 直接改資料模式在正式系統通常被關閉,且會寫入變更紀錄,不要當成日常工具。
舊系統可能遇到 Cluster table / Pooled table(為了加密或節省空間而存在,例如 BSEG、KONV 的舊型態)。這是歷史產物,S/4HANA 幾乎都已轉成 Transparent table(BSEG 保留但財務讀 ACDOCA、KONV 改 PRCD_ELEMENTS)。遇到再查即可,新開發一律建 Transparent table。

3. 技術設定:Data Class、Size Category、Buffering、Logging

SE11 裡按下「Technical Settings」是很多人跳過、卻在上線後出事的地方。

設定選項怎麼選
Data Class(資料類別)APPL0主資料(很少變動):如 ZERP300_KNA1、ZERP300_MARA
Data ClassAPPL1交易資料(頻繁新增變動):如 ZERP300_VBAK、ZERP300_VBAP
Data ClassAPPL2組織/客製設定資料(幾乎不變)
Data ClassUSER / USER1客戶自訂的資料類別(由 BASIS 建立表空間)
Size Category(大小類別)0 ~ 4預估資料筆數等級,0 最小。填錯不會壞,只影響初始空間配置;HANA 上影響已很小
Buffering(緩衝)Not buffered / Single records / Generic(指定前 n 個 Key)/ Fully buffered「幾乎不變、資料量小」的設定表才開 Fully buffered;交易資料一律 Not buffered
Log data changes(記錄變更)勾/不勾老師警告:千萬不要勾,有可能因為 log 讓系統效能變差
Buffering 的經典地雷:表被緩衝之後,你用 ABAP SQL 讀到的可能是舊資料(緩衝未同步,最長可達分鐘級)。要強制讀資料庫請加 BYPASSING BUFFER;反之,若程式會頻繁 UPDATE 這張表,就絕對不要開緩衝。另外,凡是自己在程式中 UPDATE/DELETE 後又立刻 SELECT 的邏輯,都要留意這件事。
" 需要保證讀到資料庫最新值時(例如剛做完更新、或表有開 buffering)
SELECT SINGLE kunnr, name1
  FROM zerp300_kna1
  WHERE kunnr = @lv_kunnr
  BYPASSING BUFFER
  INTO @DATA(ls_kna1).

IF sy-subrc <> 0.
  MESSAGE |客戶 { lv_kunnr ALPHA = OUT } 不存在| TYPE 'E'.
ENDIF.

SE14(Database Utility):當表結構改過(加欄位、改長度、改主鍵)而激活失敗,或狀態顯示「不一致」時的救援工具。

  • SE14 → 輸入表名 → Edit → Activate and adjust database:把字典定義同步到實體資料庫(含資料轉換)。
  • Delete data → 清空資料重建(會刪資料,正式機不要亂按)。
  • Check → 比對字典與資料庫是否一致。
  • 改主鍵、縮短欄位長度、改型別,都可能觸發完整的資料轉換(表會被鎖住),大表請排離峰執行。

4. Domain 與 Data Element:把「合法值」寫進字典

SE11 建 Domain 與 Data Element 是為了「一次定義、到處引用」。老師的話:建立資料表時,引用自定 Data Type 並綁定 Domain,可做好資料一致性的管理,這在大型系統上非常重要。

Domain(SE11 → Domain)重點頁籤

  • Definition:Data Type(CHAR/NUMC/DEC/CURR/QUAN/DATS/TIMS/UNIT/CUKY…)、No. Characters、Decimal Places。
  • Sign:數值型 Domain 才有;不打勾=只能輸入正數(講義特別提到這一點)。
  • Output Characteristics:Output Length、Conversion Routine(轉換常式)。例如 SPRAS 用 ISOLA 把內部 1 碼語言碼轉成 ISO 2 碼;物料號碼用 MATN1 做前置補零。轉換常式其實是一對 CONVERSION_EXIT_xxxxx_INPUT / _OUTPUT 函數。
  • Value Range:可填 Single Values(固定值)Intervals(區間),或指定 Value Table(值表)
Value Table 只是「候選」,它不會自動檢核。真正產生檢核的是欄位上的外來鍵;不過在 Data Element 引用有 Value Table 的 Domain 時,SE11 會主動建議你用該值表當 Check Table,按下去就自動產生外來鍵——這是老師說的「Domain 與 Data Element 能幫助業務顧問找問題」的實際機制。

Data Element(SE11 → Data type → Data element)重點

  • Elementary Type:綁 Domain(建議)或直接給 Predefined Type。
  • Field Label:Short/Medium/Long/Heading——這是畫面與 ALV 抬頭的文字來源,一定要填,否則報表抬頭是空白。
  • Further CharacteristicsSearch Help(掛 F4)、Parameter ID(SET/GET PARAMETER 記憶值)、Default Component Name。
  • Documentation:使用者在畫面上按 F1 看到的說明。

講義練習 03-3:建立一個幣別欄位的 Data Element,並限定只能填 TCURC 內的內容。作法是 Domain 型別 CUKY 長度 5、Value Table 指定 TCURC,Data Element 引用它,再在表欄位上建立外來鍵到 TCURC。

固定值(Fixed Values)是放在 DD07L/DD07T,可以直接讀出來當下拉清單:

" 讀取某個 Domain 的固定值清單(DD07L 值、DD07T 文字),用來自製下拉或做檢核
DATA(lv_domname) = CONV domname( 'ZDSTATUS' ).

SELECT l~domvalue_l AS value,
       t~ddtext     AS text
  FROM dd07l AS l
  INNER JOIN dd07t AS t
    ON  t~domname    = l~domname
    AND t~valpos     = l~valpos
    AND t~as4local   = l~as4local
    AND t~ddlanguage = @sy-langu
  WHERE l~domname  = @lv_domname
    AND l~as4local = 'A'                  " 只取 Active 版本
  ORDER BY l~valpos
  INTO TABLE @DATA(lt_fixval).

" 用 REDUCE 組出一段可讀的說明字串
DATA(lv_hint) = REDUCE string(
  INIT s = ``
  FOR ls IN lt_fixval
  NEXT s = COND #( WHEN s IS INITIAL
                   THEN |{ ls-value }={ ls-text }|
                   ELSE |{ s } / { ls-value }={ ls-text }| ) ).

cl_demo_output=>display( lv_hint ).
老師的練習流程:SE80 的 RIS(Repository Information System,或直接用 SE84)可以把 Data Element、Domain、Structure、Table 之間的關聯性串起來——想知道「這個 Domain 被誰用了」,就用 Where-Used List

5. 外來鍵(Foreign Key)與 Text Table

定義:兩個表中有一個相同欄位,它在一個表中是主鍵,那麼這個公有欄位就是另一個表的外鍵。以另一個表的外鍵作關鍵字的表稱為主表(Check Table),具有此外鍵的表稱為從表(Foreign Key Table)。作用是保持資料一致性與完整性:外鍵只能引用主表中存在的值,或使用空值。

SE11 建外來鍵的步驟:(依講義步驟整理)

  1. Fields 頁籤,游標點在要建外鍵的欄位上(例如 Z2202_00_VBAK-KUNNR)。
  2. 按工具列的外來鍵圖示(鑰匙)
  3. 輸入 Check table(例如 Z2202_00_KNA1);系統會依 Domain 自動建議欄位對應(Check table field ↔ Foreign key field)。
  4. 設定 Cardinality(基數):主表 1 或 C(C=可為空),從表 1/C/N/CN。客戶 1 → 訂單 N 就是 1:CN
  5. 設定 Foreign key field type(外鍵欄位類型)
    • Non-key-fields/candidates:外鍵欄位不是主鍵
    • Key fields/candidates:是主鍵欄位或能唯一確定記錄
    • Key fields of a text table:這張表是主表的文本表
  6. 可勾 MsgClass/MsgNo 指定檢核失敗時的訊息。
外來鍵的檢核只在畫面輸入(Dynpro/SM30/Fiori)時自動生效,ABAP SQL 的 INSERT/UPDATE 不會檢核外鍵。所以你自己寫的上傳程式一定要自己檢核主表存在與否,不能靠字典。
" 上傳/過帳前自行檢核外來鍵:一次撈出所有主表 Key,再用 line_exists 判斷
SELECT kunnr
  FROM zerp300_kna1
  FOR ALL ENTRIES IN @lt_upload
  WHERE kunnr = @lt_upload-kunnr
  INTO TABLE @DATA(lt_valid_kunnr).

" FOR ALL ENTRIES 的空表陷阱:驅動表為空時會撈出「整張表」,務必先擋
IF lt_upload IS INITIAL.
  MESSAGE '無上傳資料' TYPE 'E'.
ENDIF.

" 轉成 HASHED 表,判斷才快
DATA lt_key TYPE HASHED TABLE OF kunnr WITH UNIQUE KEY table_line.
lt_key = VALUE #( FOR ls IN lt_valid_kunnr ( ls-kunnr ) ).

LOOP AT lt_upload ASSIGNING FIELD-SYMBOL(<ls_up>).
  IF NOT line_exists( lt_key[ table_line = <ls_up>-kunnr ] ).
    <ls_up>-msg = |客戶 { <ls_up>-kunnr } 不存在於主檔|.
  ENDIF.
ENDLOOP.

Text Table(文本表):一張表是另一張表的文本表,條件是——文本表的主鍵=主表主鍵 + 語言欄位 SPRAS,除了這些欄位以外沒有其他能確定唯一性的欄位;而且要在 SPRAS 欄位上建立外來鍵、外鍵類型選 Key fields of a text table,Check table 填 T002。設定成功後,主表在 SE11 的 Delivery and Maintenance 頁籤就會顯示對應的 Text Table。

經典組合:MARA(物料主檔)↔ MAKT(物料說明)、TCURC(幣別)↔ TCURT(幣別說明)、T001W(工廠)↔ T001W 本身無文本表但有 NAME1。

練習題:知道 MAKT,如何得知對應的 MARA?講義列了四種辦法:

  1. SE11 顯示 MAKT,在主鍵欄位 MATNR 上按「外來鍵關係」,就看得到 Check table = MARA。
  2. Delivery and Maintenance 頁籤(舊版在 Further characteristics),會標註 Text Table/Master Data Table。
  3. 依 SAP 命名習慣猜(主表 + T,如 MARA→MAKT、TCURC→TCURT),但光憑名稱不一定準,最終仍要回 SE11 核對
  4. 用 Data Element/Domain 的 Where-Used List,或直接查字典表 DD03L / DD08L。
" 用程式反查:某張表的所有外來鍵關係(DD08L = 外鍵表頭)
PARAMETERS p_tab TYPE tabname OBLIGATORY DEFAULT 'MAKT'.

START-OF-SELECTION.

  SELECT tabname,            " 從表
         fieldname,          " 外鍵欄位
         checktable,         " 主表(Check Table)
         frkart,             " 外鍵類型(TEXT = 文本表)
         card                " 基數
    FROM dd08l
    WHERE tabname  = @p_tab
      AND as4local = 'A'
    INTO TABLE @DATA(lt_fk).

  " 順便帶出主表的中文說明
  SELECT tabname, ddtext
    FROM dd02t
    FOR ALL ENTRIES IN @lt_fk
    WHERE tabname    = @lt_fk-checktable
      AND ddlanguage = @sy-langu
      AND as4local   = 'A'
    INTO TABLE @DATA(lt_txt).

  cl_demo_output=>display( lt_fk ).

6. 四張 Z 表與 ER Diagram(講義主線練習)

講義要求:以下四個表的關聯性,是學習 ER Diagram 的典型案例,請手繪關聯圖。這四張表也是後面所有 View、Search Help、Lock Object 練習的基礎。

表名用途欄位(Key 以粗體)
ZERP300_KNA1客戶資料表MANDT(MANDT, CLNT 3)、KUNNR(KUNAG, CHAR 10)、NAME1(NAME1_GP, CHAR)
ZERP300_MARA物料資料表MANDTMATNR(MATNR, CHAR 40)、MAKTX(MAKTX, CHAR 40)
ZERP300_VBAK訂單主檔MANDTVBELN(VBELN_VA)、ERDAT(ERDAT)、KUNNR(KUNAG, CHAR 10)、NETWR(NETWR_AK, CURR 15,2)、WAERK(WAERK, CUKY)
ZERP300_VBAP訂單明細MANDTVBELN(VBELN_VA)、POSNR(POSNR_VA)、MATNR(MATNR, CHAR 40)、KWMENG(KWMENG, QUAN)、VRKME(VRKME, UNIT 3)、NETWR(NETWR_AK, CURR 15,2)、WAERK(WAERK, CUKY)

關聯(外來鍵方向:從表 → 主表):

  • ZERP300_VBAK-KUNNR → ZERP300_KNA1(客戶 1 → 訂單 N)
  • ZERP300_VBAP-VBELN → ZERP300_VBAK(訂單主檔 1 → 明細 N)
  • ZERP300_VBAP-MATNR → ZERP300_MARA(物料 1 → 明細 N,這是講義的練習題
  • ZERP300_VBAK-WAERK / VBAP-WAERK → TCURC(幣別檢核)

講義另一組同構的練習表(2202 班的版本),欄位命名不同但關係一樣,做外來鍵時要注意:外來鍵有限定必須透過 Data Element,所以要先建 ZDKUNNR(Domain)與 ZEKUNNR(Data Element)才能綁到 Z2202_00_KNA1;同理明細表要建 ZDMATNR / ZEMATNR 綁 Z2202_00_MARA。

Z2202_00_VBAK(訂單主檔)Z2202_00_VBAP(訂單明細)
MANDT CLNT 3
ZCC 公司代碼 CHAR 4
ZSDNO 銷售編號 CHAR 10
ZDT 銷售日期 DATS 8
KUNNR 客戶編號 CHAR 10
MANDT CLNT 3
ZSDNO 銷售編號 CHAR 10
MATNR 物料編號 CHAR 40
ZQTY 銷售數量 NUMC 8
ZPRI 物料單價 CURR 15,2
ZAMT 銷售金額 CURR 15,2
ZCUR 交易貨幣 CUKY 5
設計檢查表:① MANDT 有沒有放第一個且是 Key?② CURR/QUAN 有沒有指定參考欄位?③ Delivery Class 是不是 A?④ Maintenance 是不是 Allowed?⑤ 外來鍵有沒有建?⑥ Enhancement Category 有沒有設?六項都過,SE54 才會順。

7. SE54 表格維護產生器、SM30 與自訂 TCODE

光有表還不夠,使用者要能自己維護資料。這就是表格維護產生器(Table Maintenance Generator)的工作。入口有兩個:SE54,或 SE11 顯示該表 → Utilities → Table Maintenance Dialog(或 Table Maintenance Generator)。

產生步驟:(依講義步驟整理)

  1. Authorization Group:沒有特別需求填 &NC&
  2. Function Group:填一個已存在且已激活的 Function Group(講義示範 ZFG2202_00_KNA1)。老師提醒:若無適合的 Function Group,可選 Z000,記得要按左上的新增鈕(像 A4 紙);也可以用 SE37SE80 先建一個,Function group 一定要激活
  3. Maintenance type
    • one step:只有一張總覽畫面(Overview),適合欄位少的表
    • two step:總覽 + 單筆明細畫面,欄位多時用
  4. Maint. Screen No.:按「Find Scr. Number(s)」讓系統自動配號(例如 0001 / 0002)。
  5. Create,系統自動產生 Screen、PBO/PAI 模組與更新程式。
  6. 激活。若激活不完整,要用 SE80 打開該 Function Group 全部激活(老師 250501 特別註記過兩次的坑)。

產生後,使用者用 SM30 輸入表名/視圖名就能維護;SM31 是舊版入口;SM34 用來維護 View Cluster(多層設定表串成一個維護樹)。

為何要用 SM30 取代 SE16N?老師要學生想一下——因為 SE16N 是給開發/顧問用的原始資料瀏覽器,沒有欄位檢核順序、沒有外鍵 F4 的完整行為、沒有權限群組控管、也留不下客製檢核邏輯;SM30 走的是產生出來的 Dynpro,能掛外來鍵檢核、能寫事件(Events)做自訂驗證,才是給終端使用者用的。
改了表結構一定要重產維護畫面。老師原話:若日後又修改欄位或增加欄位時,為了讓 screen 也有這些新增欄位,必須進行修改。做法是 SE54 → Change → 先 Delete 產生的物件再重新 Create,或用「Generated Objects → Change → 調整 Screen」。忘了這一步,SM30 上就看不到新欄位。

SM30 也能變 TCODE(講義 02-4):用 SE93 建立「Transaction with parameters(parameter transaction)」

  1. SE93 → 輸入 ZKNA1M → Create → 選 Transaction with parameters (parameter transaction)
  2. Transaction 填 SM30,勾 Skip initial screen
  3. Default Values 區填:VIEWNAME = ZERP300_KNA1UPDATE = X(X=維護模式,空白=顯示)。
  4. 存檔激活,使用者輸入 ZKNA1M 就直接進入維護畫面。

SM30 的 Events(SE54 → Environment → Modification → Events)可以掛自訂邏輯,例如存檔前檢核。以下是事件常式的現代化寫法(放在維護畫面所屬 Function Group 的 Include 中):

" SE54 Event 21(Fill hidden fields)/01(Before saving the data in the database)
" 在存檔前補上異動人/異動日,並做自訂檢核
FORM zerp300_kna1_before_save.

  DATA: lv_error TYPE abap_bool.

  LOOP AT total ASSIGNING FIELD-SYMBOL(<ls_total>).

    " TOTAL 的前段就是視圖/資料表結構,後面才是維護畫面用的動作旗標
    ASSIGN <ls_total> TO FIELD-SYMBOL(<ls_row>) CASTING TYPE zerp300_kna1.
    IF sy-subrc <> 0.
      CONTINUE.
    ENDIF.

    " 自訂檢核:金額不可為負
    IF <ls_row>-zwert < 0.
      MESSAGE |客戶 { <ls_row>-kunnr } 的金額不可為負| TYPE 'E'.
      lv_error = abap_true.
    ENDIF.

  ENDLOOP.

  IF lv_error = abap_true.
    " sy-subrc 非 0 時,維護畫面會中止存檔
    sy-subrc = 4.
  ENDIF.

ENDFORM.

8. 建立 Database View / Projection View / Maintenance View

Database View(講義 05):把 Z2202_00_VBAK + Z2202_00_KNA1 建成 ZDV2202_00_VBAK

  1. SE11 → View → Create → 選 Database view
  2. Tables 區輸入兩張表 → 按 Relationships,系統會依既有外來鍵自動帶出 Join 條件(也可自己在 Join Conditions 手打)。
  3. View Fields 區按 Table fields 逐一挑欄位。
  4. Selection Conditions 可加固定過濾條件(例如 MANDT = 或某狀態碼)。
  5. Maintenance Status:多表視圖只能設 read only
  6. 激活後,就可以在 ABAP SQL 直接 SELECT ... FROM ZDV2202_00_VBAK
老師的兩句重點:Database View 是強制 INNER JOIN,而且(多表時)只能查看資料選錯 View 型態,事後還可以換(SE11 → View → Change → 選單改型別),但「Database view 轉 Maintenance view 不見得能成功,反之則一定可以」。

練習題:ZDV300_VBAP_MARA(訂單明細 + 物料)、ZDV300_VBAK_KNA1(訂單主檔 + 客戶)。

Maintenance View(講義 06):建 ZMV_2202_00_VBAK

  • 參與的表必須已有外來鍵,連接條件不能自訂。
  • 所有參與表的主鍵都要放進視圖欄位,否則激活會報錯。
  • 每個欄位可設 R/O(唯讀)Key
  • 一定要建立螢幕——也就是要走 SE54 表格維護產生器,否則 SM30 進不去。
  • 練習題:ZMV300_VBAP_VBAK

Projection View:只能一張表,用途是遮罩欄位(例如只讓某群使用者看得到客戶編號與名稱,看不到金額),可維護。

Help View:專供 Search Help 的 Selection Method;參與表必須有外來鍵,語意是 LEFT OUTER JOIN(主表沒有對應文字時仍會出現),這正是「物料 + 物料說明」F4 的標準做法。

" 不建 DB View,直接在 ABAP SQL 做 JOIN(S/4HANA 建議做法之一)
SELECT k~vbeln,
       k~erdat,
       k~kunnr,
       c~name1,
       k~netwr,
       k~waerk
  FROM zerp300_vbak AS k
  INNER JOIN zerp300_kna1 AS c
    ON c~mandt = k~mandt
   AND c~kunnr = k~kunnr
  WHERE k~erdat IN @s_erdat
    AND k~kunnr IN @s_kunnr
  ORDER BY k~vbeln
  INTO TABLE @DATA(lt_order).

" 需要「客戶沒訂單也要出現」時,改 LEFT OUTER JOIN
SELECT c~kunnr,
       c~name1,
       SUM( k~netwr ) AS total_netwr
  FROM zerp300_kna1 AS c
  LEFT OUTER JOIN zerp300_vbak AS k
    ON  k~mandt = c~mandt
    AND k~kunnr = c~kunnr
  GROUP BY c~kunnr, c~name1
  ORDER BY c~kunnr
  INTO TABLE @DATA(lt_sum).

9. Structure、Table Type、Type Group 與 SYST

Structure(講義 07):SE11 → Data type → Structure。老師的注意事項:

  • 一般結構不需要放 MANDT(結構不落地,沒有 client 概念)。
  • 命名前三個字不能有底線,所以講義用 ZDV_ 這種寫法(Z + 兩碼 + 底線)。
  • 結構可以多層巢狀(欄位型別直接引用另一個結構,或用 .INCLUDE 包進來)。
  • 結構的用處是「寫程式時會方便很多」:ALV 的輸出結構、函數的介面參數、BDC 的資料容器,全都靠它。

Table Type(講義 08):SE11 → Data type → Table type。指定 Line Type(通常是剛才那個 Structure)、Access(Standard/Sorted/Hashed)、Key(Non-unique/Unique、Key components)。有了全域 Table Type,函數模組的 TABLES/CHANGING 參數才能直接引用。

" 對照:字典裡的 Structure / Table Type,在程式端的等價寫法
TYPES: BEGIN OF ty_order,
         vbeln TYPE zerp300_vbak-vbeln,
         erdat TYPE zerp300_vbak-erdat,
         kunnr TYPE zerp300_vbak-kunnr,
         name1 TYPE zerp300_kna1-name1,
         netwr TYPE zerp300_vbak-netwr,
         waerk TYPE zerp300_vbak-waerk,
       END OF ty_order.

" 三種存取型態,對應 Table Type 的 Access 設定
TYPES: ty_t_order_std  TYPE STANDARD TABLE OF ty_order WITH DEFAULT KEY,
       ty_t_order_srt  TYPE SORTED   TABLE OF ty_order WITH NON-UNIQUE KEY vbeln,
       ty_t_order_hsh  TYPE HASHED   TABLE OF ty_order WITH UNIQUE KEY vbeln.

DATA(lt_order) = VALUE ty_t_order_std( ).

" 巢狀結構:明細掛在表頭之下(字典的多層結構在程式端就是這樣)
TYPES: BEGIN OF ty_head,
         vbeln TYPE zerp300_vbak-vbeln,
         items TYPE STANDARD TABLE OF zerp300_vbap WITH EMPTY KEY,
       END OF ty_head.

" 用 CORRESPONDING 在字典結構之間搬資料,取代逐欄 MOVE
DATA ls_out TYPE ty_order.
SELECT SINGLE vbeln, erdat, kunnr, netwr, waerk
  FROM zerp300_vbak
  WHERE vbeln = @lv_vbeln
  INTO @DATA(ls_vbak).

ls_out = CORRESPONDING #( ls_vbak ).

Type Group(講義 09):類型組是「定義了多個資料類型的程式」。宣告時一定要用類型組的名稱起頭,而且要用 TYPE-POOL 宣告。最有名的就是 ALV 用的 SLIS

" 類型組本體(SE11 → Type Group):所有型別名稱必須以類型組名開頭
TYPE-POOL slis.

TYPES: slis_list_type(1) TYPE n,
       slis_char_1(1)    TYPE c,
       slis_text40(40)   TYPE c.
老師的定位很準:Type Group 是為了向下/向上相容而存在,會用就好,不必再擴展。S/4HANA 上請注意兩件事:① 從 ABAP 7.02 起,使用類型組裡的型別不必再寫 TYPE-POOLS 宣告;② 新開發不要再建自己的 Type Group,改用 全域 Interface 的常數/型別INTERFACE zif_const 內放 CONSTANTSTYPES)或 Class 的靜態型別,因為它們支援命名空間、可被 ADT 重構、也能寫單元測試。

特殊結構 SYST(講義 07-2):SY 是系統結構,老師要求至少記 10 個

欄位意義常見用法
SY-SUBRC上一個指令執行成功與否,0 表示成功每個 SELECT/CALL FUNCTION 之後必查
SY-UNAME當前登入的使用者名稱寫入異動人欄位
SY-DATUM當前系統日期異動日;注意是應用伺服器日期
SY-UZEIT當前系統時間異動時間
SY-TCODE當前執行的交易碼Enhancement 中判斷來源
SY-INDEXDO/WHILE 迴圈已跑過的次數不要跟 SY-TABIX 混用
SY-TABIX當前處理的是內表第幾筆LOOP 中定位、之後 MODIFY
SY-MANDT用戶端(Client)編號寫 client-dependent 資料
SY-LANGU登入語系抓 Text Table 一定要用(講義 Search Help 練習「找到登錄語系的系統變數」就是它)
SY-ULINE / SY-VLINE畫橫線/畫分隔號WRITE 報表用(現代報表已改 ALV)
SY-MSGID/TY/NO/V1~V4訊息類別/類型/編號/變數函數 EXCEPTIONS 後組訊息
SY-DBCNT上一個 SQL 影響的筆數UPDATE/DELETE 後確認
S/4HANA 補充:SY-DATUM 是 AS 伺服器時區的日期,跨時區系統請改用 cl_abap_context_info=>get_system_date( ) / get_user_date( ),或 GET TIME STAMP FIELD DATA(lv_ts) 取 UTC 時戳。取使用者名稱建議用 cl_abap_context_info=>get_user_technical_name( ),這在 ABAP Cloud(RAP)中是唯一被允許的寫法。

10. Search Help:Elementary、Collective 與 Search Help Exit

搜尋說明(F4 說明)是很有用的功能,可提高使用者友善性;可用於自建表或資料元素。設計時老師要你注意四個點:傳入屬性、顯示屬性、條件屬性、傳回屬性——對應到 SE11 Search Help 畫面的參數表格。

欄位意義
Search help parameter參數名(通常=Selection Method 的欄位名)
IMPImport:畫面的值傳入當作過濾條件(連動的關鍵)
EXPExport:選定後回填到畫面欄位
LPosList Position:出現在 F4 清單的第幾欄(顯示屬性
SPosSelection Position:出現在「限制條件對話框」的第幾列(條件屬性
DPosDialog Position:兩段式對話時的位置
Data element決定該參數的欄位說明與型別
Modification not allowed使用者不可在限制畫面上改這個條件

Elementary Search Help(簡單型)建立步驟,以講義 10-1 的 ZSH_MSEHI(數量單位)為例:(依講義步驟整理)

  1. SE11 → Search help → Create → 選 Elementary search help
  2. Selection method:填資料來源,可以是 Table、View 或 Help View。單位用 T006A(含語言)或建一個 Help View 把 T006 + T006A 串起來。
  3. Dialog type:Display values immediately(直接列值)/Dialog with value restriction(先問條件)/Dialog depends on set of values(值多才問)。
  4. 參數表格填 MSEHI(IMP/EXP 勾 EXP、LPos 1)、MSEHT(LPos 2)、SPRAS(IMP 勾、預設 SY-LANGU、Modification not allowed 勾)。
  5. 激活 → 按「測試」鈕(F8)試跑。

講義另一個練習 ZSH_WAERK(讓 ZERP300_VBAK 使用)的四個步驟,重點在多語言:

  1. 直接用 TCURC 當 Selection Method「似乎不是好主意,因為不能多語言」。
  2. 換成 TCURC 的 Text Table(標準是 TCURT;講義自建版寫成 ZTURT)。
  3. 找到「登錄語系的系統變數」= SY-LANGU,設成 SPRAS 參數的預設值。
  4. 把 Search Help 掛到資料表欄位上。

Search Help 加掛在物件的五種模式(講義原文):

  1. SE11 顯示資料表 → 游標在欄位上 → 按工具列的 Srch Help 鈕(掛在 Table Field)
  2. Screen(Dynpro)的欄位屬性 → Search help 欄位
  3. Data Element 的 Further Characteristics → Search Help(最推薦,一次全系統生效)
  4. Dialog/報表:PARAMETERS p_x TYPE ... MATCHCODE OBJECT <search help>
  5. 透過 Check Table 的隱含 F4(沒建 Search Help 時,外來鍵的主表就是預設 F4 來源)
REPORT zdemo_search_help.

" 模式 4:選擇畫面欄位直接掛字典的 Search Help
PARAMETERS: p_meins TYPE zerp300_vbap-vrkme MATCHCODE OBJECT zsh_msehi,
            p_waers TYPE zerp300_vbak-waerk MATCHCODE OBJECT zsh_waerk.

" 完全自訂的 F4:用內表當值來源(字典裡做不到的情境才用)
PARAMETERS p_type TYPE char10.

AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_type.

  TYPES: BEGIN OF ty_f4,
           code TYPE char10,
           text TYPE char40,
         END OF ty_f4.

  DATA(lt_f4) = VALUE STANDARD TABLE OF ty_f4(
    ( code = 'A' text = '主資料' )
    ( code = 'B' text = '交易資料' )
    ( code = 'C' text = '設定資料' ) ).

  CALL FUNCTION 'F4IF_INT_TABLE_VALUE_REQUEST'
    EXPORTING
      retfield        = 'CODE'          " 回傳哪一欄
      dynpprog        = sy-repid
      dynpnr          = sy-dynnr
      dynprofield     = 'P_TYPE'        " 回填到哪個畫面欄位
      value_org       = 'S'             " S = 結構式內表
    TABLES
      value_tab       = lt_f4
    EXCEPTIONS
      parameter_error = 1
      no_values_found = 2
      OTHERS          = 3.

  IF sy-subrc <> 0.
    MESSAGE '取值失敗' TYPE 'S' DISPLAY LIKE 'W'.
  ENDIF.

Collective Search Help(複雜型):由多個 Elementary Search Help 組成,使用者按 F4 時會看到多個頁籤(例如物料可用「依說明找」「依物料群組找」「依工廠找」)。建立時在 Included search helps 頁籤加入各個 Elementary,並按 Parameter assignment 把 Collective 的參數對應到各 Elementary 的參數。

講義 10-3:Collective Search Help 還可以再擴充——以 ME23 為例,把游標放在欄位上按 F1 → 技術資訊,可得知搜尋說明是 MEKK,就可以針對 MEKK 加入自己的 Elementary Search Help(Append Search Help,SE11 → Goto → Append Search Help,這樣升級不會被蓋掉)。

Search Help 連動的經典坑(講義原文的兩段對照):
第一種寫法不會連動:P_1 TYPE T001W-WERKS, P_2 TYPE T001L-LGORT.
第二種寫法連動:P_1 TYPE T001L-WERKS, P_2 TYPE T001L-LGORT.
原因:儲存地點 LGORT 的 Search Help 需要 WERKS 當 IMP 參數,而系統是靠「畫面上同名欄位」去抓值。當 P_1 宣告成 T001W-WERKS,畫面欄位的字典來源不同,F4 找不到對應的 WERKS 就不會過濾;改用 T001L-WERKS 之後,兩個欄位同屬 T001L,連動才成立。設計連動 F4 時,兩個欄位要來自同一張表/同一個 Search Help 的參數空間。

Search Help Exit:Search Help 也可以增強,用一支函數模組介入 F4 的每個階段。切記必須遵守 F4IF_SHLP_EXIT_EXAMPLE 的格式(複製它的介面),把函數名填在 Search Help 的 Search help exit 欄位。

FUNCTION z_shlp_exit_werks.
*"----------------------------------------------------------------------
*"  介面必須完全複製 F4IF_SHLP_EXIT_EXAMPLE:
*"  TABLES    shlp_tab   TYPE shlp_desct
*"            record_tab STRUCTURE seahlpres
*"  CHANGING  shlp       TYPE shlp_descr
*"            callcontrol LIKE ddshf4ctrl
*"----------------------------------------------------------------------

  " callcontrol-step 會依序被呼叫多次,用 CASE 分階段處理
  CASE callcontrol-step.

    WHEN 'SELONE'.
      " 選擇要用哪一個 Elementary Search Help(Collective 才會走到)
      RETURN.

    WHEN 'PRESEL'.
      " 進入選值畫面前:動態塞入額外的限制條件
      " 例如只允許使用者看自己有權限的工廠
      READ TABLE shlp-selopt ASSIGNING FIELD-SYMBOL(<ls_sel>)
           WITH KEY shlpfield = 'WERKS'.
      IF sy-subrc <> 0.
        APPEND VALUE ddshselopt( shlpfield = 'WERKS'
                                 sign      = 'I'
                                 option    = 'EQ'
                                 low       = '1000' ) TO shlp-selopt.
      ENDIF.
      RETURN.

    WHEN 'SELECT'.
      " 自己撈資料:撈完把 callcontrol-step 設成 'DISP' 可跳過標準選取
      RETURN.

    WHEN 'DISP'.
      " 清單顯示前:過濾掉不該讓使用者看到的列
      LOOP AT record_tab ASSIGNING FIELD-SYMBOL(<ls_rec>).
        IF <ls_rec>-string CS 'TEST'.
          DELETE record_tab INDEX sy-tabix.
        ENDIF.
      ENDLOOP.
      RETURN.

    WHEN 'RETURN'.
      " 使用者選定之後、回填畫面之前
      RETURN.

  ENDCASE.

ENDFUNCTION.
講義 10-2 的練習「ZUSER 練習修改名字」:以 USER 起頭的 Search Help(如 USER_ADDR、USER_COMP)可以提供大量與使用者相關的 F4。練習目標是自建一個 ZUSER 系列的 Search Help,Selection Method 用 USR21 + ADRP(或直接用 Help View),讓 F4 能顯示使用者代號與姓名,並可回填。

11. Lock Object 鎖物件與 ENQUEUE/DEQUEUE

鎖物件是對資料的存取進行並行控制。加鎖就是把使用者正在存取的資料登記在記憶體的 Lock Table(由 Enqueue Server 管理),解鎖就釋放。建立鎖物件會自動產生兩個函數,一個加鎖、一個解鎖;鎖物件名稱要以 E 開頭。

SE11 建立步驟:(依講義步驟整理)

  1. SE11 → Lock object → 輸入 EZERP300_VBAK → Create。
  2. Tables 頁籤:Primary table 填 ZERP300_VBAK;若要連明細一起鎖,可再加 Secondary table(要有外來鍵關係)。
  3. Lock mode 選:
    • E(Write lock,獨佔可累加):最常用,同一交易可重複加鎖
    • S(Read lock,共享):多人可同時讀,但不可有人寫
    • X(Exclusive,不可累加)
    • O(Optimistic):先樂觀讀,存檔時才升級為寫鎖
  4. Lock parameter:預設是主表的所有 Key 欄位;可只挑部分(未挑的欄位視為萬用,會鎖更廣)。
  5. 激活 → 系統產生 ENQUEUE_EZERP300_VBAKDEQUEUE_EZERP300_VBAK
REPORT zerp300_vbak_change.

PARAMETERS p_vbeln TYPE zerp300_vbak-vbeln OBLIGATORY.

START-OF-SELECTION.

  " 1. 先加鎖,鎖不到就不要往下做
  CALL FUNCTION 'ENQUEUE_EZERP300_VBAK'
    EXPORTING
      mode_zerp300_vbak = 'E'          " E = 寫鎖
      mandt             = sy-mandt
      vbeln             = p_vbeln
      _scope            = '2'          " 2 = 鎖延續到 COMMIT WORK 才由更新程序釋放
      _wait             = abap_true    " 鎖不到時稍等再試
    EXCEPTIONS
      foreign_lock      = 1
      system_failure    = 2
      OTHERS            = 3.

  CASE sy-subrc.
    WHEN 0.
      " 取得鎖,繼續
    WHEN 1.
      " sy-msgv1 會帶回鎖住這筆資料的使用者
      MESSAGE |訂單 { p_vbeln ALPHA = OUT } 正被使用者 { sy-msgv1 } 鎖定中| TYPE 'E'.
    WHEN OTHERS.
      MESSAGE '鎖定失敗,請洽系統管理員' TYPE 'E'.
  ENDCASE.

  " 2. 讀取 → 修改
  SELECT SINGLE vbeln, erdat, kunnr, netwr, waerk
    FROM zerp300_vbak
    WHERE vbeln = @p_vbeln
    INTO @DATA(ls_vbak).

  IF sy-subrc = 0.
    ls_vbak-netwr = ls_vbak-netwr + 100.

    UPDATE zerp300_vbak
       SET netwr = @ls_vbak-netwr
     WHERE vbeln = @p_vbeln.

    IF sy-subrc = 0.
      COMMIT WORK AND WAIT.
      MESSAGE |訂單 { p_vbeln ALPHA = OUT } 已更新,共 { sy-dbcnt } 筆| TYPE 'S'.
    ELSE.
      ROLLBACK WORK.
      MESSAGE '更新失敗' TYPE 'E'.
    ENDIF.
  ENDIF.

  " 3. 一定要解鎖(_scope = '2' 時 COMMIT 會自動解,但顯式解鎖較保險)
  CALL FUNCTION 'DEQUEUE_EZERP300_VBAK'
    EXPORTING
      mode_zerp300_vbak = 'E'
      mandt             = sy-mandt
      vbeln             = p_vbeln
      _scope            = '3'.
鎖物件三大坑:
_SCOPE 參數:1=鎖屬於當前對話(要自己 DEQUEUE);2=鎖交給更新程序,COMMIT WORK 時自動釋放(標準交易最常用);3=兩者都要。用錯會出現「資料改完鎖還在」或「還沒存檔鎖就沒了」。
程式中途 LEAVE/MESSAGE E 跳出而沒解鎖,鎖會殘留到 session 結束。可用 SM12 查看與手動刪除(正式機刪鎖要非常小心)。
加鎖不等於資料庫鎖。SAP Enqueue 是「君子協定」:只有大家都去呼叫 ENQUEUE 才有效。你直接 UPDATE 而不加鎖,系統不會阻止你——所以改標準表一定要用標準的 ENQUEUE 函數(例如改物料要用 ENQUEUE_EMMARAE)。
批次程式要一次釋放全部的鎖,可呼叫 DEQUEUE_ALL;若要用「集中送出」提升效能,ENQUEUE 加 _COLLECT = abap_true,最後呼叫 FLUSH_ENQUEUE

12. 擴充既有資料表:Append Structure 與 Include

要在標準表(或別人的表)上加欄位,不能直接改表定義(升級會被蓋掉、且屬於修改標準),正確做法有兩種:

方式做法適用
Append StructureSE11 顯示表 → Append Structure… → 建 ZA_MARA_EXT,欄位一律以 ZZYY 開頭加在表的最後面;升級安全;一張表可以有多個 Append
Customizing Include(CI_)SAP 事先在表中預留 .INCLUDE CI_xxxx,你只要去建那個結構SAP 指定的擴充點,如 CI_COBL(會計指派區塊)
.INCLUDE 自建結構自建表的 Fields 中直接寫 .INCLUDE 引用一個 Structure自己的 Z 表,讓多張表共用同一段欄位(如異動人/異動日)
Append Structure 的限制:① 欄位名必須在客戶命名空間(ZZ/YY)否則升級可能撞名;② 不能加到主鍵(Append 一定在最後面);③ Pooled/Cluster table 不能 Append;④ 加完之後要 SE14 Activate and adjust database;⑤ S/4HANA 上優先考慮 Custom Fields and Logic(Key User Extensibility,Fiori App),它會自動把欄位延伸到 CDS View 與 Fiori 畫面,比手工 Append 省事很多。
" 讀取自己 Append 上去的欄位,跟標準欄位混用完全一樣
SELECT matnr,
       mtart,
       matkl,
       zzcustom_flag,          " 來自 Append Structure ZA_MARA_EXT
       zzcustom_grp
  FROM mara
  WHERE mtart = @p_mtart
    AND lvorm = @space
  ORDER BY matnr
  INTO TABLE @DATA(lt_mara).

" 動態判斷某欄位是否存在(跨系統版本相容時很好用)
DATA(lo_struct) = CAST cl_abap_structdescr(
                    cl_abap_typedescr=>describe_by_name( 'MARA' ) ).

DATA(lt_comp) = lo_struct->get_components( ).

IF line_exists( lt_comp[ name = 'ZZCUSTOM_FLAG' ] ).
  cl_demo_output=>write( '本系統的 MARA 已擴充 ZZCUSTOM_FLAG' ).
ELSE.
  cl_demo_output=>write( '本系統尚未擴充' ).
ENDIF.

13. S/4HANA 補充:CDS View 取代自建 DB View、HANA 上的 Z 表設計

講義的 Database View 觀念要保留(考證照、看舊程式都用得到),但新開發請改用 ABAP CDS View。CDS 的好處是:可加註解(讓 Fiori/OData/分析工具直接使用)、支援參數、支援關聯(association)、支援彙總與 SQL 函數、程式碼可進版控。

版本差異一定要記:S/4HANA 1809(ABAP 7.52/7.53)只有 DDIC-based CDS View,語法是 DEFINE VIEW 且必須加 @AbapCatalog.sqlViewName(會產生一個 SQL View)。從 S/4HANA 2020(ABAP 7.55)起才有 CDS View EntityDEFINE VIEW ENTITY),不需要也不可以寫 sqlViewName。兩者都只能在 Eclipse ADT 裡建立,SE11/SE80 只能顯示。
" 寫法 A:S/4HANA 1809 ~ 1909 可用(DDIC-based CDS View)
@AbapCatalog.sqlViewName: 'ZVERP300ORD'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: '訂單主檔含客戶名稱'
define view Z_I_Erp300Order
  as select from zerp300_vbak as Ord
  inner join    zerp300_kna1  as Cust
    on  Ord.mandt = Cust.mandt
    and Ord.kunnr = Cust.kunnr
{
  key Ord.vbeln  as SalesOrder,
      Ord.erdat  as OrderDate,
      Ord.kunnr  as Customer,
      Cust.name1 as CustomerName,
      @Semantics.amount.currencyCode: 'Currency'
      Ord.netwr  as NetAmount,
      @Semantics.currencyCode: true
      Ord.waerk  as Currency
}
" 寫法 B:S/4HANA 2020(ABAP 7.55)以後的 CDS View Entity — 新專案請用這個
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: '訂單主檔含客戶名稱'
define view entity Z_I_Erp300Order
  as select from zerp300_vbak as Ord
  association [1..1] to zerp300_kna1 as _Customer
    on  $projection.Customer = _Customer.kunnr
{
  key Ord.vbeln       as SalesOrder,
      Ord.erdat       as OrderDate,
      Ord.kunnr       as Customer,
      _Customer.name1 as CustomerName,

      @Semantics.amount.currencyCode: 'Currency'
      Ord.netwr       as NetAmount,

      @Semantics.currencyCode: true
      Ord.waerk       as Currency,

      _Customer                       // 曝露 association 給上層 View 使用
}
" 在 ABAP 中消費 CDS View:跟讀一般資料表一樣,但可用別名欄位與彙總
SELECT SalesOrder,
       OrderDate,
       Customer,
       CustomerName,
       NetAmount,
       Currency
  FROM z_i_erp300order
  WHERE OrderDate IN @s_erdat
    AND Customer  IN @s_kunnr
  ORDER BY SalesOrder
  INTO TABLE @DATA(lt_order).

" Code Pushdown:彙總交給 HANA 做,不要撈回 ABAP 再 LOOP 加總
SELECT Customer,
       CustomerName,
       SUM( NetAmount ) AS TotalAmount,
       COUNT( * )       AS OrderCount
  FROM z_i_erp300order
  WHERE OrderDate IN @s_erdat
  GROUP BY Customer, CustomerName
  HAVING SUM( NetAmount ) > 10000
  ORDER BY Customer
  INTO TABLE @DATA(lt_sum).

Z 表在 HANA 上的設計原則

  • SE11 建的表在 HANA 預設是 Column Store(欄式儲存),適合分析與大量掃描;只有極高頻單筆讀寫的小表才考慮 Row Store(技術設定的 Storage Type)。
  • 不要亂建 Secondary Index。Row Store 時代靠索引解決的問題,HANA 靠欄式壓縮+平行掃描就夠了;多餘索引反而拖慢寫入、佔記憶體。真的要建,先用 ST05/SQL Monitor 證明有需要。
  • Code Pushdown:能在 SQL 做的(JOIN、彙總、CASE、字串處理)就不要撈回 ABAP 用 LOOP 做。
  • 大表避免 SELECT *——欄式儲存下,多讀一欄就多一份 I/O。
  • SELECT SINGLE 沒有排序保證:多筆符合時回傳哪一筆由 DB 決定,HANA 上結果可能與 AnyDB 不同。要指定就寫 ORDER BY ... UP TO 1 ROWS
這正是專案上真實發生過的坑:某支成本報表因為資料同時存在兩種角度(例如 item category(H)cost component(E)),兩者的成本構成欄位數量不同,而程式沒有排序就取第一筆,於是「一下抓到 H、一下抓到 E」,成本結果不穩定。解法就是明確指定要哪一種(加 WHERE 條件)並加上 ORDER BY。另外相關經驗:條件用列舉(IN 明列值)比用區間(BETWEEN)安全,區間可能把不該納入的代碼一起掃進來。

14. 用程式讀字典:DD* 表與 RTTI

字典本身也是一堆表。會查這些表,等於多了一把萬用鑰匙:

字典表內容
DD02L / DD02T資料表清單/表的說明文字
DD03L / DD03T表的欄位清單(含 Key 旗標、位置、Data Element)/欄位說明
DD04L / DD04TData Element/其文字
DD01L / DD01TDomain/其文字
DD07L / DD07TDomain 的固定值/固定值文字
DD08L / DD05Q外來鍵表頭/外來鍵欄位對應
DD25L / DD26S / DD27SView 表頭/View 的基底表/View 的欄位
TADIR所有 Repository 物件的登錄(誰建的、屬於哪個 Package)
REPORT zdict_explorer.

PARAMETERS p_tab TYPE tabname OBLIGATORY DEFAULT 'ZERP300_VBAK'.

START-OF-SELECTION.

  " 一次撈出表的欄位清單,並帶出 Data Element 的中文說明
  SELECT f~position,
         f~fieldname,
         f~keyflag,
         f~rollname AS data_element,
         f~domname  AS domain,
         f~datatype,
         f~leng,
         f~decimals,
         t~ddtext   AS description
    FROM dd03l AS f
    LEFT OUTER JOIN dd04t AS t
      ON  t~rollname   = f~rollname
      AND t~ddlanguage = @sy-langu
      AND t~as4local   = 'A'
    WHERE f~tabname   = @p_tab
      AND f~as4local  = 'A'
      AND f~fieldname <> '.INCLUDE'
    ORDER BY f~position
    INTO TABLE @DATA(lt_field).

  IF lt_field IS INITIAL.
    MESSAGE |資料表 { p_tab } 不存在或未激活| TYPE 'E'.
  ENDIF.

  TRY.
      cl_salv_table=>factory(
        IMPORTING r_salv_table = DATA(lo_alv)
        CHANGING  t_table      = lt_field ).
      lo_alv->get_functions( )->set_all( abap_true ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->display( ).
    CATCH cx_salv_msg INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.
" 用 RTTS 動態建立內表:使用者輸入表名,就把它整張撈出來顯示
PARAMETERS p_tname TYPE tabname OBLIGATORY DEFAULT 'ZERP300_KNA1'.

DATA lr_data TYPE REF TO data.
FIELD-SYMBOLS <lt_any> TYPE STANDARD TABLE.

START-OF-SELECTION.

  TRY.
      " 依表名取得結構描述,再組出對應的內表型別
      DATA(lo_struct) = CAST cl_abap_structdescr(
                          cl_abap_typedescr=>describe_by_name( p_tname ) ).

      DATA(lo_table) = cl_abap_tabledescr=>create(
                         p_line_type  = lo_struct
                         p_table_kind = cl_abap_tabledescr=>tablekind_std ).

      CREATE DATA lr_data TYPE HANDLE lo_table.
      ASSIGN lr_data->* TO <lt_any>.

      " 動態 SELECT(整列取出,這裡是有意為之的例外)
      SELECT * FROM (p_tname)
        INTO TABLE @<lt_any>
        UP TO 500 ROWS.

      cl_salv_table=>factory(
        IMPORTING r_salv_table = DATA(lo_alv)
        CHANGING  t_table      = <lt_any> ).
      lo_alv->display( ).

    CATCH cx_sy_move_cast_error.
      MESSAGE |{ p_tname } 不是結構或資料表| TYPE 'E'.
    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.
另一個常用的動態技巧:cl_abap_typedescr=>describe_by_data( ls_any ) 可以在執行期問「這個變數到底是什麼型別、多長」;配合 ASSIGN COMPONENT 'MATNR' OF STRUCTURE ls_any TO FIELD-SYMBOL(<lv>) 就能寫出通吃各種結構的共用副程式。

練習與自我檢核

  1. 建立 ZERP300_KNA1:MANDT/KUNNR/NAME1/ZWERT/ZMENG/ZIEME。檢核點:CURR 有沒有指定幣別參考欄位(VBAP-WAERK)、QUAN 有沒有指定單位參考欄位(VBAP-ZIEME)?Delivery Class 是不是 A?做到能用 SE16N 新增出資料才算成功。
  2. SE54 建維護畫面 → SM30 能維護。若 Function Group 選不到,用 SE37/SE80 自己建一個並全部激活;也可先用 Z000。做完再用 SE93 把它包成一支 TCODE(parameter transaction,VIEWNAME + UPDATE=X + Skip initial screen)。
  3. 建立四張表並手繪 ER Diagram:ZERP300_KNA1 / ZERP300_MARA / ZERP300_VBAK / ZERP300_VBAP,標出 1:N 關係與外來鍵方向。
  4. Domain + Data Element 練習:建一個幣別 Data Element,限定只能填 TCURC 內的值(Domain 型別 CUKY 5 + Value Table TCURC,Data Element 綁它,表欄位建外來鍵)。另建一個數值 Domain 並觀察 Sign 不打勾=只能輸入正數
  5. 外來鍵練習(講義 04):Z2202_00_VBAK N → 1 Z2202_00_KNA1。要先建 ZDKUNNR(Domain)+ ZEKUNNR(Data Element)。練習題:為訂單明細建 ZDMATNR + ZEMATNR,Z2202_00_VBAP N → 1 Z2202_00_MARA。
  6. Text Table 練習:知道 MAKT,如何找到 MARA?寫出四種方法(SE11 外鍵、Delivery and Maintenance 頁籤的 Text Table 標註、命名習慣+回頭核對、Where-Used List / DD03L / DD08L)。
  7. Database View:建 ZDV2202_00_VBAK(VBAK + KNA1)。練習題:ZDV300_VBAP_MARA、ZDV300_VBAK_KNA1。答案要點:多表 DB View 只能顯示、強制 INNER JOIN。
  8. Maintenance View:建 ZMV_2202_00_VBAK,記得所有主鍵都要進視圖並用 SE54 產生螢幕。練習題:ZMV300_VBAP_VBAK。
  9. Structure / Table Type:建一個 ZDV_ 開頭的多層結構(不要放 MANDT,前三碼不能有底線),再建對應的 Table Type,然後在報表中用它接 SELECT 結果並用 CL_SALV_TABLE 顯示。
  10. SYST 默寫:至少寫出 10 個 SY 欄位與用途(SUBRC/UNAME/DATUM/UZEIT/TCODE/INDEX/TABIX/MANDT/LANGU/ULINE/VLINE/MSGV1/DBCNT)。
  11. Type Group:說明 TYPE-POOL slis 裡的型別命名規則(必須以類型組名開頭),並回答:S/4HANA 上為什麼不必再寫 TYPE-POOLS?新開發應該改用什麼?
  12. Search Help 10-1:建 ZSH_MSEHI,並為 Z2202_00_MARA 新增 MSEHI 欄位(Data Element MEINS)掛上它。10-2:ZUSER 系列練習修改名字。ZSH_WAERK:四步驟(TCURC 不能多語言 → 改用 Text Table → SPRAS 預設 SY-LANGU → 掛到 ZERP300_VBAK)。
  13. Search Help 連動:解釋為什麼 P_1 TYPE T001W-WERKS 不會連動、P_1 TYPE T001L-WERKS 會連動。
  14. Collective Search Help 擴充:以 ME23 找出採購資訊的 Search Help 是 MEKK,說明如何用 Append Search Help 掛上自己的 Elementary(升級安全)。並說出 Search Help Exit 必須遵守哪支範例函數的介面(F4IF_SHLP_EXIT_EXAMPLE)與五個 step(SELONE/PRESEL/SELECT/DISP/RETURN)。
  15. Lock Object:建 EZERP300_VBAK,寫一支程式做「加鎖 → 讀 → 改 → COMMIT → 解鎖」,並測試兩個 session 同時執行時第二個會拿到什麼訊息(FOREIGN_LOCK,SY-MSGV1 帶鎖定者)。再說明 _SCOPE 1/2/3 的差別,以及用 SM12 查鎖。
  16. S/4HANA 觀念題:① 為什麼新開發不建 Database View 而用 CDS?② 1809 與 2020 的 CDS 語法差在哪(sqlViewName / DEFINE VIEW ENTITY)?③ Z 表在 HANA 上該不該加索引?④ 為什麼「SELECT SINGLE 沒排序保證」會造成成本報表結果不穩定?

第 1-3 章 三層 VIEW 與重要主數據:SM30 / SE54 / SE39 / ZUSER 實作

這一章要做兩件事。第一件是把「資料表 → 維護視圖 → 維護對話框 → View Cluster → 自訂 TCODE」這條完整的三層式維護鏈路親手做一遍,做出跟 SAP 標準畫面(NACE、OKKP、OMT3)一模一樣的多階維護畫面。第二件是把 SAP 的重要主數據表一次認完:使用者(USR21/ADRP/USR02)、客戶(KNA1/KNB1/KNVV)、廠商(LFA1/LFB1/LFM1)、物料、銷售、採購、財務、庫存,並且知道在 S/4HANA 這些表哪些已經變成相容視圖。學完之後,你能看到任何一個 SAP 畫面就反推出它背後是哪張表、是怎麼被設計出來的。

核心 TCODESE11SE54SM30SM34SE39SE38SE93SE80SE16NSE16HSE84SDBEST05NACEOKKPOMT3XD99XK99SU01PA30PA40PPOMESE37
關鍵物件ZMARA/ZMARC/ZMARD、ZMV_MARA/ZMV_MARC/ZMV_MARD、VCLDIR、VCLSTRUC、USR21、ADRP、USR02、KNA1、KNVV、KNB1、LFA1、LFM1、LFB1、MARA、MARC、MARD、MBEW、VBAK、VBAP、EKKO、EKPO、ACDOCA、MATDOC、HRP1001、CL_SALV_TABLE、VIEW_MAINTENANCE_CALL

1. 為什麼要學三層維護?先看 SAP 自己怎麼做

SAP 標準系統裡有一大類畫面長得都一樣:左邊一個樹狀或清單,選一筆,雙擊進去下一層,再選一筆,再進去更下一層。這種畫面叫做多階維護視圖(View Cluster,中文常稱「維護視圖群組」),不是有人手工刻的 Dialog 程式,而是用 SE54 產生出來的。

  • NACE 輸出控制(Output Determination)。應用別 → 輸出類型 → 存取順序 → 條件記錄,典型的多階維護。講義第二節就是叫你先去 NACE 點一遍,體會「原來這是產生出來的」。
  • OKKP 管控範圍(Controlling Area)維護,也是三層式:管控範圍 → 分派公司代碼 → 啟動元件/控制指標。
  • OMT3 把倉庫號碼指派到工廠/儲存地點(Assign Warehouse Number to Plant/Storage Location),是 WM 與 IM 整合的重要設定。路徑:SPRO → Enterprise Structure → Assignment → Logistics Execution → Assign Warehouse Number to Plant/Storage Location。它其實就是一支「參數交易」,背後跑的是 View Cluster。
老師的重點:學完這一章,以後看到類似的畫面就能知道他是如何設計的了。這是 ABAP 顧問跟一般 Coder 的分水嶺 —— 能配置解決的就不要寫程式,而多階維護畫面根本不用寫程式,用產生的就好。

2. 三層式維護的觀念地圖:表 → 維護視圖 → 維護對話框 → View Cluster

整條鏈路一共四個物件,缺一不可。很多同學做不出來,都是因為搞不清楚自己現在在做哪一層。

層次物件建立工具用途
① 資料層透明表 Transparent Table(ZMARA/ZMARC/ZMARD)SE11真正存資料的地方;表跟表之間用外部索引鍵(Foreign Key)串起來
② 視圖層維護視圖 Maintenance View(ZMV_MARA/ZMV_MARC/ZMV_MARD)SE11 → View → Maintenance View決定「這一層要維護哪些欄位」,並帶 Delivery Class 與 Data Browser/Table View Maint. 設定
③ 畫面層維護對話框 Table Maintenance Dialog(Function Group + Screen + 事件)SE54(Generated Table Maintenance)產生 SM30 可以叫得動的畫面;一步式或兩步式
④ 群組層維護視圖群組 View Cluster(例如 ZMV_MARA_000)SE54 → Edit View Cluster把三個維護對話框串成「上層選一筆 → 下層只顯示相關資料」的階層畫面,用 SM34 執行
名詞對照:SE54 的正式名稱是「Generate Table Maintenance Dialog」,也可以從 SE11 的選單 Utilities → Table Maintenance Generator 進去,兩者是同一件事。SM30 執行「單一維護視圖」,SM34 執行「View Cluster」。

3. 第一步:SE11 建三張表 ZMARA / ZMARC / ZMARD 與外部索引鍵

這三張表在模仿 SAP 標準物料主檔的三層結構:ZMARA(一般資料,Key = MANDT + MATNR)→ ZMARC(工廠資料,Key = MANDT + MATNR + WERKS)→ ZMARD(儲存地點/庫存資料,Key = MANDT + MATNR + WERKS + LGORT)。

主鍵建議欄位外部索引鍵
ZMARAMANDT、MATNRMTART(物料類型)、MATKL(物料群組)、MEINS(基本單位)、ERSDA、ERNAMMTART → T134、MEINS → T006
ZMARCMANDT、MATNR、WERKSDISMM(MRP 類型)、EKGRP(採購群組)、BESKZ(採購類型)MANDT、MATNR → ZMARA;WERKS → T001W
ZMARDMANDT、MATNR、WERKS、LGORTLABST(未受限庫存)、INSME(品檢庫存)、SPEME(凍結庫存)MANDT、MATNR、WERKS → ZMARC;LGORT → T001L

建外部索引鍵的操作:在 SE11 的 Fields 頁籤把游標停在該欄位 → 按工具列的 Foreign Keys 鑰匙圖示 → Check table 填上層表名 → 系統會自動提出欄位對應(Key fields)→ 確認 Field assignment 正確 → Cardinality 通常填 1 : CN(上層一筆對下層多筆)→ Foreign key field type 選 Key fields of a text table / Non-key-fields 視情況。(依講義步驟整理)

外部索引鍵不是裝飾品。SE54 產生 View Cluster 時,就是靠外部索引鍵自動推導上下層的關聯鍵。外鍵沒建或建錯,SE54 抓出來的關聯欄位就會是錯的,做到最後一定卡住。這也是講義 6-04 心得第 2 點在講的事。

三張表建好之後,先用一段 ABAP 驗證資料串得起來。這是現代寫法的 JOIN,一次把三層撈出來:

REPORT zmm_zmard_check.

" 三層 JOIN:ZMARA(一般) → ZMARC(工廠) → ZMARD(儲存地點)
" 注意:ABAP SQL 一律用 @ 逸出主機變數;不要用 SELECT *
PARAMETERS: p_werks TYPE t001w-werks OBLIGATORY.

SELECT a~matnr,
       a~mtart,
       a~matkl,
       a~meins,
       c~werks,
       c~dismm,
       c~ekgrp,
       d~lgort,
       d~labst,
       d~insme
  FROM zmara AS a
  INNER JOIN zmarc AS c ON c~matnr = a~matnr
  INNER JOIN zmard AS d ON d~matnr = c~matnr
                       AND d~werks = c~werks
  WHERE c~werks = @p_werks
  ORDER BY a~matnr, c~werks, d~lgort
  INTO TABLE @DATA(lt_stock).

IF lt_stock IS INITIAL.
  MESSAGE |工廠 { p_werks } 沒有任何資料| TYPE 'S' DISPLAY LIKE 'W'.
  RETURN.
ENDIF.

" 用 REDUCE 直接算總庫存,不要再寫迴圈累加
DATA(lv_total) = REDUCE zmard-labst( INIT s = CONV zmard-labst( 0 )
                                     FOR <ls> IN lt_stock
                                     NEXT s = s + <ls>-labst ).
WRITE: / |工廠 { p_werks } 未受限庫存合計:{ lv_total NUMBER = USER }|.
MANDT 不需要出現在 JOIN 條件裡。ABAP SQL 對 client-dependent 的表會自動加上目前 client 的條件(若真的要跨 client,才用 USING ALL CLIENTSUSING CLIENT @lv_mandt)。

4. 第二步:SE11 建三個維護視圖 ZMV_MARA / ZMV_MARC / ZMV_MARD

路徑:SE11 → View → 輸入 ZMV_MARA → Create → 選 Maintenance View

  1. Tables/Join Conditions:Tables 只放一張表(ZMARA)。三層維護的每一個維護視圖各自對應一張表,不要在這裡就 JOIN 起來,JOIN 是 View Cluster 的工作。
  2. View Flds:按 Table fields 把要維護的欄位全部帶進來。Key 欄位一定要在。
  3. Maint.Status(維護狀態)頁籤:Access = Read and change(可讀可改);Delivery class 通常設 A(Application table)或 C(Customizing table,會產生傳輸請求)。
  4. 存檔 → 指定 Package(三個 View 必須放同一個 Package)→ Activate。
  5. ZMV_MARC、ZMV_MARD 重複同樣動作。
講義 6-04 心得第 3 點的「S」:在建 Maintenance View 與建 Cluster View 的欄位清單裡,都有一欄要勾成 S(代表該欄位是上層傳下來的「Selection/Sub-set 欄位」,也就是用來把下層資料限縮成「只屬於上層那一筆」的欄位)。兩個地方都要勾,只勾一邊做不出階層效果。
Delivery Class 選 C 還是 A 的實務差別:設定型資料(客戶自訂的組態)選 C,維護時會跳出傳輸請求視窗,可以從 DEV 傳到 PRD;交易型/主資料選 A,資料留在各系統不跟著傳輸。做客戶專案時選錯,會變成「PRD 的資料被 DEV 覆蓋」或「設定傳不過去」。

5. 第三步:SE54 產生維護對話框(一步式 vs 兩步式)

SE54 初始畫面:輸入 Table/View = ZMV_MARA → 選 Generated Objects → Create。要填的三個關鍵欄位:

欄位填什麼說明
Authorization Group&NC& 或自訂的權限群組&NC& = 不檢查。正式專案應建自己的權限群組(SE54 → Authorization Groups)
Function Group自己的 Z 開頭 FG,例如 ZFG_MM_MARA老師特別強調:要用自己的 Function Group,不要像截圖裡用 TMP。TMP 是本機物件,不能傳輸,而且會跟別人打架
Maintenance typeone steptwo step見下表
Overview screen / Single screenFind scr. number(s) 讓系統自動配號(通常 0001 / 0002)兩步式才需要第二個畫面號
維護型態畫面適用時機
一步式 One step只有一個總覽畫面(Overview),所有欄位都在同一列上編輯欄位少(大約 10 個以內)、螢幕放得下;三層維護的下層通常用一步式
兩步式 Two step總覽畫面只顯示 Key + 少數欄位,雙擊某一列才進入單筆明細畫面(Single screen)維護全部欄位欄位多、或需要在明細畫面做分組與說明文字

三個維護視圖各做一次 SE54,做完三個維護對話框。做完之後用 SM30 逐一測試:輸入 ZMV_MARA → Maintain → 應該可以新增/修改/刪除。

SE54 產生出來的東西可以在 SE80 的那個 Function Group 底下看到:畫面 0001/0002、PBO/PAI 模組、以及一個 Events 掛點。要加自訂檢核邏輯就是掛在 Events,不是去改產生出來的畫面(會被重新產生時蓋掉)。

6. 在維護對話框掛事件:存檔前檢核(Event 01 / 05 / 21)

SE54 → 進入產生畫面後選單 Environment → Modification → Events,選一個事件編號,填一個 FORM 名稱,按小鉛筆讓系統把 FORM 骨架寫進 Function Group 的 include(通常是 LZFG_MM_MARAF01)。常用事件:

事件時機典型用途
01Before saving the data in the database存檔前的完整性檢核、擋掉不合理資料
02After saving the data in the database寫 Log、觸發後續處理
05Creating a new entry自動帶預設值、自動編號
21Fill hidden fields補 ERDAT/ERNAM/AEDAT/AENAM 這種不顯示在畫面上的欄位
AAInstead of the standard data read routine自己控制資料讀取(少用)
*&---------------------------------------------------------------------*
*& Include LZFG_MM_MARAF01 —— SE54 事件的自訂 FORM
*&---------------------------------------------------------------------*

" Event 21:Fill hidden fields(自動補建檔人與建檔日)
FORM fill_hidden_fields.
  " TOTAL / EXTRACT 是維護對話框產生的全域內表,型別為 view 的結構
  zmv_mara-ersda = COND #( WHEN zmv_mara-ersda IS INITIAL
                           THEN sy-datum ELSE zmv_mara-ersda ).
  zmv_mara-ernam = COND #( WHEN zmv_mara-ernam IS INITIAL
                           THEN sy-uname ELSE zmv_mara-ernam ).
ENDFORM.

" Event 01:Before saving —— 存檔前檢核
FORM check_before_save.
  " 物料類型必須存在於 T134,否則擋下
  FIELD-SYMBOLS <ls_row> TYPE zmv_mara.

  LOOP AT total ASSIGNING FIELD-SYMBOL(<ls_total>).
    " TOTAL 的第 1 個字元是動作旗標,資料本體從第 2 個字元起
    ASSIGN <ls_total>+1 TO <ls_row> CASTING.
    IF sy-subrc <> 0.
      CONTINUE.
    ENDIF.

    SELECT SINGLE @abap_true FROM t134
      WHERE mtart = @<ls_row>-mtart
      INTO @DATA(lv_exists).

    IF lv_exists <> abap_true.
      MESSAGE |物料類型 { <ls_row>-mtart } 不存在於 T134| TYPE 'E'.
    ENDIF.
  ENDLOOP.
ENDFORM.
維護對話框的內表 TOTAL 第一個字元是「動作旗標」('N' 新增、'U' 修改、'D' 刪除、空白=未異動),資料本體從第 2 個字元起,所以上面才要用 <ls_total>+1 搭配 CASTING。直接把 TOTAL 當成 view 結構用會整個欄位位移。

7. 第四步:SE54 建 View Cluster(三層)與 SM34 執行

路徑:SE54 → 在初始畫面選 Edit View Cluster(維護視圖群組)→ 輸入名稱 ZMV_MARA_000 → Create。

  1. Object Structure(物件結構):依序把三個維護視圖登記進來
    • ZMV_MARA Position 1、Dep. = 上層(起始節點)、Predecessor 留空
    • ZMV_MARC Position 2、Predecessor = ZMV_MARA
    • ZMV_MARD Position 3、Predecessor = ZMV_MARC
  2. 每一列雙擊進 Field-Dependence(欄位相依):把上層傳下來的 Key 欄位對應好,並勾上前面說的 S。ZMARC 要對 MANDT + MATNR;ZMARD 要對 MANDT + MATNR + WERKS。系統自動抓的關聯鍵可能不準,要會自己調整。
  3. Header Entry:填 View Cluster 的說明文字。
  4. 存檔 → 指定 Package(跟三個 View 同一個 Package)→ 返回上一頁之後,要再按 ACTIVATE 鈕(老師特別提醒,很多人漏掉這一步,結果 SM34 打不開)。

執行:SM34 → 輸入 ZMV_MARA_000 → Maintain。應該看到左邊 Dialog Structure 樹狀,點 ZMARA 選一筆物料 → 雙擊左樹的「工廠」節點 → 只出現該物料的工廠資料 → 再往下就是儲存地點。

三層維護實作心得(老師的三條鐵律)

  1. 所有的 View 都要在同一個 Package。跨 Package 會在啟用或傳輸時出問題。
  2. SE54 讓系統自動抓關聯鍵時,可能不準,要會自己調整。自動推導靠的是外部索引鍵,外鍵設計不完整就會抓錯。
  3. 「S」設定在建 Maintenance View 與 Cluster View 時都要勾。

SE11 改良修正(6-03)

  • 執行結果應該是 MANDT 不必勾成 S。MANDT 是 client 欄位,系統自己會處理,勾成 S 反而會多出一個沒用的相依條件。
  • 有些欄位的抬頭(Column heading)不正確,這是資料元素的短文字問題。只能在 Screen Layout(畫面配置)上修飾 —— 進 SE51 或從 SE54 的產生畫面雙擊欄位,改 Text 屬性;或者回到 SE11 把 Data Element 的 Field Label(Short/Medium/Long/Heading)補齊,再重新產生一次維護對話框。
改完 Data Element 的欄位標籤後,維護對話框不會自動更新,要回 SE54 對該 view 執行「重新產生(Generated Objects → Change → 重新產生)」。如果你已經在產生的畫面上手工改過東西,重新產生會蓋掉,這也是為什麼自訂邏輯要寫在 Events 而不是寫在畫面裡。

8. 第五步:把 View Cluster 變成自己的 TCODE(參數交易)

SM34 每次都要輸入 View Cluster 名稱很不方便。SAP 標準的做法(OMT3、OVXM 這些交易就是這樣做出來的)是建一支參數交易(Parameter Transaction),直接跳過初始畫面。

操作:SE93 → 建立交易碼 ZMM_MARA3 → 選 Transaction with parameters (parameter transaction)

設定View Cluster(SM34)單一維護視圖(SM30)
TransactionSM34(或 VIEWCLUSTER)SM30
Skip initial screen勾選勾選
Default Values:欄位 1VCLDIR-VCLNAME = ZMV_MARA_000VIEWNAME = ZMV_MARA
Default Values:欄位 2UPDATE = XUPDATE = X
UPDATE = 'X' 代表進去就是「維護模式」(可改);不填就是顯示模式。這正是講義寫的 VCLDIR-VCLNAME zmv_mara_000 / UPDATE x 兩行的意思。

相關的系統表(想知道某個 TCODE 背後掛的是哪個 View Cluster,就查這幾張):

內容
VCLDIRView Cluster 目錄(VCLNAME=群組名稱)
VCLSTRUC / VCLSTRUCTView Cluster 的物件結構與階層關係
VCLMFView Cluster 的欄位相依設定
TVDIR維護對話框目錄:哪個 view 用哪個 Function Group、幾號畫面、一步還兩步
TSTC / TSTCP交易碼主檔/交易碼的參數(參數交易的預設值就存在 TSTCP-PARAM)

如果不想建 TCODE,也可以在自己的程式裡直接叫起維護畫面:

REPORT zmm_call_maint.

" (1) 叫起單一維護視圖 = 程式版的 SM30
CALL FUNCTION 'VIEW_MAINTENANCE_CALL'
  EXPORTING
    action                 = 'U'          " U = 維護(可改),S = 顯示
    view_name              = 'ZMV_MARA'
  EXCEPTIONS
    client_reference       = 1
    foreign_lock           = 2
    invalid_action         = 3
    no_clientindependent_auth = 4
    no_database_function   = 5
    no_editor_function     = 6
    no_show_auth           = 7
    no_tvdir_entry         = 8
    no_upd_auth            = 9
    only_show_auth         = 10
    permission_failure     = 11
    view_not_found         = 12
    OTHERS                 = 13.

IF sy-subrc <> 0.
  MESSAGE |呼叫維護畫面失敗,SUBRC = { sy-subrc }| TYPE 'E'.
ENDIF.

" (2) 叫起 View Cluster = 程式版的 SM34
CALL FUNCTION 'VIEWCLUSTER_MAINTENANCE_CALL'
  EXPORTING
    viewcluster_name = 'ZMV_MARA_000'
    maintenance_action = 'U'
  EXCEPTIONS
    client_reference          = 1
    foreign_lock              = 2
    viewcluster_not_found     = 3
    viewcluster_is_inconsistent = 4
    missing_generated_function = 5
    no_upd_auth               = 6
    no_show_auth              = 7
    object_not_found          = 8
    no_tvdir_entry            = 9
    no_clientindependent_auth = 10
    called_from_other_system  = 11
    OTHERS                    = 12.

IF sy-subrc <> 0.
  MESSAGE |呼叫 View Cluster 失敗,SUBRC = { sy-subrc }| TYPE 'E'.
ENDIF.
實務常用的第三支:VIEW_MAINTENANCE_SINGLE_ENTRY 可以直接跳到某一筆資料的明細畫面,做「從 ALV 雙擊 → 跳去維護該筆設定」很好用。

9. SE39:分割螢幕編輯器與程式比較

SE39ABAP Splitscreen Editor(分割螢幕編輯器):左右各開一支程式,同一個畫面對照著看、對照著改。做客製最常用在「客戶版程式 vs 標準程式」、「新版 vs 舊版」的比對。

  • 開啟:SE39 → 左欄輸入程式 A、右欄輸入程式 B → Display/Change。也可以從 SE38 的選單 Utilities → Splitscreen Editor 進去。
  • 比對:左右都載入之後按 Compare(比較)鈕,系統會標出差異行;Next difference / Previous difference 逐段跳。
  • 搬程式碼:選取左邊的段落 → 用剪下貼上直接放進右邊,做版本移植時很省事。
  • 版本管理:真正要看「同一支程式的不同版本」差異,用 SE38 → Utilities → Versions → Version Management,選兩個版本按 Compare;跨系統比對可以按 Remote comparison 指定 RFC 目的地,比對 DEV 與 PRD 的同一支程式。
  • Eclipse/ADT:在 ADT 裡是 Compare With → Another ABAP Object / Local History,功能更好用,但 SE39 在只有 SAP GUI 的客戶現場仍然是必備技能。
老師的用法:把「上課寫的版本」跟「自己回家改的版本」用 SE39 並排,一邊比一邊記,比單純重看程式有效得多。另外,做增強(User Exit/BAdI)前,先用 SE39 把標準程式跟自己要改的 Include 並排,可以避免改錯地方。

10. 查資料的兩把刀:SE16N 與 SE16H;以及怎麼由畫面欄位反查資料表

SE16 / SE16N / SE16H 的差別

TCODE特色什麼時候用
SE16最原始的 Data Browser,畫面陽春權限只給到 SE16 時
SE16NGeneral Table Display。可自選輸出欄位、可存 Variant、可直接輸出成 ALV/Excel、支援 &SAP_EDIT(多數 S/4 系統已鎖住或需 Debug 才能開)日常查資料的主力
SE16HHANA 版。多了 Group by/Aggregation(SUM、MAX、MIN、AVG、COUNT)HavingOrder by、多表 JOIN(Table Join 頁籤)、以及「Total」「Sort」欄位設定要做小計、分群、快速驗證統計數字時 —— 不用寫程式就能當一次性報表
SE16H 的隱藏技:勾 Outer Join 可以做左外接;Number of Hits 設 0 代表不限筆數(大表小心);輸入條件時用列舉(逗號分隔的多個單值)比用區間(BETWEEN)安全,因為某些欄位(例如版本、成本類別)在區間裡會夾進你不想要的值。
SE16N 的 &SAP_EDIT 直接改資料庫、不會走任何應用邏輯、不會寫變更文件,在生產機是重大事故來源。任何時候都不要在 PRD 用它「修資料」。

由畫面欄位反查資料表(四招)

  1. F1 → 技術資訊(Technical Information):把游標放在欄位上按 F1 → 按小鎚子(Technical Information)→ 會顯示 Table Name(畫面結構名)Field NameData ElementScreen fieldProgram name / Screen number。注意這裡看到的常常是畫面結構(例如 RM03M、VBAK 的 dialog structure),不一定是真正的資料庫表。
  2. 從 Data Element 反查:拿到 Data Element 之後,SE11 顯示該 Data Element → 按 Where-Used List(Ctrl+Shift+F3) → 勾 Table Fields → 就會列出所有用到這個資料元素的資料表。
  3. SE84(Repository Information System):ABAP Dictionary → Database Tables/Views,可以用名稱樣式、Package、Application Component 搜尋。例如想找 SD 的表就用 Application Component = SD 去撈。SE84 也可以反查「某個 Table 被哪些程式用到」。
  4. SDBE / ST05(追 SQL)
    • ST05 SQL Trace:Activate Trace → 去畫面做一次操作 → 回來 Deactivate TraceDisplay Trace。追出來的每一行都是真正打到 DB 的 SQL,表名一目瞭然。這是最可靠的一招。
    • SDBE Explain SQL Statement:把一段 SQL 貼進去看執行計畫(Explain Plan),用來判斷有沒有走到索引。ST05 的 trace 明細上按 Explain 也會轉到同樣的功能。

也可以用程式查字典。下面這段用 DDIC 表反查「哪些資料表含有某個欄位」:

REPORT zdd_find_table.

PARAMETERS: p_field TYPE dd03l-fieldname DEFAULT 'MATNR'.

" DD02L 資料表目錄 / DD03L 欄位 / DD02T 表說明 / DD04T 資料元素說明
SELECT t~tabname,
       t~tabclass,
       f~fieldname,
       f~keyflag,
       f~rollname,
       x~ddtext AS table_text
  FROM dd02l AS t
  INNER JOIN dd03l AS f ON f~tabname = t~tabname
  LEFT OUTER JOIN dd02t AS x ON x~tabname = t~tabname
                            AND x~ddlanguage = @sy-langu
  WHERE f~fieldname = @p_field
    AND t~tabclass  = 'TRANSP'          " 只看透明表
    AND t~as4local  = 'A'               " 只看已啟用版本
  ORDER BY t~tabname
  INTO TABLE @DATA(lt_tab).

IF lt_tab IS INITIAL.
  MESSAGE |找不到含欄位 { p_field } 的透明表| TYPE 'S' DISPLAY LIKE 'W'.
  RETURN.
ENDIF.

TRY.
    cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                            CHANGING  t_table      = lt_tab ).
    lo_alv->get_functions( )->set_all( ).
    lo_alv->get_columns( )->set_optimize( abap_true ).
    lo_alv->get_display_settings( )->set_list_header( |含有欄位 { p_field } 的資料表| ).
    lo_alv->display( ).
  CATCH cx_salv_msg INTO DATA(lx_salv).
    MESSAGE lx_salv->get_text( ) TYPE 'E'.
ENDTRY.

11. SAP 重要主數據表總覽(以及 S/4HANA 的相容視圖)

這是老師要求「一定要背」的部分。寫 ABAP 的人腦子裡要有這張地圖,看到需求才知道去哪撈資料。

物料主檔(MM)

層級 / 主鍵內容
MARAMATNR物料一般資料:MTART 物料類型、MATKL 物料群組、MEINS 基本單位、MBRSH 產業別
MAKTMATNR + SPRAS物料說明(多語言)。查中文要加 spras = @sy-langu
MARCMATNR + WERKS工廠層資料:DISMM MRP 類型、EKGRP 採購群組、BESKZ 採購類型、DZEIT 自製時間
MARDMATNR + WERKS + LGORT儲存地點庫存:LABST 未受限、INSME 品檢、SPEME 凍結
MBEWMATNR + BWKEY(+BWTAR)評價資料:VPRSV 價格控制(S/V)、STPRS 標準價、VERPR 移動平均價、LBKUM 總量、SALK3 總值
MVKEMATNR + VKORG + VTWEG銷售資料
MCHA / MCHB批次主檔 / 批次庫存有啟用批次管理才有
MARDH / MBEWH / MCHBH+ LFGJA + LFMON歷史庫存(期末)。查上月底庫存必用,但只有「該月有異動」才會產生歷史列
S/4HANA 的 MM-IM 簡化:MARD、MCHB、MSKA、MSKU、MSLB、MKOL、MSPR、MSTB、MSTE、MSTQ 等庫存量表,已經改成建立在 MATDOC / MATDOC_EXTRACT 之上的相容視圖(NSDM_V_MARD 之類)。它們仍然 SELECT 得動(相容視圖的用意就是讓舊程式不用改),但要注意:(1) 不可以直接 INSERT/UPDATE/MODIFY;(2) 拿它們當大量 JOIN 的主表效能會很差,S/4 的正解是改用對應的 CDS View(例如 I_MaterialStock、I_MaterialDocumentItem);(3) MARDH 這種歷史表在 S/4 一樣是由 MATDOC 推算,抓期末庫存要注意「無異動月份不產生資料」的老坑。

客戶主檔(SD/FI)—— XD99 大量維護

視角重要欄位
KNA1一般資料(General)KUNNR 客戶編號、NAME1 名字、NAME2 姓、LAND1 國別、SPRAS 語言、SORTL 搜尋字串、ERDAT 建檔日期、ERNAM 建檔人員
KNB1公司代碼(Company Code)KUNNR、BUKRS 公司代碼、PERNR 員工編號、AKONT 應收帳款統馭科目、ZTERM 付款條件、ERDAT、ERNAM
KNVV銷售範圍(Sales Area)KUNNR、VKORG 銷售組織、VTWEG 銷售通路、SPART 產品別、WAERS 幣別、ZTERM 付款條件、KTGRD 客戶科目分類群組(收入類別)、VWERK 工廠、INCO1 貿易條件、INCO2 條件地點、ERDAT、ERNAM
KNVK / KNVP / KNVH聯絡人 / 夥伴功能 / 客戶階層KNVP 的 PARVW 決定 SP(售達)、SH(送達)、BP(帳單)、PY(付款)
講義的練習:把 KNA1(共同資料)、KNB1(公司資料)、KNVV(銷售資料)合併在一個 ALV 上,這就是 XD99(客戶主檔大量維護)在做的事的簡化版。

廠商主檔(MM/FI)—— XK99 大量維護

視角重要欄位
LFA1一般資料LIFNR 廠商編號、NAME1、NAME2、LAND1、SPRAS、KTOKK 帳戶群組、ERDAT、ERNAM、KUNNR 對應客戶(同一法人同時是客戶也是廠商時填)
LFB1公司代碼LIFNR、BUKRS、PERNR、AKONT 應付帳款統馭科目、ZTERM、ERDAT、ERNAM
LFM1採購組織LIFNR、EKORG 採購組織、WAERS 幣別、ZTERM 付款條件、INCO1/INCO2、ERDAT、ERNAM
LFM2 / LFBK / LFBW採購資料(依產品別) / 銀行 / 扣繳
講義原文把 LFA1-KTOKK 標成「搜尋字串」、LFM1 第一欄寫成 KUNNR,這是抄寫時的筆誤。正確是:LFA1-KTOKK = 帳戶群組(Account Group),搜尋字串是 LFA1-SORTLLFM1 的主鍵是 LIFNR + EKORG,沒有 KUNNR 與 VTWEG。
S/4HANA 的最大變化:Business Partner(BP)強制。客戶與廠商一律先建 BP(TCODE BP),系統再透過 CVI(Customer/Vendor Integration)同步產生 KNA1/LFA1。相關表:BUT000(BP 一般資料)、BUT020(BP 地址)、BUT100(BP 角色)、CVI_CUST_LINK(BP ↔ KUNNR 對照)、CVI_VEND_LINK(BP ↔ LIFNR 對照)。XD01/XK01 這些舊交易在 S/4 已被停用(只剩顯示或直接不能用),程式如果要建客戶/廠商,要改呼叫 BP 的 API(例如 CVI_EI_INBOUND_MAIN 或 OData API_BUSINESS_PARTNER)。

銷售 / 出貨 / 開票(SD)

單據抬頭明細備註
銷售訂單 VA01/02/03VBAK(VBELN)VBAP(VBELN+POSNR)VBEP 交期排程、VBPA 夥伴、VBKD 商務資料
交貨單 VL01N/VL03NLIKPLIPSLIPS-VGBEL/VGPOS 回指銷售訂單
發票 VF01/VF03VBRKVBRPVBRP-AUBEL/AUPOS 回指銷售訂單
流程串接VBFA(單據流 Document Flow)VBELV 前置單據 → VBELN 後續單據,VBTYP_N 單據類別
S/4HANA:VBUK / VBUP(銷售單據狀態表)已被刪除,狀態欄位(GBSTK 總體狀態、LFSTK 交貨狀態、FKSTK 開票狀態等)併入 VBAK / VBAP 本身。舊程式若 SELECT VBUK 會直接語法錯誤,要改抓 VBAK。同理 LIKP/LIPS 的狀態併回 LIKP/LIPS。另外 KONV(條件明細)改名為 PRCD_ELEMENTS,KONV 只剩相容視圖。

採購(MM)

內容
EBAN採購申請 PR(BANFN + BNFPO)
EKKO採購單抬頭(EBELN):LIFNR 廠商、EKORG 採購組織、BSART 單據類型、BEDAT 單據日期
EKPO採購單明細(EBELN + EBELP):MATNR、WERKS、MENGE 數量、NETPR 單價、LOEKZ 刪除旗標、ELIKZ 交貨完成
EKET交期排程(Delivery Schedule)
EKBE採購單歷史:收貨(BEWTP='E')、發票(BEWTP='Q'),抓「PO 收了多少、付了多少」一定用它
EINA / EINE採購資訊記錄(一般 / 採購組織)

生產(PP)

內容
AUFK訂單主檔(AUFNR)—— 生產訂單、內部訂單共用
AFKO生產訂單抬頭(AUFNR + AUFPL 工序計畫)
AFPO生產訂單明細(產出物料、數量)
AFVC / AFVV工序(Operation)與其數量/時間
RESB預留(Reservation)= 生產訂單的用料清單
MAST / STKO / STPOBOM:物料↔BOM 對照 / BOM 抬頭 / BOM 明細
PLKO / PLPO / MAPL途程:抬頭 / 工序 / 物料途程對照

財務與庫存(FI / MM-IM)—— S/4 變動最大的區域

傳統表內容S/4HANA 現況
BKPF / BSEG會計憑證抬頭 / 明細仍然存在,但唯一真實來源是 ACDOCA(Universal Journal)。BSEG 只保留部分欄位角色
ACDOCA統一日記帳行項目FI + CO + AA + ML 合而為一。欄位:RBUKRS 公司代碼、GJAHR、BELNR、DOCLN、RACCT 會計科目、HSL/KSL/OSL 各幣別金額、RCNTR 成本中心、PRCTR 利潤中心、KOKRS、AUFNR、MATNR、WERKS、USNAM 輸入人員
BSIS/BSAS、BSID/BSAD、BSIK/BSAK總帳/客戶/廠商 未清+已清 索引表已改為相容視圖,資料實際來自 ACDOCA + BSEG。不要再拿它們做大量 JOIN
GLT0、FAGLFLEXA/T、COEP、COSS/COSP總帳/新總帳/CO 明細與彙總全部由 ACDOCA 取代,原表改為相容視圖
MKPF / MSEG物料憑證抬頭 / 明細MATDOC 取代;MKPF/MSEG 變成相容視圖(NSDM_V_MKPF / NSDM_V_MSEG)。不可寫入,且 MB5B 這類報表改讀 MATDOC
MATDOC物料憑證單一表同時扛「憑證明細 + 庫存彙總」,欄位極多(1000+),查詢請明列欄位

組織與共用表

內容
T001公司代碼(BUKRS、BUTXT、WAERS 本位幣、LAND1、KTOPL 科目表)
T001W工廠/銷售據點(WERKS、NAME1、地址)
T001L儲存地點(WERKS + LGORT)
T001K評價範圍(BWKEY),連到 MBEW
TVKO / TVTW / TSPA / TVKOV銷售組織 / 通路 / 產品別 / 銷售範圍
T024E / T024採購組織 / 採購群組
TKA01 / TKA02管控範圍(OKKP 維護的就是它)/ 公司代碼分派
TCURR / TCURC / TCURV / TCURF匯率/幣別/匯率類型/換算因子。TCURR 的 GDATU 是反轉日期(99999999 − YYYYMMDD),不能直接當日期比大小
T006 / T006A計量單位(內部 MEINS vs 外部 MSEHI,要用 CONVERSION_EXIT_CUNIT_OUTPUT 轉)
T134 / T023物料類型 / 物料群組
T000 / T002Client 清單 / 語言碼
TCURR 的三大坑:(1) GDATU 是反轉日期,要用 FM CONVERSION_EXIT_INVDT_INPUT/OUTPUT 轉;(2) 匯率有 FROM/TO 方向與 UKURS 正負號(負值代表分子分母顛倒);(3) 換算金額不要自己乘,用 FM CONVERT_TO_LOCAL_CURRENCY / CONVERT_TO_FOREIGN_CURRENCY,或 S/4 的 CURRENCY_CONVERSION SQL 函數,才會處理到 TCURF 的換算因子。

12. 使用者主檔:USR21 / ADRP / USR02 與 ZUSER 實作(ALV 版)

這是講義 7-01 的核心:以 USR21 為核心,往兩邊接。

關聯鍵重要欄位
USR21(使用者 ↔ 位址對照)核心表BNAME 用戶代碼、PERSNUMBER 員工(人員)編號、ADDRNUMBER 地址編號、KOSTL 成本中心、START_MENU 初始選單
ADRP(人員位址)USR21-PERSNUMBER = ADRP-PERSNUMBERNAME_FIRST 名字、NAME_LAST 姓氏、NAME_TEXT 全名、DATE_FROM/DATE_TO 有效期間
USR02(登入資料)USR21-BNAME = USR02-BNAMEANAME 建檔人員、TRDAT 最後登錄日、LTIME 最後登錄時、ERDAT 建檔日、UFLAG 鎖定旗標、CLASS 使用者群組
其他常用USR03 位址、ADR6 email、AGR_USERS 角色分派、UST04 Profile、USREFUS 參考使用者
ADRP 是時間相依的:同一個 PERSNUMBER 可能有多筆(不同 DATE_FROM/DATE_TO),還有 NATION 欄位(''=標準格式、'I'=國際格式、'K'=漢字格式)。原講義用 SELECT SINGLE 抓 ADRP,如果剛好有多筆就會抓到不確定的那一筆 —— 這正是「SELECT SINGLE 沒有排序保證」的經典陷阱。正解是加上 date_from <= @sy-datum AND date_to >= @sy-datumnation = @space

原講義寫法的問題(要看得懂、但不要照抄)

  • TABLES: USR02, USR21, ADRP. —— 只為了 SELECT-OPTIONS 的參照就宣告 TABLES,會產生同名的全域工作區,是舊寫法。改用 SELECT-OPTIONS s_bname FOR gv_bname 搭配 DATA gv_bname TYPE usr21-bname.
  • LOOP 裡對每一筆做兩次 SELECT SINGLE —— 典型的 N+1 查詢。1000 個使用者就打 2000 次 DB。正解是一次 JOIN,或先用 FOR ALL ENTRIES 撈進兩張 hash 內表再對照。
  • SELECT * ... INTO CORRESPONDING FIELDS OF TABLE —— 欄位靠名字硬對,欄位改名就默默出錯,而且傳整列浪費 IO。
  • ALV 版裡 i_callback_pf_status_set = 'STATUS_SET',但實際 FORM 叫 FORM_PF_STATUS_SET名字對不上,事件不會觸發。這是原程式的實際 Bug,抄的時候要注意。
  • lt_event 都填好了卻又同時傳 i_callback_*,兩套機制重複;REUSE_ALV_GRID_DISPLAY 在 S/4 已不建議新開發使用。

ZUSER 現代版(CL_SALV_TABLE + 一次 JOIN)

*&---------------------------------------------------------------------*
*& Report ZUSERS  —— 使用者清單(USR21 + USR02 + ADRP)
*& S/4HANA 現代寫法版本
*&---------------------------------------------------------------------*
REPORT zusers.

" 不用 TABLES:,只宣告參照用的變數
DATA: gv_bname TYPE usr21-bname.

SELECT-OPTIONS: s_bname FOR gv_bname.

TYPES: BEGIN OF ty_out,
         recn       TYPE i,                  "項次
         bname      TYPE usr21-bname,        "用戶帳號
         aname      TYPE usr02-aname,        "建檔人員
         trdat      TYPE usr02-trdat,        "最後登錄日
         ltime      TYPE usr02-ltime,        "最後登錄時
         uflag      TYPE usr02-uflag,        "鎖定旗標
         persnumber TYPE adrp-persnumber,    "人事編號
         name_last  TYPE adrp-name_last,     "姓
         name_first TYPE adrp-name_first,    "名
         kostl      TYPE usr21-kostl,        "成本中心
       END OF ty_out.

START-OF-SELECTION.

  " 一次 JOIN 解決,不要在迴圈裡 SELECT SINGLE(N+1 查詢)
  " ADRP 是時間相依,一定要加有效期間與 NATION 條件
  SELECT u21~bname,
         u02~aname,
         u02~trdat,
         u02~ltime,
         u02~uflag,
         u21~persnumber,
         p~name_last,
         p~name_first,
         u21~kostl
    FROM usr21 AS u21
    LEFT OUTER JOIN usr02 AS u02 ON u02~bname = u21~bname
    LEFT OUTER JOIN adrp  AS p   ON p~persnumber = u21~persnumber
                                AND p~nation     = @space
                                AND p~date_from <= @sy-datum
                                AND p~date_to   >= @sy-datum
    WHERE u21~bname IN @s_bname
    ORDER BY u21~bname
    INTO CORRESPONDING FIELDS OF TABLE @DATA(lt_out).

  IF lt_out IS INITIAL.
    MESSAGE '查無符合條件的使用者' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  " 補流水號:用 INDEX INTO 取代自己累加的計數器
  LOOP AT lt_out ASSIGNING FIELD-SYMBOL(<ls_out>).
    <ls_out>-recn = sy-tabix.
  ENDLOOP.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_out ).

      lo_alv->get_functions( )->set_all( ).                       "啟用全部工具列功能
      lo_alv->get_display_settings( )->set_striped_pattern( abap_true ). "斑馬紋
      lo_alv->get_display_settings( )->set_list_header(
        |ZUSERS 用戶清單列表 (CASPER) 來源:USR21 / USR02 / ADRP| ).
      lo_alv->get_columns( )->set_optimize( abap_true ).          "最佳欄寬

      " 逐欄設定中文抬頭 —— 取代舊版一大段 fieldcat
      DATA(lo_cols) = lo_alv->get_columns( ).

      LOOP AT VALUE stringtab( ( `RECN` ) ( `BNAME` ) ( `ANAME` ) ( `TRDAT` )
                               ( `LTIME` ) ( `UFLAG` ) ( `PERSNUMBER` )
                               ( `NAME_LAST` ) ( `NAME_FIRST` ) ( `KOSTL` ) )
           INTO DATA(lv_col).
        TRY.
            DATA(lo_col) = CAST cl_salv_column_table( lo_cols->get_column( CONV #( lv_col ) ) ).
            DATA(lv_text) = SWITCH string( lv_col
                              WHEN `RECN`       THEN `項次`
                              WHEN `BNAME`      THEN `用戶代碼`
                              WHEN `ANAME`      THEN `建檔人員`
                              WHEN `TRDAT`      THEN `最後登錄日`
                              WHEN `LTIME`      THEN `最後登錄時`
                              WHEN `UFLAG`      THEN `鎖定`
                              WHEN `PERSNUMBER` THEN `人事編號`
                              WHEN `NAME_LAST`  THEN `姓`
                              WHEN `NAME_FIRST` THEN `名`
                              WHEN `KOSTL`      THEN `成本中心`
                              ELSE lv_col ).
            lo_col->set_short_text( CONV #( lv_text ) ).
            lo_col->set_medium_text( CONV #( lv_text ) ).
            lo_col->set_long_text( CONV #( lv_text ) ).
          CATCH cx_salv_not_found.
            CONTINUE.
        ENDTRY.
      ENDLOOP.

      " 把 BNAME 設為 Key 欄位(會變色並釘在左邊)
      TRY.
          CAST cl_salv_column_table( lo_cols->get_column( 'BNAME' ) )->set_key( abap_true ).
        CATCH cx_salv_not_found.
      ENDTRY.

      lo_alv->display( ).

    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.
CL_SALV_TABLE 對照 REUSE_ALV_GRID_DISPLAY 的心智轉換:fieldcat → get_columns( )layout → get_display_settings( )it_sort → get_sorts( )PF-STATUS → get_functions( )TOP_OF_PAGE → set_top_of_list( cl_salv_form_layout_grid )USER_COMMAND → get_event( ) + SET HANDLER

13. ZUSER(WRITE 版)—— 老師要求「要會默寫」的那一支

還沒學 ALV 之前,先用 WRITE 版練基本功。這一支的重點是:SELECT → SORT → LOOP → WRITE → 統計筆數,把整個報表骨架背下來。以下是現代化改寫,語意跟原講義完全一致(篩選 BNAME LIKE 'E%')。

*&---------------------------------------------------------------------*
*& Report ZUSERS_WRITE —— WRITE 版(背骨架用)
*&---------------------------------------------------------------------*
REPORT zusers_write LINE-SIZE 160.

TYPES: BEGIN OF ty_out,
         bname      TYPE usr21-bname,
         aname      TYPE usr02-aname,
         persnumber TYPE adrp-persnumber,
         name_last  TYPE adrp-name_last,
         name_first TYPE adrp-name_first,
       END OF ty_out.

START-OF-SELECTION.

  " LIKE 'E%' 在 ABAP SQL 用 LIKE 'E%';跳脫字元用 ESCAPE
  SELECT u21~bname,
         u02~aname,
         u21~persnumber,
         p~name_last,
         p~name_first
    FROM usr21 AS u21
    LEFT OUTER JOIN usr02 AS u02 ON u02~bname = u21~bname
    LEFT OUTER JOIN adrp  AS p   ON p~persnumber = u21~persnumber
                                AND p~nation     = @space
    WHERE u21~bname LIKE 'E%'
    ORDER BY u21~bname
    INTO TABLE @DATA(lt_out).

  " 用 lines( ) 取代 DESCRIBE TABLE ... LINES
  DATA(lv_cnt) = lines( lt_out ).

  IF lv_cnt = 0.
    WRITE: / '查無資料'.
    RETURN.
  ENDIF.

  WRITE: /05 '項次',
          25 '員工帳號',
          50 '建檔人員',
          75 '員工編號',
         100 '姓',
         125 '名'.
  ULINE.

  LOOP AT lt_out INTO DATA(ls_out).
    WRITE: /05 sy-tabix,
            25 ls_out-bname,
            50 ls_out-aname,
            75 ls_out-persnumber,
           100 ls_out-name_last,
           125 ls_out-name_first.
  ENDLOOP.

  ULINE.
  WRITE: / |Record : { lv_cnt } 執行人員:{ sy-uname } 日期:{ sy-datum DATE = USER }|.
原程式的 DESCRIBE TABLE LT_OUTTAB LINES lv_cnt 放在 LOOP 之前、MODIFY LT_OUTTAB FROM WA_OUTTAB 放在 LOOP 之內,是舊式「先撈主表再回填」的寫法。這種寫法在資料量大時效能很差,而且 MODIFY ... FROM 依賴隱含的 sy-tabix,一旦中間加了 SORTDELETE 就會改到錯的列。改用 ASSIGNING FIELD-SYMBOL(<ls>) 直接就地修改,既快又安全。

14. 客戶(XD99)與廠商(XK99)三合一 ALV 練習

講義 7-02/7-03 的兩題練習,做法完全對稱。這裡各給一支範本。

*&---------------------------------------------------------------------*
*& Report ZSD_CUSTOMER_ALL —— 客戶三視角合併(KNA1 + KNB1 + KNVV)
*&---------------------------------------------------------------------*
REPORT zsd_customer_all.

DATA: gv_kunnr TYPE kna1-kunnr,
      gv_bukrs TYPE knb1-bukrs,
      gv_vkorg TYPE knvv-vkorg.

SELECT-OPTIONS: s_kunnr FOR gv_kunnr,
                s_bukrs FOR gv_bukrs,
                s_vkorg FOR gv_vkorg.

START-OF-SELECTION.

  " 一般 + 公司代碼 + 銷售範圍,一次三表 JOIN
  " 一個客戶可能有多家公司代碼、多個銷售範圍 → 結果會是笛卡兒展開,這是正常的
  SELECT a~kunnr,
         a~name1,
         a~name2,
         a~land1,
         a~spras,
         a~sortl,
         a~erdat,
         a~ernam,
         b~bukrs,
         b~akont,                    "應收帳款統馭科目
         b~zterm AS zterm_fi,
         v~vkorg,
         v~vtweg,
         v~spart,
         v~waers,
         v~zterm AS zterm_sd,
         v~ktgrd,                    "客戶科目分類群組
         v~vwerk,
         v~inco1,
         v~inco2
    FROM kna1 AS a
    LEFT OUTER JOIN knb1 AS b ON b~kunnr = a~kunnr
    LEFT OUTER JOIN knvv AS v ON v~kunnr = a~kunnr
    WHERE a~kunnr IN @s_kunnr
      AND ( b~bukrs IN @s_bukrs OR b~bukrs IS NULL )
      AND ( v~vkorg IN @s_vkorg OR v~vkorg IS NULL )
    ORDER BY a~kunnr, b~bukrs, v~vkorg, v~vtweg, v~spart
    INTO TABLE @DATA(lt_cust).

  IF lt_cust IS INITIAL.
    MESSAGE '查無客戶資料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_cust ).
      lo_alv->get_functions( )->set_all( ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->get_display_settings( )->set_list_header( '客戶主檔:一般 / 公司代碼 / 銷售範圍' ).
      lo_alv->display( ).
    CATCH cx_salv_msg INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.
*&---------------------------------------------------------------------*
*& Report ZMM_VENDOR_ALL —— 廠商三視角合併(LFA1 + LFB1 + LFM1)
*&---------------------------------------------------------------------*
REPORT zmm_vendor_all.

DATA: gv_lifnr TYPE lfa1-lifnr,
      gv_bukrs TYPE lfb1-bukrs,
      gv_ekorg TYPE lfm1-ekorg.

SELECT-OPTIONS: s_lifnr FOR gv_lifnr,
                s_bukrs FOR gv_bukrs,
                s_ekorg FOR gv_ekorg.

START-OF-SELECTION.

  SELECT a~lifnr,
         a~name1,
         a~name2,
         a~land1,
         a~spras,
         a~ktokk,                    "帳戶群組(不是搜尋字串!)
         a~sortl,                    "搜尋字串在這裡
         a~kunnr,                    "同一法人對應的客戶代號
         a~erdat,
         a~ernam,
         b~bukrs,
         b~akont,                    "應付帳款統馭科目
         b~zterm AS zterm_fi,
         m~ekorg,
         m~waers,
         m~zterm AS zterm_mm,
         m~inco1,
         m~inco2
    FROM lfa1 AS a
    LEFT OUTER JOIN lfb1 AS b ON b~lifnr = a~lifnr
    LEFT OUTER JOIN lfm1 AS m ON m~lifnr = a~lifnr
    WHERE a~lifnr IN @s_lifnr
      AND ( b~bukrs IN @s_bukrs OR b~bukrs IS NULL )
      AND ( m~ekorg IN @s_ekorg OR m~ekorg IS NULL )
    ORDER BY a~lifnr, b~bukrs, m~ekorg
    INTO TABLE @DATA(lt_vend).

  IF lt_vend IS INITIAL.
    MESSAGE '查無廠商資料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_vend ).
      lo_alv->get_functions( )->set_all( ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->get_display_settings( )->set_list_header( '廠商主檔:一般 / 公司代碼 / 採購組織' ).
      lo_alv->display( ).
    CATCH cx_salv_msg INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.
用 LEFT OUTER JOIN 時,WHERE b~bukrs IN @s_bukrs 會把「沒有公司代碼資料的客戶」整筆濾掉(因為 NULL 不符合任何條件),所以上面才寫成 ( b~bukrs IN @s_bukrs OR b~bukrs IS NULL )。這是 OUTER JOIN 最常見的坑。真的要嚴格篩選就改用 INNER JOIN。
員工、客戶、廠商三大主數據(講義 7-06)的記憶口訣:三者都是「一般資料 → 組織層資料 → 交易用資料」三段式。員工=USR21/ADRP/USR02(或 HR 的 PA0002/PA0001/PA0008),客戶=KNA1/KNB1/KNVV,廠商=LFA1/LFB1/LFM1。看懂一組就看懂三組。

15. Infotype 與 HR 資料表

什麼是 Infotype

Infotype(資訊類型)是把內容相似的資料分組管理的單位,用四位數字當代碼,例如 0001 組織分派、0002 個人資料。從技術面看,一個 Infotype 同時是一組資料分群、一個畫面、一張資料庫表、以及一個結構;它還帶著系統控制屬性,例如時間限制(Time Constraint)、追溯計算相關性等。

為什麼 HR 要用 Infotype?講義給了兩個理由:

  • Security(安全):HR 資料的敏感度差很多。地址、稅務資料員工可以自己看自己改;薪資、考評不行。切成不同 Infotype 才能對每一組資料套不同的權限設定 —— 誰能看、誰能改。
  • History(歷史):HR 資料是時間相依的。你現在的薪資是一個數字,下個月一號調薪之後是另一個數字;七月一日搬家。舊資料必須保留,Payroll 才知道今天該付多少、下個月該付多少;報表也才回答得出「YuLin 從到職到現在的薪資變化」。所以每一個 Infotype 記錄都有起始日(BEGDA)與結束日(ENDDA)
Infotype內容
0000Events(人事異動措施)
0001Org assignment(組織分派):PERNR、姓名、員工群組/次群組、職位、人事範圍/次範圍、成本中心
0002Personal info(個人資料)
0003Payroll data(薪資狀態)
0007Work time(計畫工時)
0221Adjustment(薪資調整)
1000–1999PD Relationship infotypes(組織管理的物件與關係)
Infotype 與內表的差別:Infotype 是一筆「記錄/畫面」,掛在員工或 PD 物件(職位、組織單位)上;資料表存的是這個 Infotype 的值與行為設定。HR 的主程式(邏輯資料庫 PNP/PNPCE)會在執行期自動幫每個 Infotype 產生對應的內表Pnnnn,例如 P0001),程式用 GET PERNR 事件搭配 INFOTYPES: 0001, 0002. 取用。
自我測驗(講義原題):Infotypes are accessed directly from Object Maintenance in — a. Simple Maintenance b. Organization & Staffing Interface c. Expert Mode d. Structure Graphics e. None.答案:c

HR 資料表命名規則

前綴內容
PA*Personnel Administration 的 Infotype 表,PA + 四位 Infotype 號PA0001(組織分派)、PA0002(個人資料)、PA0008(基本薪資)
HRP*Personnel Development / OM 的 Infotype 表HRP1000(物件)、HRP1001(關係)
PB*Recruitment(招募)的 Infotype 表PB0001
PCL*HR Cluster(薪資結果等,存成 cluster,要用 IMPORT FROM DATABASE 讀)PCL1、PCL2
PCERTPayroll posting runs(薪資過帳執行)
CATSDBCATS 工時登錄PTEX2000 是 CATS 轉 Infotype 2001/2002 的介面表

常用 TCODE:PA20 顯示人事主檔、PA30 維護人事主檔、PA40 人事措施(Personnel Actions,到職/調動/離職就是走這裡,會依序帶出一連串 Infotype 畫面)、PPOME 組織與職位維護、PPOSE 顯示組織架構、PP01 維護 PD 物件。

講義 9-2 的題目:取得 PPOME 的輸入內容 → HRP1001。現代寫法:

*&---------------------------------------------------------------------*
*& Report ZHR_HRP1001 —— 讀組織管理關係表 HRP1001 並以 SALV 顯示
*&---------------------------------------------------------------------*
REPORT zhr_hrp1001.

DATA: gv_objid TYPE hrp1001-objid.

PARAMETERS:     p_plvar TYPE hrp1001-plvar DEFAULT '01',  "計畫版本,01=現行
                p_otype TYPE hrp1001-otype DEFAULT 'O'.   "物件類型 O=組織單位 S=職位 P=人員
SELECT-OPTIONS: s_objid FOR gv_objid,
                s_uname FOR gv_objid NO INTERVALS.        "維護人員(示意)

START-OF-SELECTION.

  " 明列欄位、用 @ 逸出、加上有效期間;不要 SELECT *
  SELECT plvar,          "計畫版本
         otype,          "物件類型
         objid,          "物件編號
         subty AS relat, "關係代碼(A/B + 三位)
         istat,          "規劃狀態
         begda,
         endda,
         sclas,          "對方物件類型
         sobid,          "對方物件編號
         uname,          "最後維護人員
         aedtm           "最後維護日期
    FROM hrp1001
    WHERE plvar = @p_plvar
      AND otype = @p_otype
      AND objid IN @s_objid
      AND begda <= @sy-datum
      AND endda >= @sy-datum
    ORDER BY objid, subty, sobid
    INTO TABLE @DATA(lt_rel).

  IF lt_rel IS INITIAL.
    MESSAGE '查無組織關係資料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_rel ).
      lo_alv->get_functions( )->set_all( ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->get_display_settings( )->set_list_header( 'HRP1001 組織管理關係(PPOME 的資料來源)' ).
      lo_alv->display( ).
    CATCH cx_salv_msg INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.
原講義寫 SELECT * INTO TABLE gt_hrp1001 FROM hrp1001 WHERE hrp1001~uname = 'PU407':(1) 沒有 @ 逸出(S/4 的嚴格模式會警告或報錯);(2) SELECT * 撈整張寬表;(3) 沒有帶 PLVAR / BEGDA / ENDDA,HRP1001 是時間相依表,不帶期間會撈到所有歷史版本。OM 的表一定要帶 PLVAR + 有效期間。
HRP1001-SUBTY 就是關係代碼:A002/B002 報告關係、A003/B003 隸屬、A008/B008 職位↔人員、A012/B012 主管職位。A 是「由下往上」,B 是「由上往下」,同一組關係在表裡會存成兩筆互為反向。

16. 開發命名規則釋例

這是一份實際專案的開發準則案例,僅供參考;到職場之後仍需依公司的規範。命名的四個原則:簡易快捷且唯一、符合 SAP 客戶化命名空間、盡可能短、有意義;少用特殊字元(底線除外);如果是複製 SAP 標準物件,請在前面加 "Z_"

Repository 物件

物件範例長度規則
Program / ReportZFIR000181=Z;2-3=模組簡寫;4=程式類型;5-8=序號
Transaction CodeZFIR00018同 Program
Function GroupZFI_CHECK_DOCNUM261=Z;2-3=模組;4 起底線+有意義名稱
Function ModuleZFI_ASK_FOR_CONFIRMATION30同上
Package / Development ClassZDEV_FI301=Z;2-4=DEV;5-7=模組
Authorization ObjectZMM_INVENT101=Z;2-3=模組;其後自由但唯一
Enhancement ProjectZMM000181=Z;2-3=模組;5-8=序號
SmartForms / SAPscriptZMMF_INVOICE301=Z;2-3=模組;4=F;5-30 自由
Table / View / StructureZSD_BILLING_RANGES161=Z;2-3=模組;5-16 自由。View 加 _V 後綴
DomainZCUSTTYPE30Z 開頭
Data ElementZCUSTYPE30Z 開頭
Type GroupZCUST5Z 開頭
FieldCUSTTYPE16自訂欄位加 Z;SAP 既有欄位沿用不加
Search HelpZSH_CUST301=Z;2-3=SH;其後自由
Lock ObjectEZTCUST161=E;其後接物件名(通常是表或 view 名)
Append StructureZZVBAK(append 到 VBAK)161-2=ZZ
Custom BAdIZMM_CUSTOMER_DATA201=Z;2-3=模組
BAdI ImplementationZMM_CUSTOMER_DATA_0120最好包含 BAdI 名稱
BAdI MethodGetSalesOrder30自由但要描述功能
Message IDZSD01201=Z;2-3=模組;4-5=序號
Area Menu(PF-Status)S001201=S;2-4 自由
IDoc Type / Message Type / Process CodeZFIDELV001、ZSDBILLING301=Z;2-3=模組
Segment Definition / DocumentationZSDORDER01 / Z1KORDER0127Z 開頭+模組

程式類型代碼(Program Type,命名第 4 碼)

代碼說明代碼說明
FSmartform / ScriptformMModule Pool
RReportBBDC
UUtility ProgramECustomer Enhancement
CConversionIInterface

SAP 模組代碼

代碼模組代碼模組
XACross ApplicationPSProject System
FIFinanceHRHuman Resource / Payroll
COControllingBWBusiness Warehouse
LOLogistics GeneralSMStrategic Enterprise Mgmt
MMMaterials ManagementBSBasis related program
WMWarehouse ManagementSVService Management
SDSales and DistributionPLProduct Lifecycle Management
PPProduction PlanningCRCRM
QMQuality ManagementAMAsset Management
PMPlant MaintenanceEPEnterprise Portal development

程式內變數命名

種類前綴範例
Constant 常數CNS_CNS_BASE_CLIENT
Types 自訂型別TY_TY_DATA
Work Variable 工作變數W_W_TEMP_SALARY_NB、W_LAST_UPDATE_DT
Internal Table 全域內表IT_IT_EKKO
Internal Table 區域內表LI_LI_EKKO
Work Area 工作區WA_WA_EKKO
Screen ElementRB_ / PB_ / CB_Radio Button / Push Button / Check Box
Parameter(單值輸入,≤8 字元)P_P_CUSTNM、P_FISCYR
Select-Option(區間輸入,≤8 字元)S_S_LIFNR、S_BUKRS
Range(≤8 字元)R_R_LIFNR、R_BUKRS
Subroutine 子程式FRM_PERFORM FRM_GET_EKKO
System FieldSY-SY-DATUM、SY-UNAME、SY-SUBRC
RANGES 語句會建立一張選擇表(欄位為 SIGN/OPTION/LOW/HIGH),可以在程式中自行填值後直接用在 WHERE ... IN。現代寫法建議改用 TYPES ty_r_lifnr TYPE RANGE OF lifnr. 搭配 VALUE #( ( sign = 'I' option = 'EQ' low = '0000100001' ) ),比 RANGES 更清楚且可當方法參數傳遞。

訊息與傳輸請求

Message Type意義
AAbend —— 取消資訊(程式直接中止)
EError —— 錯誤資訊
WWarning —— 警告資訊
IInformational —— 訊息視窗
SSuccess —— 成功資訊(顯示在下一個畫面的狀態列)

Message Number 為 3 位數字,000~999。傳輸請求(Task / Request)的描述格式:「YYMMDD:程式名稱代碼:修改或新增主要描述」,最長 60 字元,例如 060720:ZMMR0001:增加物料類型條件。實務上仍應以 Basis 顧問提供的 CR 描述規定為準。

17. 總結練習:從零做出一整套(第一節的驗收題)

講義開宗明義給的練習順序,是把 1-2 章的資料字典與這一章的維護畫面串起來的完整流程:

  1. Domain(域)—— 定資料型態、長度、值範圍(Value Range/Fixed Values)
  2. Data Element(資料元素)—— 掛 Domain,定四種欄位標籤與 F1 說明文件
  3. Master Table(主檔表)—— 例如 Z00_KNA1
  4. Transaction Table(交易表)—— 例如 Z00_VBAK
  5. Foreign Key(外部索引鍵)—— 把交易表接回主檔表
  6. Search Help(搜尋說明)—— 掛在欄位或 Data Element 上,F4 才有東西
  7. Maintenance View(維護視圖)+ SE54 產生維護畫面

驗收成果:用 SM30 打開 Z00_VBAK,能新增、修改、刪除,且外鍵欄位按 F4 會跳出正確的搜尋說明。

做完之後,可以寫一支小程式驗證資料的參照完整性(下層有、上層沒有的孤兒資料):

*&---------------------------------------------------------------------*
*& Report ZMM_CHECK_ORPHAN —— 檢查三層表的參照完整性
*&---------------------------------------------------------------------*
REPORT zmm_check_orphan.

" 先把上層 Key 撈成 HASHED,查找 O(1)
SELECT matnr FROM zmara
  INTO TABLE @DATA(lt_mara).

DATA lt_mara_key TYPE HASHED TABLE OF zmara-matnr WITH UNIQUE KEY table_line.
lt_mara_key = VALUE #( FOR <m> IN lt_mara ( <m>-matnr ) ).

SELECT matnr, werks FROM zmarc
  ORDER BY matnr, werks
  INTO TABLE @DATA(lt_marc).

DATA lt_bad TYPE TABLE OF string.

LOOP AT lt_marc INTO DATA(ls_marc).
  " line_exists 判斷內表是否有該筆,取代 READ TABLE ... TRANSPORTING NO FIELDS + sy-subrc
  IF NOT line_exists( lt_mara_key[ table_line = ls_marc-matnr ] ).
    APPEND |ZMARC 有孤兒資料:MATNR = { ls_marc-matnr },WERKS = { ls_marc-werks }| TO lt_bad.
  ENDIF.
ENDLOOP.

IF lt_bad IS INITIAL.
  MESSAGE '參照完整性檢查通過' TYPE 'S'.
ELSE.
  LOOP AT lt_bad INTO DATA(lv_msg).
    WRITE: / lv_msg.
  ENDLOOP.
  WRITE: / |共 { lines( lt_bad ) } 筆異常 —— 表示外部索引鍵沒有設好,或有人繞過維護畫面直接改資料庫|.
ENDIF.
FOR ALL ENTRIES 空表陷阱:如果上面改用 SELECT ... FOR ALL ENTRIES IN lt_mara,而 lt_mara 是空的,SAP 會忽略整個 WHERE 條件把全表撈回來。任何 FOR ALL ENTRIES 之前一定要先 IF lt_x IS INITIAL. RETURN. ENDIF.,並且對驅動內表先做 SORT + DELETE ADJACENT DUPLICATES

練習與自我檢核

  1. 照著 SE11 建出 ZMARA / ZMARC / ZMARD 三張表,並設定 ZMARC → ZMARA(MANDT、MATNR)與 ZMARD → ZMARC(MANDT、MATNR、WERKS)的外部索引鍵。提示:Cardinality 選 1:CN。
  2. 建三個維護視圖 ZMV_MARA / ZMV_MARC / ZMV_MARD,全部放在同一個 Package,用 SE54 各產生一次維護對話框。提示:Function Group 用自己的 Z 開頭,不要用 TMP。
  3. 建 View Cluster ZMV_MARA_000,用 SM34 執行,做到「選一個物料 → 只看到該物料的工廠 → 只看到該工廠的儲存地點」。提示:Field-Dependence 要勾 S,MANDT 不用勾 S;存檔返回後記得按 Activate。
  4. 用 SE93 把 ZMV_MARA_000 包成一支自己的 TCODE。答案要點:參數交易,Transaction = SM34、勾 Skip initial screen、VCLDIR-VCLNAME = ZMV_MARA_000、UPDATE = X。
  5. 在 ZMV_MARA 的維護對話框掛上 Event 21,讓 ERSDA/ERNAM 自動帶值;再掛 Event 01,檢核 MTART 必須存在於 T134。提示:路徑是 SE54 → Environment → Modification → Events。
  6. 去 NACE、OKKP、OMT3 各點一遍,說出它們各有幾層、每一層對應哪張表。提示:用 SE93 看 TCODE 的參數,再查 VCLDIR / VCLSTRUC。
  7. 寫 ZUSERS(WRITE 版),輸出項次/員工帳號/建檔人員/員工編號/姓/名,最後印總筆數與執行者。要求:不用 TABLES、不用 SELECT SINGLE 在迴圈裡、用 lines( ) 取代 DESCRIBE。
  8. 把上題改寫成 CL_SALV_TABLE 版本,加上中文欄位抬頭、斑馬紋、最佳欄寬、清單抬頭。提示:get_columns( ) / get_display_settings( ) / get_functions( )。
  9. 做 XD99 練習:把 KNA1 + KNB1 + KNVV 三視角合併在同一個 ALV。陷阱:LEFT OUTER JOIN 加 WHERE 會濾掉 NULL 那一側。
  10. 做 XK99 練習:把 LFA1 + LFB1 + LFM1 合併在同一個 ALV。順便回答:LFA1-KTOKK 是什麼?搜尋字串是哪個欄位?(答:帳戶群組;SORTL)
  11. 用 F1 → 技術資訊查出 VA03 畫面上「銷售訂單號碼」欄位的 Data Element,再用 Where-Used List 找出所有含此資料元素的資料表。答案要點:Data Element = VBELN_VA,會找到 VBAK、VBAP、VBFA 等。
  12. 用 ST05 追一次 ME23N 的顯示動作,說出它讀了哪些表;再用 SE16H 對 EKPO 做 Group by LIFNR 的金額小計。
  13. 說明在 S/4HANA 中,下列各項的現況:MSEG、KONV、VBUK、MARD、BSIS、COEP。答案要點:分別對應 MATDOC、PRCD_ELEMENTS、併入 VBAK、NSDM 相容視圖、ACDOCA 相容視圖、ACDOCA。
  14. 用 SE39 把「自己寫的 ZUSERS」與「老師版 ZUSERS」並排比較,列出三個你改掉的地方與理由。
  15. Infotype 為什麼要切這麼細?用「安全」與「歷史」兩個角度各舉一個例子。再說明 PA0001 與 HRP1001 的差別。
  16. 依命名規則,替下列需求取名:一支 MM 的 BDC 程式、一個 SD 的搜尋說明、一個 append 到 EKPO 的結構、一個 FI 的 Function Group。參考答案:ZMMB0001、ZSH_xxx、ZZEKPO、ZFI_xxxx。

第 1-4 章 不寫程式的報表開發:SQVI QuickViewer / SQ01 SQ02 SQ03 InfoSet Query / SQ99

這一章的教學重點只有一句話:在不寫一行程式的狀況下,完成二階(多表聯結)報表的查詢。你會學到 SQVI 快速檢視器(三分鐘出報表、但只屬於你自己),以及 SQ03 使用者群組 → SQ02 InfoSet → SQ01 Query 這套「三件套」的正確建立順序、資料來源怎麼選、附加欄位怎麼補、產生出來的 AQ 程式名怎麼解碼、怎麼掛成 TCODE、怎麼傳輸到 QAS/PRD,最後是什麼時候該放棄 Query 直接寫 ABAP,以及 S/4HANA 時代改用 CDS View + Fiori Embedded Analytics 的時機。老師說這門課三小時可以教完,但它能幫你擋掉一半以上的臨時報表需求。

核心 TCODESQVISQ00SQ01SQ02SQ03SQ07SE93SE38SE16HSE16NSU01SU3SE37GRR1GRR2GR31FGI0
關鍵物件KNA1、KNB1、KNVV、VBAK、VBAP、QALS、LIKP、LIPS、START_REPORT、D_SREPOVARI、RSAQ_DECODE_REPORT_NAME、RSAQ_QUERY_CALL、RSAQR3TR、AQ* 產生程式、CL_SALV_TABLE、CDS View

1. 三層工具的定位:SE16H → SQVI → SAP Query → 自己寫程式

使用者丟一個報表需求過來,先問自己「非得寫程式不可嗎?」。SAP 給了一整排不寫程式的工具,成本由低到高排序如下:

層級工具能做什麼不能做什麼
0SE16N / SE16H單表瀏覽;SE16H 在 HANA 上可做群組加總、排序、甚至多表聯結沒有固定版面、不能給一般使用者當正式報表、不易掛 TCODE
1SQVI QuickViewer個人用的多表聯結報表,ALV 輸出,五分鐘完成只屬於建立者本人、無法傳輸、無法給別人執行
2SQ03SQ02SQ01 SAP Query可分享、可授權、可掛 TCODE、可傳輸;支援附加欄位與 ABAP 程式碼區段複雜邏輯、效能調校、單元測試、對外介面都很吃力
3SE38 自己寫 ABAP什麼都能做成本最高,要走完整開發與傳輸流程
3+CDS View + Fiori(S/4HANA)可重用、可 OData 曝露、可進 Analytical List Page/Embedded Analytics需要 Eclipse ADT 與較完整的建模概念
老師的三小時總結:「教學重點:在不寫程式的狀況下,完成二階報表的查詢。」所謂「二階」就是主檔+子檔兩張表聯結,例如 KNA1+KNB1、VBAK+KNA1、VBAP+QALS。這正是 90% 臨時報表需求的形狀。
講義建議的練習命名原則(把同一個需求用六種工具做一遍,名字就看得出來是誰做的):
SE16H → ZERP###H;SQVI → ZERP###V;SQ03 使用者群組 → ZERP###U;SQ02 InfoSet → ZERP###I;SQ01 Query → ZERP###Q;SE38+TCODE → ZERP###P;START_REPORT 參數交易 → ZERP###A

2. SQVI QuickViewer:最快的個人報表(但不可傳輸)

SQVI 是 SAP Query 的「單人簡化版」。它不需要使用者群組、不需要 InfoSet,一個畫面內就把「選哪些表、怎麼聯結、要哪些欄位、要哪些選擇條件」全部做完。(依講義步驟整理)

  1. 進入 SQVI,輸入 QuickView 名稱(最長 14 碼,建議 ZERP###V),按「建立」。
  2. 輸入抬頭說明(Title)、備註,選擇資料來源
    • 表格(Table):只查一張表,最單純。
    • 表格聯結(Table join):多表聯結,二階報表就選這個。
    • 邏輯資料庫(Logical database):沿用 SAP 標準的取數樹(含權限檢查)。
    • InfoSet:直接沿用 SQ02 已經做好的 InfoSet。
  3. 選擇「表格聯結」後進入 Join 畫面,按「插入表格」加入第二張表;SAP 會依外鍵自動連線,務必檢查自動連上的欄位是否正確,多餘的連線用滑鼠右鍵刪除。
  4. 回到基本清單(Basis mode)畫面,左邊是欄位群組樹,勾選 清單欄位(List fields)選擇欄位(Selection fields)
  5. 存檔後按 執行(F8),出現選擇畫面 → 再 F8 → ALV 報表。
SQVI 的三個硬限制,一定要先跟使用者講清楚:
1. QuickView 是使用者私有的,別人登入看不到,也不能執行。
2. 無法掛傳輸請求,DEV 做好不會自己跑到 QAS/PRD;老師的說法是「一般是不傳輸,直接就地使用」。
3. 不支援附加欄位的 ABAP 程式碼區段(那是 SQ02 InfoSet 才有的功能)。
要正式化,就在 SQ01 用「Query → 由 QuickView 轉換(Convert QuickView)」把它升級成 Query,之後才有使用者群組、授權與傳輸。
SQVI 的定位是「可取代 SQ01/SQ02/SQ03 的個人版」。自己查資料、驗證 Join 是否正確時用 SQVI 最快;確定邏輯 OK 再花時間做成 Query。

講義第 11 節的實作是 SQVI 品質檢驗:把 VBAP(銷售訂單行項目)與 QALS(檢驗批記錄)聯結,做出「待出貨訂單對應成品檢驗狀態」的清單。Join 條件通常走物料與工廠(VBAP-MATNR = QALS-MATNR、VBAP-WERKS = QALS-WERK),必要時再加交貨單(QALS 的檢驗批來源可為交貨 LIKP/LIPS)。(依講義步驟整理)

3. SQ03 使用者群組:三件套的第一步

SAP Query 的建立順序是固定的,講義特別提醒:「設計之前,要先將 INFOSET、USER_GROUP 指定正確。」

順序TCODE物件做什麼
1SQ03使用者群組 User Group決定「誰可以用這組報表」,並把 InfoSet 掛進來
2SQ02InfoSet決定「資料從哪來、有哪些欄位」(Join/LDB/程式)
3SQ01Query 查詢決定「報表長什麼樣子」(選擇欄位、輸出欄位、小計、排序)
4SQ00執行SQ00 與 SQ01 畫面幾乎一樣,SQ00 定位是「執行」、SQ01 是「維護」

SQ03 建立步驟(依講義步驟整理)

  1. SQ03,先看畫面上方的查詢區(Query Area)是不是你要的(見下一節),必要時用「環境 → 查詢區」切換。
  2. 輸入使用者群組名稱(講義用 ZCERPSZVBAP_QALSZVBAK_KNA1Z000),按「建立」,輸入說明文字後存檔。
  3. 「指定使用者與 InfoSet」
    • 指定使用者:把要執行報表的 SAP 帳號加進來。有勾「Change authorization」的人才能改 Query。
    • 指定 InfoSet:把 SQ02 做好的 InfoSet 打勾掛進這個群組(InfoSet 還沒建時先跳過,建完再回來掛)。
講義的三個實作使用者群組:ZCERPS(對應 Query QUE_KNV1)、ZVBAP_QALS(出貨檢驗)、ZVBAK_KNA1 / Z000(訂單主檔,對應 Query QUE_VBAK_KNA1)。

4. 查詢區 Standard Area 與 Global Area:能不能傳輸的關鍵

SAP Query 的所有物件(使用者群組、InfoSet、Query)都存在某個查詢區裡,這決定了它的可見範圍與傳輸方式。講義第 01-3 節的「AREA 群組範圍」講的就是這件事。

查詢區代碼範圍傳輸方式
標準區 Standard Area(空白)依 Client 分離,只在建立它的 client 看得到;不屬於任何 Package不會自動進傳輸請求,要用程式 RSAQR3TR 匯出/匯入
全域區 Global AreaG跨 Client,整個系統共用屬於 Package、可掛傳輸請求,像一般開發物件一樣走 STMS
要讓報表跟著傳輸走,就把使用者群組/InfoSet/Query 建在全域區。這也是為什麼講義的 START_REPORT 參數裡,使用者群組後面第 13 碼是一個 G:那個 G 就是在告訴系統「這個 Query 在全域區」。

如何切換查詢區:在 SQ01/SQ02/SQ03 任一畫面,選單「環境(Environment) → 查詢區(Query Areas)」,選「標準區」或「全域區」。切錯區最典型的症狀是「明明建好了卻在清單裡找不到」

SU01 參數 AQW(講義 10-1):在 SU01(或使用者自己用 SU3)的「參數」頁籤加一筆參數 ID AQW,就能指定該使用者預設進入的查詢區;填 G 代表預設進全域區。講義的說法是「maintain AQW parameter in SU01 for required user ID and SAVE,This will enable that particular User to see all ABAP Queries」——實際效果是讓該使用者一登入 SQ01 就看到全域區的 Query,不必每次手動切換。

標準區的 Query 是 client-specific:你在 DEV 100 建的,DEV 200 看不到,PRD 更看不到。實務上很多「報表不見了」的求救單,答案都是查詢區或 client 不對。跨系統搬運標準區 Query 要跑 RSAQR3TR(SE38 執行,選 Export/Import,會產生 transport dataset)。

5. SQ02 InfoSet:資料來源怎麼選

InfoSet 是三件套裡最重要、也最需要動腦的一層。它回答兩個問題:資料從哪裡來使用者可以看到哪些欄位

建立 InfoSet 時,SAP 會要你四選一:

資料來源說明什麼時候用
表格聯結 Table join over table自己拉 DDIC 表格做 Join(可設 inner/left outer join)最常用。二階報表、主檔+子檔都走這個
邏輯資料庫 Logical database沿用 SAP 標準的取數程式樹(例如 SD 的 VAV、MM 的 MEPOLDB、HR 的 PNP),自帶選擇畫面與權限檢查需要標準權限檢查、或資料結構是階層式(抬頭→項目→排程行)時
使用程式讀取的表格 Table + read program指定一張表,再自己寫取數程式填值資料要先加工才進表的情況
資料檢索程式 Data retrieval program(Sequential dataset)完全自己寫 ABAP 取數,把資料丟進定義好的結構來源是檔案、外部系統、或邏輯太複雜時

SQ02 建立步驟(依講義步驟整理)

  1. SQ02,確認查詢區,輸入 InfoSet 名稱(講義:ZKNA1_KNB1ZVBAP_QALSZVBAK_KNA1),按「建立」。
  2. 輸入說明文字,選「表格聯結」並填第一張表(主表),Enter。
  3. 在 Join 畫面按「插入表格」加入第二張表;SAP 依外鍵自動畫連線,手動核對每一條連線;按右鍵可切換 Inner/Left Outer Join。
  4. 回上一頁時,SAP 問「欄位群組(Field Groups)」要怎麼建,通常選 「每個表格一個欄位群組,只含清單欄位/關鍵欄位」 或「包含所有表格欄位」。欄位群組決定使用者在 SQ01 看到的欄位分類。
  5. 把需要的欄位從右邊的表格樹拖進左邊的欄位群組(沒有拖進去的欄位,SQ01 就選不到)。
  6. 存檔 → 產生(Generate,Ctrl+F3)沒有按產生,Query 端不會看到你的修改。
  7. 按「角色/使用者群組指派」,把這個 InfoSet 掛給 SQ03 建好的使用者群組。
口訣:「InfoSet 改完一定要 Generate;Generate 完一定要回頭看 Query 有沒有壞掉。」InfoSet 改欄位群組或刪欄位,會讓已經在用該欄位的 Query 出現「欄位不存在」的錯誤。

講義的三個 InfoSet 實作:

  • ZKNA1_KNB1(02-1):客戶一般資料 KNA1 + 公司代碼資料 KNB1,Join 條件 KNA1-KUNNR = KNB1-KUNNR。用來查「客戶編號與帳號(統馭科目 AKONT)關聯」。
  • ZVBAP_QALS(02-2):銷售訂單行項目 VBAP + 檢驗批 QALS,用來查「待出貨訂單與成品檢驗」。
  • ZVBAK_KNA1(02-3):銷售訂單抬頭 VBAK + 客戶主檔 KNA1,Join 條件 VBAK-KUNNR = KNA1-KUNNR,即「訂單主檔+客戶名稱」。

6. Join 的陷阱:Inner Join、Left Outer Join 與效能

InfoSet 的 Join 就是 ABAP SQL 的 Join,所有 SQL 的坑它一個都不會少。以下三個是最常踩的。

坑一:該用 Left Outer 卻用了 Inner,資料整批消失。例如 VBAP + QALS,如果某些訂單行項目還沒有檢驗批,Inner Join 會把它們整批濾掉,使用者就會說「我的訂單少了一半」。

" 用 ABAP SQL 表達 InfoSet 裡那條 Join:VBAP + QALS(出貨檢驗)
" Left Outer Join:沒有檢驗批的訂單行項目也要留下來
SELECT a~vbeln,
       a~posnr,
       a~matnr,
       a~werks,
       a~kwmeng,
       a~vrkme,
       b~prueflos,          " 檢驗批號
       b~herkunft,          " 檢驗批來源(01 進料、03 生產、04 交貨…)
       b~stat34,            " 使用決策已作成
       b~lmengezu           " 允收數量
  FROM vbap AS a
  LEFT OUTER JOIN qals AS b
    ON  b~matnr = a~matnr
    AND b~werk  = a~werks
 WHERE a~vbeln IN @s_vbeln
   AND a~matnr IN @s_matnr
  INTO TABLE @DATA(lt_out).

IF lt_out IS INITIAL.
  MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'W'.
  RETURN.
ENDIF.
Left Outer Join 的 WHERE 條件位置很關鍵。如果把「b~stat34 = 'X'」寫在 WHERE,Left Outer 會退化成 Inner Join(因為 NULL 不符合條件);要保留左表全部資料,條件必須寫在 ON 子句裡。SQ02 的 Join 畫面沒有辦法漂亮地表達這件事,這正是 Query 做不下去、該改寫 ABAP 的訊號之一。

坑二:Join 太多張表,HANA 上照樣慢。InfoSet 裡拉五、六張表很容易,但每多一張表就是一次 Join。實務建議:主表加 1~2 張參照表就好,文字(MAKT、T001W 之類)盡量用「附加表格」在 RECORD PROCESSING 裡單筆讀,或改用有內建文字的 CDS View。

坑三:沒有給有索引的選擇欄位。Query 的選擇畫面是使用者自己填的,如果沒有把 MANDT 以外的主鍵/索引欄位放進選擇欄位,使用者就會用一個沒有索引的欄位掃全表。做 InfoSet 時,一定要把主表的關鍵欄位(VBELN、MATNR、KUNNR、WERKS、日期)都放進欄位群組。

7. 附加欄位 Additional Field 與 InfoSet 的 ABAP 程式碼區段

InfoSet 比 SQVI 強大的地方,就在於它可以加欄位、寫程式。在 SQ02 的 Join/欄位畫面,選單「附加項(Extras)」可以新增三種東西:

類型用途要不要寫程式
附加欄位 Additional Field自己算出來的欄位:金額換算、天數差、狀態中文化、單價 = 金額 / 數量要,寫在該欄位的 Code 區
附加表格 Additional Table用目前這筆資料的鍵值去讀另一張表(例如用 MATNR 讀 MAKT 取品名)不用,指定表名即可,系統自動做 SELECT SINGLE
附加結構 Additional Structure把一組附加欄位包成一個結構,方便管理依內含欄位而定

新增附加欄位的步驟(依講義步驟整理):Extras → 建立 → 選「附加欄位」→ 輸入欄位名稱(建議 Z 開頭)→ 指定長度、輸出長度、資料型態(或直接「Like 參考欄位」指向某個 DDIC 欄位)→ 輸入抬頭文字(Header)→ 存檔 → 切到 Code 頁籤寫程式 → 回主畫面把它拖進欄位群組 → Generate

InfoSet 的程式碼區段(Code 頁籤上方的下拉選單)依執行時機分成幾段,觀念跟一支 ABAP 報表的事件一樣:

區段執行時機典型用途
DATA全域宣告區宣告暫存變數、緩衝用的內表
INIT / INITIALIZATION選擇畫面顯示前給選擇欄位預設值
AT SELECTION-SCREEN使用者按執行後檢核輸入值
START-OF-SELECTION取數開始前,只跑一次一次把要用的參照資料撈進緩衝內表
RECORD PROCESSING每讀到一筆資料就跑一次計算附加欄位、過濾資料(可用 CHECK/REJECT)
END-OF-SELECTION全部讀完後收尾、統計

下面是一段實用的 RECORD PROCESSING 範例,示範「算出未交量、把狀態轉成中文、算出交期逾期天數」。InfoSet 的程式碼區段裡,表格欄位是以工作區形式直接引用(例如 VBAP-KWMENG),這是 SAP Query 產生器的規定寫法,不能改成內嵌宣告的變數名。

" ===== 區段:DATA(宣告)=====
TYPES: BEGIN OF ty_makt,
         matnr TYPE makt-matnr,
         maktx TYPE makt-maktx,
       END OF ty_makt.
DATA gt_makt TYPE HASHED TABLE OF ty_makt WITH UNIQUE KEY matnr.

" ===== 區段:START-OF-SELECTION(只跑一次,先把品名一次撈進緩衝)=====
SELECT matnr, maktx
  FROM makt
 WHERE spras = @sy-langu
  INTO TABLE @gt_makt.

" ===== 區段:RECORD PROCESSING(每一筆資料都會跑)=====
" 1. 未交數量 = 訂單量 - 已交量
ZOPEN_QTY = VBAP-KWMENG - VBAP-KLMENG.

" 2. 用緩衝內表取品名,避免在迴圈裡下 SELECT
ZMAKTX = COND #( WHEN line_exists( gt_makt[ matnr = VBAP-MATNR ] )
                 THEN gt_makt[ matnr = VBAP-MATNR ]-maktx
                 ELSE '' ).

" 3. 檢驗狀態中文化(QALS-STAT34 = 'X' 代表已作使用決策)
ZQM_STATUS = SWITCH string( QALS-STAT34
                            WHEN 'X' THEN '檢驗完成'
                            ELSE          '檢驗中/未檢驗' ).

" 4. 交期逾期天數(負數代表尚未到期)
ZDELAY_DAYS = sy-datum - VBAP-EDATU.

" 5. 過濾:只留下有未交量的資料(CHECK 不成立就跳過這一筆)
CHECK ZOPEN_QTY > 0.
RECORD PROCESSING 裡千萬不要下 SELECT。它是每筆資料跑一次,一萬筆資料就是一萬次 DB round trip,在 HANA 上一樣會被 DB 連線成本拖垮。正確做法是上面示範的:START-OF-SELECTION 一次撈進 HASHED TABLE 緩衝,RECORD PROCESSING 只做記憶體查表。真的需要單筆讀取時,改用「附加表格 Additional Table」讓系統幫你做(它有做內部緩衝)。
附加欄位若要出現在選擇畫面,在建立附加欄位時要勾選讓它可以當選擇欄位;但要記得,附加欄位是在資料讀出來之後才算的,用它當條件等於「先全撈再過濾」,資料量大時很慢。能用原生欄位篩選就用原生欄位。

8. SQ01 設計報表:從欄位到版面

InfoSet 準備好、使用者群組也掛好之後,才輪到 SQ01。步驟(依講義步驟整理)

  1. SQ01,先按「使用者群組(Shift+F7)」選定群組(例如 ZVBAP_QALS)——沒選群組就建不出 Query
  2. 輸入 Query 名稱(講義:QUE_KNV1ZVBAP_QALSQUE_VBAK_KNA1),按「建立」,選 InfoSet。
  3. 第 1 頁:抬頭與備註——填 Query 標題、備註,以及輸出格式(SAP List Viewer/ALV Grid 最常用,也可選 ABAP List、Excel、Word 合併列印、下載檔案)。
  4. 第 2 頁:選擇欄位群組——勾選這個 Query 要用到哪些欄位群組。
  5. 第 3 頁:選擇欄位——勾選要出現在報表上的欄位。
  6. 第 4 頁:選擇(Selections)——勾選哪些欄位要出現在選擇畫面上,並可指定是 SELECT-OPTION 還是 PARAMETER、是否必填。
  7. 基本清單(Basic List)——排欄位順序(Line/Sequence)、排序(Sort)、小計(Total/Counter)、群組小計。
  8. 還可再做 統計清單(Statistics)排名清單(Ranked List),做出「前 10 大客戶」這種報表。
  9. 存檔 → 執行(F8)→ 測試 → 回頭調整。
老師的命名習慣是 QUE_ 開頭(QUE_KNV1、QUE_VBAK_KNA1)。講義在第 09-2 節特別註記「最好還是命名成 QUE_」,因為 Query 名稱會被編進產生的程式名裡,前綴一致才好在 SE38 用萬用字元找到它們。
Query 的執行結果如果選 ALV,使用者可以自己拉版面(Layout)並儲存成 /變式名稱,等於免費得到一套個人化報表。另外 SQ01 選單「Query → 執行並列印」、「Query → 背景執行」都支援排程。

9. 產生的程式:AQ 開頭的程式名怎麼解碼

存檔 Query 時,SAP 會偷偷幫你產生一支真的 ABAP 程式,名字是 AQ 開頭的 30 碼怪名字。講義列出的三個實例:

產生的程式名使用者群組Query 名稱
AQZZZCERPS======QUE_KNV1======ZCERPSQUE_KNV1
AQZZZVBAP_QALS==ZVBAP_QALS====ZVBAP_QALSZVBAP_QALS
AQZZZ000========QUE_VBAK_KNA1=Z000QUE_VBAK_KNA1

名稱結構:第 1~2 碼固定是 AQ;第 3~4 碼是查詢區/工作區識別;第 5~16 碼(12 碼)是使用者群組,不足以 = 補滿;第 17~30 碼(14 碼)是 Query 名稱,不足一樣以 = 補滿。所以看到 AQZZZCERPS======QUE_KNV1======,去掉等號就知道是「使用者群組 ZCERPS 的 Query QUE_KNV1」。

不用自己數格子,SAP 有現成的函數 RSAQ_DECODE_REPORT_NAME:在 SE37 直接測試執行,輸入程式名就會回傳 Query、InfoSet 與使用者群組。這在「PRD 出現 dump 只知道程式名是 AQ 開頭」時特別好用。

REPORT zaq_decode.

PARAMETERS p_prog TYPE progname OBLIGATORY
           DEFAULT 'AQZZZVBAP_QALS==ZVBAP_QALS===='.

START-OF-SELECTION.

  DATA lv_query     TYPE aqadef-quname.
  DATA lv_usergroup TYPE aqadef-bgname.
  DATA lv_infoset   TYPE aqadef-isname.
  DATA lv_workspace TYPE aqadef-wsid.

  " 用標準函數把 AQ 程式名還原成 Query / InfoSet / 使用者群組
  TRY.
      CALL FUNCTION 'RSAQ_DECODE_REPORT_NAME'
        EXPORTING
          program   = p_prog
        IMPORTING
          workspace = lv_workspace
          usergroup = lv_usergroup
          query     = lv_query
          infoset   = lv_infoset.
    CATCH cx_root INTO DATA(lx_err).
      MESSAGE lx_err->get_text( ) TYPE 'E'.
  ENDTRY.

  WRITE: / |查詢區 Workspace : { lv_workspace }|,
         / |使用者群組 Group : { lv_usergroup }|,
         / |InfoSet         : { lv_infoset }|,
         / |Query           : { lv_query }|.
另一個方向(由 Query 找程式名):在 SQ01 選單「Query → 其他功能(More Functions) → 顯示報表名稱(Display Report Name)」,SAP 會直接把 30 碼程式名秀給你,複製即可貼到 SE38/SE93。

產生的程式長成下面這樣。它是系統自動產生的骨架,只是把工作丟給幾支同樣自動產生的 Function Module(...EXTR 取數、...SHOW 顯示):

REPORT AQZZZVBAP_QALS==ZVBAP_QALS====.

INCLUDE /1BCDWB/IQG000000000008DAT.          " 資料宣告(自動產生)
DATA %dtab TYPE STANDARD TABLE OF /1BCDWB/IQG000000000008 WITH HEADER LINE.
DATA %subrc TYPE sy-subrc.
INCLUDE /1BCDWB/IQG000000000008SSCR.         " 選擇畫面(自動產生)
INCLUDE /1BCDWB/IQG000000000008SSCRAT.       " 選擇畫面事件(自動產生)

START-OF-SELECTION.
  IF %runmode-extr_on <> space.
    CALL FUNCTION '/1BCDWB/IQG000000000008EXTR'   " 取數
      TABLES     %selopt = %seloptions
                 %dtab   = %dtab
      CHANGING   %rtmode = %runmode
      EXCEPTIONS no_data = 1
                 OTHERS  = 2.
    %subrc = sy-subrc.
    CALL FUNCTION 'RSAQRT_CHECK_EXTR'
      EXPORTING extr_subrc = %subrc
      TABLES    dtab       = %dtab
      CHANGING  rtmode     = %runmode.
  ENDIF.

END-OF-SELECTION.
  IF %runmode-show_on <> space.
    CALL FUNCTION '/1BCDWB/IQG000000000008SHOW'   " 顯示(ALV)
      TABLES   %dtab   = %dtab
      CHANGING %rtmode = %runmode.
  ENDIF.
這支程式絕對不能手動修改。它是 Query 的產物,只要有人在 SQ01/SQ02 存檔或系統升級後重新產生,你的修改就會全部消失。它用了 WITH HEADER LINE 這種在自寫程式中已被禁止的舊語法,也不代表你可以照抄——那是產生器的內部實作。
另外,S/4HANA 升級後常見的 LOAD_PROGRAM_NOT_FOUND dump,原因就是 AQ 程式尚未重新產生;解法是在 SQ01 重新產生該 Query,或執行標準的 Query 重產生程式(RSAQGENQ 系列)。

10. 把 Query 掛成 TCODE:方式一(直接掛程式)與方式二(START_REPORT 參數交易)

方式一:SE93 直接掛 AQ 程式

最直覺的做法:SE93 → 建立交易碼(講義:ZQUE_KNV1ZVBAP_QALSZVBAK_KNA1)→ 選「使用選擇畫面的程式與變式(Report Transaction)」→ 程式名填那 30 碼的 AQ...

方式一有風險:AQ 程式名裡「編」進了使用者群組與 Query 名,但不同系統/不同 client 產生出來的 /1BCDWB/IQG0000000000xx 內部編號不一樣;且升級或重產生後程式可能被刪除重建。所以 TCODE 掛在 AQ 程式上,在 DEV 好好的,傳到 PRD 可能就找不到程式。這正是講義要教「方式二」的原因。

方式二:START_REPORT 參數交易(推薦)

SAP 提供標準程式 START_REPORT,它接受三個參數,執行時動態算出該叫哪支 Query,就不受 AQ 程式名變動影響。先在 SE38 執行 START_REPORT 試跑,確認參數填對,再去 SE93 做成 TCODE。講義的原話是:「確保可以試行成功,再做 SE93。」

參數(Screen Field)填什麼講義實例
D_SREPOVARI-REPORTTYPE報表類型,SAP Query 固定填 AQAQ
D_SREPOVARI-REPORTSQ03 使用者群組,補空白到 12 碼;第 13 碼填查詢區(全域區填 GZCERPS G
ZVBAP_QALS G
ZVBAK_KNA1 G
D_SREPOVARI-EXTDREPORTSQ01 的 Query 名稱QUE_KNV1
ZVBAP_QALS
QUE_VBAK_KNA1
老師特別提醒:「參數的長度要注意。」D_SREPOVARI-REPORT 是「12 碼使用者群組 + 第 13 碼查詢區」。用講義的尺規對照:
1234567890123
ZCERPS G(ZCERPS 6 碼 + 6 個空白 + G)
ZERP300 G(ZERP300 7 碼 + 5 個空白 + G)
ZVBAP_QALS G(ZVBAP_QALS 10 碼 + 2 個空白 + G)
空白數算錯,執行時就會說找不到 Query。用 SE38 試跑時,把游標放到欄位上按 F1 → 技術資訊,可確認欄位長度。

SE93 建立參數交易的步驟(依講義步驟整理)

  1. SE93 → 輸入交易碼(講義:ZVBAP_QALS2ZQUE_VBAK_KNA1,建議命名 ZERP###A)→ 建立。
  2. 「具有參數的交易(Transaction with parameters / parameter transaction)」
  3. TransactionSTART_REPORT;勾選 「Skip initial screen(略過起始畫面)」「Inherit GUI attributes」
  4. 下方參數表填入三行:D_SREPOVARI-REPORTTYPE = AQD_SREPOVARI-REPORT = <使用者群組>+GD_SREPOVARI-EXTDREPORT = <Query 名>
  5. 存檔、掛傳輸請求、測試。
講義 08 節的兩組完整設定:
ZVBAP_QALS2:REPORTTYPE = AQ、REPORT = ZVBAP_QALS G、EXTDREPORT = ZVBAP_QALS(2024/11/07)
ZQUE_VBAK_KNA1:REPORTTYPE = AQ、REPORT = ZVBAK_KNA1 G、EXTDREPORT = QUE_VBAK_KNA1(2025/03/21)
另有一組示範 REPORT = ZERP300 G、EXTDREPORT = ZERP300

11. 從 Query 畢業:把它變成自己的 Z 程式

Query 做到一個程度就會撞牆:要加複雜判斷、要串多個資料來源、要效能、要寫測試。這時有三條路。

路一:直接 SUBMIT 那支 AQ 程式,把結果撈回來加工。適合「Query 已經對了,只是要多做一點事」。

REPORT zque_wrapper.

SELECT-OPTIONS s_vbeln FOR vbak-vbeln.

START-OF-SELECTION.

  " 把選擇條件組成 RSPARAMS,動態丟給 Query 產生的程式
  DATA(lt_seltab) = VALUE rsparams_tt(
    FOR ls_r IN s_vbeln (
      selname = 'SP$00001'          " Query 選擇畫面上的欄位名,可在 SE38 顯示程式時查到
      kind    = 'S'
      sign    = ls_r-sign
      option  = ls_r-option
      low     = ls_r-low
      high    = ls_r-high ) ).

  SUBMIT ('AQZZZ000========QUE_VBAK_KNA1=')
    WITH SELECTION-TABLE lt_seltab
    AND RETURN.
SUBMIT 呼叫 AQ 程式的最大問題,是程式名與選擇欄位名(SP$00001、SP$00002…)都不穩定,Query 一改就可能對不上。只適合短期救急。

路二:用標準函數 RSAQ_QUERY_CALL 取得 Query 的資料。比 SUBMIT 乾淨得多,因為你傳的是「使用者群組 + Query 名 + 查詢區」這種穩定的邏輯名稱,而不是 30 碼的實體程式名。

REPORT zque_call.

START-OF-SELECTION.

  DATA lt_seltab  TYPE TABLE OF rsparams.
  DATA lt_listdsc TYPE TABLE OF rsaqldesc.
  DATA lr_data    TYPE REF TO data.

  TRY.
      CALL FUNCTION 'RSAQ_QUERY_CALL'
        EXPORTING
          workspace       = 'G'                 " 'G' = 全域區;空白 = 標準區
          query           = 'QUE_VBAK_KNA1'     " SQ01 的 Query 名
          usergroup       = 'ZVBAK_KNA1'        " SQ03 的使用者群組
          skip_selscreen  = 'X'                 " 不顯示選擇畫面
          data_to_memory  = 'X'                 " 資料放記憶體,不直接輸出清單
        IMPORTING
          ref_to_ldata    = lr_data             " 指向結果內表的參考
        TABLES
          selection_table = lt_seltab           " 選擇條件
          listdesc        = lt_listdsc.         " 欄位描述

      " 用 Field Symbol 接住動態產生的結果內表
      ASSIGN lr_data->* TO FIELD-SYMBOL(<lt_result>).
      IF <lt_result> IS ASSIGNED.
        cl_salv_table=>factory(
          IMPORTING r_salv_table = DATA(lo_alv)
          CHANGING  t_table      = <lt_result> ).
        lo_alv->get_functions( )->set_all( ).
        lo_alv->display( ).
      ENDIF.

    CATCH cx_root INTO DATA(lx_err).
      MESSAGE lx_err->get_text( ) TYPE 'E'.
  ENDTRY.

路三(正解):把 InfoSet 的 Join 直接改寫成一支自己的 ABAP 報表。Query 只是幫你想清楚了資料模型,真正要長期維護的東西,還是自己寫比較好。下面把講義的 ZVBAK_KNA1(訂單抬頭+客戶主檔)寫成現代 ABAP:

REPORT zsd_order_customer.

TABLES: vbak.   " 僅為了讓 SELECT-OPTIONS 取得型別參考

SELECT-OPTIONS: s_vbeln FOR vbak-vbeln,
                s_kunnr FOR vbak-kunnr,
                s_erdat FOR vbak-erdat.
PARAMETERS: p_vkorg TYPE vbak-vkorg.

START-OF-SELECTION.

  " InfoSet ZVBAK_KNA1 的 Join:VBAK-KUNNR = KNA1-KUNNR
  SELECT h~vbeln,
         h~erdat,
         h~auart,          " 訂單類型
         h~vkorg,          " 銷售組織
         h~vtweg,
         h~spart,
         h~kunnr,          " 售達方
         h~netwr,          " 淨值
         h~waerk,          " 幣別
         c~name1 AS cust_name,
         c~ort01 AS city,
         c~land1 AS country
    FROM vbak AS h
    INNER JOIN kna1 AS c ON c~kunnr = h~kunnr
   WHERE h~vbeln IN @s_vbeln
     AND h~kunnr IN @s_kunnr
     AND h~erdat IN @s_erdat
     AND ( @p_vkorg IS INITIAL OR h~vkorg = @p_vkorg )
   ORDER BY h~vbeln
    INTO TABLE @DATA(lt_order).

  IF lt_order IS INITIAL.
    MESSAGE |查無符合條件的訂單| TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  TRY.
      cl_salv_table=>factory(
        IMPORTING r_salv_table = DATA(lo_alv)
        CHANGING  t_table      = lt_order ).

      lo_alv->get_functions( )->set_all( ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->get_display_settings( )->set_list_header( |訂單主檔與客戶清單(共 { lines( lt_order ) } 筆)| ).
      lo_alv->display( ).

    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.

同樣的邏輯套用在講義第一個實作 ZKNA1_KNB1(客戶編號與帳號關聯):

REPORT zfi_customer_account.

TABLES kna1.

SELECT-OPTIONS: s_kunnr FOR kna1-kunnr.
PARAMETERS      p_bukrs TYPE knb1-bukrs OBLIGATORY.

START-OF-SELECTION.

  " InfoSet ZKNA1_KNB1:KNA1(一般資料)+ KNB1(公司代碼資料)
  SELECT a~kunnr,
         a~name1,
         a~land1,
         a~ort01,
         b~bukrs,          " 公司代碼
         b~akont,          " 統馭科目(客戶對應的總帳科目)
         b~zterm,          " 付款條件
         b~zwels,          " 付款方式
         b~loevm AS del_flag
    FROM kna1 AS a
    INNER JOIN knb1 AS b ON b~kunnr = a~kunnr
   WHERE a~kunnr IN @s_kunnr
     AND b~bukrs = @p_bukrs
   ORDER BY a~kunnr
    INTO TABLE @DATA(lt_cust).

  cl_salv_table=>factory(
    IMPORTING r_salv_table = DATA(lo_alv)
    CHANGING  t_table      = lt_cust ).
  lo_alv->get_functions( )->set_all( ).
  lo_alv->display( ).
實務流程建議:SQVI 驗證 Join → SQ02/SQ01 做成正式 Query 給使用者先用 → 需求穩定後再改寫成 Z 程式或 CDS View。這樣既快又不會做白工。

12. SQ00、SQ99 與 Query 家族 TCODE 一覽

TCODE名稱說明
SQ00SAP Query: Start Queries執行 Query。講義原話:「Sq00 : to start queries and Sq01 : to maintain queries,其實畫面長得一樣」
SQ01SAP Query: Maintain Queries建立與維護 Query(主戰場)
SQ02SAP Query: Maintain InfoSet建立資料來源與欄位群組
SQ03SAP Query: Maintain User Groups使用者群組與 InfoSet 指派
SQ07SAP Query: Language ComparisonQuery 文字的多語言比對/翻譯
SQVIQuickViewer個人用快速報表
SE93Maintain Transaction把 Query 掛成 TCODE
SE16HGeneral Table Display (HANA)HANA 版資料瀏覽器,支援群組、加總、多表關聯
關於 SQ99:講義標題把 SQ99 與 QuickViewer、InfoSet Query 並列。要注意的是,SAP Query 家族的標準交易碼是 SQ00~SQ03、SQ07~SQ11 與 SQVI,SQ99 在多數標準系統中查不到;若你的系統有 SQ99,通常是客戶自建、指向 InfoSet Query(Ad Hoc Query)的捷徑。要用 InfoSet Query,正規入口是在 SQ01 選定 InfoSet 後按「InfoSet Query」按鈕(HR 模組則是 Ad Hoc Query/PQAH)。InfoSet Query 的特色是「拖拉欄位、即時看筆數」的探索式介面,不需要先設計版面。

Query 跳轉(Drill-down,講義 10-2):在 SQ01 的「基本清單」設計畫面,可以用選單「Goto → 報表指派(Report assignment)」把某個欄位連結到另一支報表或交易(例如點訂單號跳 VA03、點客戶號跳 XD03)。這樣使用者從清單可以直接鑽進單據,是 Query 最受歡迎的功能之一。(依講義步驟整理)

13. Report Painter / Report Writer:另一條不寫程式的路

SAP 還有另一套「不寫程式」的報表工具,方向完全不同,常被拿來跟 SAP Query 混淆。

比較項目SAP Query / SQVIReport Painter / Report Writer
TCODESQ00~SQ03、SQVIGRR1/GRR2/GRR3(Painter)、GR31/GR32/GR33(Writer)、GS01 建 Set、GR21 建 Library
資料來源任意 DDIC 表格、View、邏輯資料庫只讀「報表表格(Report Table)」——FI-SL/CO 的彙總表,如 GLPCT、COSP、COSS、CCSS
輸出形狀清單式(一列一筆明細)矩陣式(列=科目/成本要素,欄=期間、實際/預算/差異)
典型用途訂單清單、客戶清單、檢驗批清單損益表、成本中心報表、預算 vs 實際差異分析
學習難度中高(要懂 Library、Set、Characteristic、Key Figure)
選擇口訣:「要看明細清單用 SAP Query;要看期間對比的財務矩陣用 Report Painter。」Report Writer 是 Report Painter 的底層(Painter 是所見即所得的介面,Writer 是原始的列/欄定義),Painter 做不出來的才降到 Writer。
S/4HANA 上 CO/FI 的資料集中到 ACDOCA,很多舊的 Report Painter 報表(讀 COSP/COSS/GLT0)在 Migration 後會抓不到資料或抓錯。S/4 的建議路線是 FGI0/FGI1(Drilldown Report)或直接用 Fiori 的 Embedded Analytics(例如 Financial Statement 系列 App),而不是繼續加 Report Painter。

14. S/4HANA 補充:什麼時候該改用 CDS View + Fiori

SAP Query 在 S/4HANA 1809 之後仍然可以用,SAP 也沒有宣告停用,但它已經是「classic」工具。S/4 的官方報表路線是 CDS View → OData → Fiori Elements,也就是 Embedded Analytics。

把 InfoSet 的 Join 改寫成 CDS View(用 ADT/Eclipse 建立 Data Definition):

@AbapCatalog.sqlViewName: 'ZVSDORDCUST'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: '銷售訂單 + 客戶(取代 InfoSet ZVBAK_KNA1)'
define view Z_I_SalesOrderCustomer
  as select from vbak as SalesOrder
    inner join   kna1 as Customer
      on SalesOrder.kunnr = Customer.kunnr
{
  key SalesOrder.vbeln          as SalesOrderId,
      SalesOrder.erdat          as CreationDate,
      SalesOrder.auart          as OrderType,
      SalesOrder.vkorg          as SalesOrganization,
      SalesOrder.kunnr          as SoldToParty,
      Customer.name1            as CustomerName,
      Customer.land1            as Country,
      @Semantics.amount.currencyCode: 'Currency'
      SalesOrder.netwr          as NetAmount,
      @Semantics.currencyCode: true
      SalesOrder.waerk          as Currency
}

再加上分析註解,就能直接被 Fiori 的分析清單頁(Analytical List Page)與 Query Browser 使用:

@AbapCatalog.sqlViewName: 'ZVSDORDCUBE'
@Analytics.dataCategory: #CUBE           " 宣告為分析用的 Cube
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: '銷售訂單分析 Cube'
define view Z_C_SalesOrderCube
  as select from Z_I_SalesOrderCustomer
{
      @ObjectModel.foreignKey.association: '_Customer'
  key SalesOrderId,
      @Consumption.filter.selectionType: #INTERVAL
      @Consumption.filter.mandatory: true
      CreationDate,
      SalesOrganization,
      SoldToParty,
      CustomerName,
      Country,
      @DefaultAggregation: #SUM          " 金額預設加總
      @Semantics.amount.currencyCode: 'Currency'
      NetAmount,
      @Semantics.currencyCode: true
      Currency
}
S/4HANA 上的「不寫程式報表」新選項:
Custom CDS Views(Fiori App,線上拉關聯建 View)、View Browser(F2170)找現成的標準 CDS View、Query Browser(F1068)直接執行分析型 Query、Custom Analytical Queries(F1572)拉出自己的分析報表、KPI Modeler 做卡片與磚。這幾支 App 才是 S/4 版的 SQ01。
情境建議工具理由
臨時、一次性、只有自己看SE16H 或 SQVI五分鐘搞定,不留技術債
部門共用、GUI 使用者、ECC 沿用SQ03/SQ02/SQ01(建在全域區)可授權、可傳輸、免開發
需要複雜計算、多來源、效能調校、單元測試自己寫 ABAP + CL_SALV_TABLEQuery 的 Code 區段撐不住
要重複被多個報表/App/介面使用CDS View一次建模、處處可用(ABAP SQL、OData、Analytics)
使用者要在 Fiori Launchpad 上看、要下鑽、要圖表CDS Cube + Analytical List Page / Query BrowserEmbedded Analytics 直接讀即時資料,免 BW
要對外提供 APICDS + RAP/OData ServiceQuery 沒有介面能力
S/4HANA 上做 InfoSet 的兩個地雷:
1. MKPF/MSEG 與 BKPF/BSEG 在 S/4 已改為相容視圖(實體表為 MATDOCACDOCA)。把它們拉進 InfoSet Join 通常還能查,但效能可能非常差,因為每次都要即時組出相容視圖。物料憑證改查 MATDOC、會計行項目改查 ACDOCA 或標準 CDS View。
2. VBUK/VBUP 在 S/4 已被移除(狀態欄位併入 VBAK/VBAP)。ECC 沿用過來的 InfoSet 若含 VBUK/VBUP,升級後一定要重做。
3. 部分版本可以把 CDS View 的 SQL View 名稱當成一般 View 放進 InfoSet;但新式(不產生 SQL View)的 CDS 就不行,這時只能改寫程式。

15. 常用 SAP 標準表速查(做 InfoSet 前先背起來)

講義 10-5 節整理了一份標準表清單,這是做 Join 的彈藥庫,完整保留如下。

SD 主資料:客戶主檔

資料表說明
KNA1客戶主檔的一般資料(全公司共用)
KNB1客戶主檔公司代碼資料(含統馭科目 AKONT)
KNBK客戶主檔銀行明細
KNVK客戶主要聯繫夥伴
KNVV客戶主檔銷售資料
KNVP客戶主檔夥伴功能

SD 主資料:價格條件

資料表說明
KONH條件(抬頭)
KONP條件(項目)
KONV條件(交易資料)
KOND條件(資料)
S/4HANA:KONV 已被 PRCD_ELEMENTS 取代(KONV 保留為相容視圖)。新程式與新 InfoSet 請直接用 PRCD_ELEMENTS。

SD 憑證:訂單、交貨、發票

資料表說明
VBAK / VBAP銷售憑證抬頭 / 行項目資料
VBUK / VBUP銷售憑證抬頭狀態 / 項目狀態(S/4 已移除,狀態併入 VBAK/VBAP
VBPA銷售憑證夥伴
VBEP銷售憑證排程行資料
LIKP / LIPS交貨單抬頭 / 行項目
VBRK / VBRP發票(Billing)抬頭 / 行項目

MM 主資料:物料

資料表說明
MARA物料一般資料
MAKT物料說明(依語言)
MARC物料的工廠資料
MARD物料的儲存地點資料(庫存)
MBEW物料評價(成本)
MVKE物料銷售視圖資料

MM 主資料:供應商

資料表說明
LFA1供應商主檔(一般)
LFB1供應商主檔(公司代碼)
LFB5供應商主檔(催款資料)
LFM1供應商採購組織資料
LFM2供應商採購資料
LFBK供應商銀行明細

MM 憑證:採購與物料異動

資料表說明
EINA / EINE採購資訊記錄:一般資料 / 採購組織資料
EKKO / EKPO採購訂單抬頭 / 行項目
EKET交貨計畫(Schedule Lines)
EKKN採購憑證的帳戶設定
EKPA採購夥伴功能
EBAN / EBKN採購申請 / 採購申請帳戶設定
MKPF / MSEG物料憑證抬頭 / 行項目(S/4 實體表為 MATDOC

FI / CO 常用透明表

資料表說明
SKA1總帳科目主檔(科目表層級)
BNKA銀行主檔
BKPF / BSEG會計憑證抬頭 / 行項目(S/4 明細集中在 ACDOCA
BSAD / BSID已清 / 未清應收帳款(S/4 為 CDS 相容視圖)
BSAK / BSIK已清 / 未清應付帳款
BSAS / BSIS已清 / 未清總帳科目
TKA01 / TKA02成本控制範圍 / 成本控制範圍分配
CSKS / CSKT成本中心主檔 / 成本中心說明文字
CRCO工作中心對成本中心的分配
KALO成本核算運行:成本核算對象
KANZ銷售訂單項目的成本核算對象分配
COSPCO 對象:外部記帳的成本總計
COEP / COBKCO 物件:期間別行項目 / 憑證抬頭
COSTCO 物件:價格總計
FAGLFLEXA / FAGLFLEXT新總帳實際行項目 / 總計(S/4 由 ACDOCA 取代

QM 品質管理

資料表說明
QALS檢驗批記錄(本章 ZVBAP_QALS 實作的主角)
QAVE檢驗處理:使用決策(UD)
QAMV檢驗處理的特性說明
QAMR檢驗處理的特性結果
QASR檢驗特性的採樣結果

PP 生產計劃

資料表說明
AUFK訂單主檔(工單/內部訂單共用)
AFKOPP 訂單抬頭資料
AFPO訂單項目
AFVC訂單的工序
AFRU訂單確認(報工)
AUFM針對訂單的貨物移動

狀態(Status)相關表

資料表說明
JEST個別物件狀態(輸入 OR00000+工單號可查該工單所有狀態編號)
TJ01 / TJ01T使用者自訂狀態 / 其文字(講義寫 JJ01T,正確表名為 TJ01T)
TJ02 / TJ02T系統狀態 / 系統狀態文字
JSTO狀態物件資訊(可用 order type 的 status profile 查詢)
TJ30 / TJ30T使用者狀態(依狀態設定檔)/ 其文字

使用者與固定資產

資料表說明
USR21使用者名稱與地址碼的對應
USR02登入資料(密碼、鎖定、有效期)
ADRP個人(中央地址管理)
ANLA資產主檔區段
ANLC資產價值欄位(年度)
ANLP資產期間價值
ANEP / ANEK資產憑證行項目 / 資產過帳憑證抬頭
ANLZ資產的時間相依配置(成本中心、廠區等)
T087評估群組
找表的萬用技巧,比背表名更重要:在畫面欄位上按 F1 → 技術資訊(Technical Information),直接看到表名與欄位名;或用 SE11*KNA* 這種萬用字元搜尋;或開 ST05 SQL Trace 錄一段標準交易,看它到底讀了哪些表。

16. 傳輸、授權與運維檢查表

  • 傳輸:全域區物件 → 掛傳輸請求走 STMS;標準區物件 → 用 RSAQR3TR 匯出/匯入。傳輸順序一定是 使用者群組 → InfoSet → Query,順序反了會因為找不到相依物件而失敗。
  • 授權:核心權限物件是 S_QUERY(02=維護、23=傳輸),加上使用者要被指派到對應的 SQ03 使用者群組;此外報表讀到的資料表仍受 S_TABU_DIS/S_TABU_NAM 與模組權限(如 V_VBAK_VKO)管制。權限出問題先跑 SU53
  • 使用者參數AQW=預設查詢區(填 G 進全域區)。
  • 升級後:Query 產生的 AQ 程式需要重新產生,否則 TCODE 會 dump LOAD_PROGRAM_NOT_FOUND
  • 命名:使用者群組、InfoSet、Query 一律 Z 開頭,Query 建議 QUE_ 前綴,TCODE 用 Z 開頭。
  • 文件化:把「TCODE ↔ Query ↔ InfoSet ↔ 使用者群組 ↔ AQ 程式名」做成一張對照表存起來。這張表在別人接手時價值連城。

最後給一段小工具程式,把系統裡所有 AQ 開頭的程式列出來並解碼,一次盤點所有 Query:

REPORT zaq_inventory.

START-OF-SELECTION.

  " 找出所有由 SAP Query 產生的報表程式
  SELECT name AS progname
    FROM trdir
   WHERE name LIKE 'AQ%'
     AND subc = '1'                 " 1 = 可執行報表
   ORDER BY name
    INTO TABLE @DATA(lt_prog).

  TYPES: BEGIN OF ty_out,
           progname  TYPE progname,
           usergroup TYPE aqadef-bgname,
           query     TYPE aqadef-quname,
           infoset   TYPE aqadef-isname,
         END OF ty_out.
  DATA lt_out TYPE STANDARD TABLE OF ty_out.

  LOOP AT lt_prog ASSIGNING FIELD-SYMBOL(<ls_prog>).
    CLEAR: DATA(lv_grp), DATA(lv_qry), DATA(lv_iset).
    CALL FUNCTION 'RSAQ_DECODE_REPORT_NAME'
      EXPORTING program   = <ls_prog>-progname
      IMPORTING usergroup = lv_grp
                query     = lv_qry
                infoset   = lv_iset
      EXCEPTIONS OTHERS    = 1.
    CHECK sy-subrc = 0.
    APPEND VALUE #( progname  = <ls_prog>-progname
                    usergroup = lv_grp
                    query     = lv_qry
                    infoset   = lv_iset ) TO lt_out.
  ENDLOOP.

  cl_salv_table=>factory(
    IMPORTING r_salv_table = DATA(lo_alv)
    CHANGING  t_table      = lt_out ).
  lo_alv->get_functions( )->set_all( ).
  lo_alv->get_display_settings( )->set_list_header( |系統內 SAP Query 盤點({ lines( lt_out ) } 支)| ).
  lo_alv->display( ).
上面的 CLEAR: DATA(...) 只是為了示意;實務上請把三個變數在 LOOP 外用 DATA lv_grp TYPE aqadef-bgname. 宣告好,迴圈內用 CLEAR 即可。舊式 Function Module 的 IMPORTING 參數不支援內嵌宣告。

練習與自我檢核

  1. (講義 11 節)SQVI 品質檢驗:用 SQVI 建 ZERP###V,聯結 VBAPQALS,做出「待出貨訂單+檢驗批狀態」清單。
    提示:資料來源選「表格聯結」,Join 條件 MATNR+WERKS;輸出欄位至少要有 VBELN、POSNR、MATNR、KWMENG、PRUEFLOS、HERKUNFT、STAT34。做完想想:為什麼這份 QuickView 沒辦法給主管執行?
  2. (講義 02-1)ZKNA1_KNB1:用 SQ03 建使用者群組、SQ02 建 InfoSet 聯結 KNA1+KNB1、SQ01 做出「客戶編號 → 統馭科目(AKONT)」對照表。
    提示:KNB1 一定要用 BUKRS 當必填選擇條件,否則跨公司代碼的客戶會出現重複列。
  3. (講義 02-3 / 03-3)ZVBAK_KNA1 + QUE_VBAK_KNA1:做出訂單抬頭+客戶名稱報表,並在 SQ01 加上「依銷售組織小計淨值」。
    提示:小計要在「基本清單」畫面對 VKORG 設 Sort,再對 NETWR 勾 Total。
  4. 把第 3 題的 Query 用 方式二 START_REPORT 掛成 TCODE ZERP###A
    答案要點:REPORTTYPE=AQ;REPORT=使用者群組補空白到 12 碼+第 13 碼 G;EXTDREPORT=Query 名。做之前先在 SE38 執行 START_REPORT 試跑成功。
  5. 找出第 3 題 Query 產生的程式名,並說明它的 30 碼各代表什麼。
    答案要點:SQ01 選單「Query → 其他功能 → 顯示報表名稱」,或用 RSAQ_DECODE_REPORT_NAME;AQ+區域 2 碼+使用者群組 12 碼(以=補滿)+Query 名 14 碼(以=補滿)。
  6. 在 InfoSet 裡加一個附加欄位「未交數量」= KWMENG − KLMENG,並讓它只顯示大於 0 的資料。
    答案要點:Extras → 附加欄位 → 型別參考 VBAP-KWMENG;程式寫在 RECORD PROCESSING;過濾用 CHECK
  7. 解釋:為什麼在 RECORD PROCESSING 裡寫 SELECT SINGLE maktx FROM makt ... 是壞習慣?該怎麼改?
    答案要點:每筆資料一次 DB 存取;改成 START-OF-SELECTION 一次撈進 HASHED TABLE,或用「附加表格」。
  8. 你的 Query 在 DEV 100 建好了,PRD 的使用者說看不到。列出三個要檢查的地方。
    答案要點:(a) 建在標準區還是全域區?標準區要跑 RSAQR3TR;(b) 使用者有沒有被指派到該 SQ03 使用者群組;(c) S_QUERY 與資料表權限(SU53)。
  9. 比較 SAP Query 與 Report Painter 的差異,各舉一個最適合的報表需求。
    答案要點:Query=明細清單(訂單清單);Report Painter=期間矩陣(成本中心實際 vs 預算)。
  10. 在 S/4HANA 上,什麼情況你會選擇「不做 Query,直接建 CDS View」?至少說三個理由。
    答案要點:需要被多處重用/要曝露成 OData 給 Fiori 或外部系統/要進 Embedded Analytics 做下鑽與圖表/要版本控管與傳輸/效能需要 code pushdown。
  11. 把講義的 ZVBAP_QALS InfoSet 改寫成一支自己的 ABAP 報表(CL_SALV_TABLE),並加上 Left Outer Join 讓沒有檢驗批的訂單也能顯示。
    提示:對照本章第 6 節的程式碼,注意 QALS 的過濾條件要放在 ON 而不是 WHERE。
  12. 用本章最後的 ZAQ_INVENTORY 概念,盤點你手上系統有幾支 Query、分屬哪些使用者群組,並整理成「TCODE ↔ Query ↔ InfoSet」對照表。

第 2-1 章 基本語法:資料型別 / 判斷式 / 迴圈 / 日期時間 / 字串處理

這一章是 ABAP 的地基:宣告什麼型別、值怎麼在型別之間搬、判斷與迴圈怎麼寫、日期時間怎麼算、字串怎麼切怎麼拼、金額小數怎麼才不會差一塊錢。老師在講義裡放了 20 支練習程式(ZDATATYPE01~ZDATATYPE20)與 10 個作業,這裡全部整理成可查閱的條目,並把 1992 年代的舊寫法一律改寫成 S/4HANA 1809 以後可直接編譯執行的現代 ABAP。學完之後,你看到任何一段別人寫的老程式,都能立刻判斷它在做什麼、以及該怎麼改。

核心 TCODESE38SE11SE37SE80SE24SE91SU3ABAPDOCUABAPHELPICONSE54SM30SCU3STZAC
關鍵物件SYST(SY-*)、TCURX、TCURC、T009/T009B、TFACD/TFACS、TTZZ/TTZCU、CL_ABAP_MATH、CL_ABAP_TSTMP、CL_ABAP_CONTEXT_INFO、CL_ABAP_CODEPAGE、CL_SALV_TABLE、RP_LAST_DAY_OF_MONTHS、HR_JP_MONTH_BEGIN_END_DATE、RP_CALC_DATE_IN_INTERVAL、CONVERT_DATE_TO_INTERNAL、BAPI_CURRENCY_CONV_TO_INTERNAL

1. 內建資料型別總表(先把這張表背起來)

ABAP 的「資料型別」本身不佔記憶體,只有「資料物件(變數/常數)」才佔記憶體。型別可以是系統預定義(built-in)、也可以是 SE11 裡的 Data Element / Domain。下表是講義的核心表格,加上 S/4HANA 常用但講義只帶過的幾個型別。

縮寫型別預設長度允許長度初始值說明與典型用途
C文字型 Character11~262143space固定長度字串。取值靠左,超長截斷。MATNR、KUNNR 這類 key 都是 C
N數字字串 Numeric text11~262143'00…0'只放 0~9 的字串。取值靠右、左補 0。單據號、月份、POSNR 用它
D日期 Date8固定 8'00000000'內部格式一律 YYYYMMDD,可直接加減(單位=天)
T時間 Time6固定 6'000000'內部格式一律 HHMMSS,可直接加減(單位=秒)
I整數 Integer4 bytes固定0-2 147 483 648 ~ 2 147 483 647,計數器用
INT8長整數8 bytes固定0約 ±9.2×10^18。筆數/金額分位加總避免溢位
P壓縮數 Packed81~16 bytes(1~31 位)0定點十進位,小數最多 14 位。所有金額、數量都是 P
F二進位浮點 Float8 bytes固定0約 15~16 位有效數字,不精確。只用在科學計算
DECFLOAT16十進位浮點8 bytes固定016 位精確十進位,運算比 P 快、比 F 準
DECFLOAT34十進位浮點16 bytes固定034 位,超大/超精密數字(利率、匯率連乘)首選
STRING可變長字串動態不限``(空字串)長度隨內容變動,保留尾端空白
X位元組 Byte11~524287X'00'二進位資料、GUID、旗標位元
XSTRING可變長位元組串動態不限檔案上傳/下載、PDF、附件內容
另有 b(INT1)s(INT2) 兩個小整數型別,不能直接寫在 DATA ... TYPE b,必須透過 DDIC 的資料元素 INT1 / INT2 使用(講義的 TYPE int2 就是這個)。INT8 是 ABAP 7.54(S/4HANA 1909)才成為內建型別;1809 上請用 DDIC 資料元素 INT8

講義強調的五個取值規則,實務上天天踩到:

  • 不寫 TYPE 時,預設型別是 CDATA lv_a(10). 等同 TYPE c)。
  • C 型從左取、N 型從右取,超過定義長度就截斷。
  • C 可以放數字、文字或混合;但把 C 當數值用時是靠右對齊解讀。
  • D 與 T 可以直接做四則運算(D 加減的單位是「天」,T 加減的單位是「秒」)。
  • P 一定要用 DECIMALS 宣告小數位,沒寫就等於整數。
REPORT zdatatype01.
" 練習 01:C N D T I P F 的初始值 — 看清楚每個型別「空的時候長什麼樣」
DATA lv_c TYPE c LENGTH 1.
DATA lv_d TYPE d.
DATA lv_f TYPE f.
DATA lv_i TYPE i.
DATA lv_n TYPE n LENGTH 1.
DATA lv_p TYPE p LENGTH 8 DECIMALS 2.
DATA lv_t TYPE t.

cl_demo_output=>write( |LV_C: >{ lv_c }<| ).      " space
cl_demo_output=>write( |LV_D: { lv_d }| ).          " 00000000
cl_demo_output=>write( |LV_F: { lv_f }| ).          " 0.0
cl_demo_output=>write( |LV_I: { lv_i }| ).          " 0
cl_demo_output=>write( |LV_N: { lv_n }| ).          " 0
cl_demo_output=>write( |LV_P: { lv_p }| ).          " 0.00
cl_demo_output=>write( |LV_T: { lv_t }| ).          " 000000
cl_demo_output=>display( ).
老師的口訣:「C 靠左、N 靠右、D 八碼、T 六碼、P 才是錢」。任何金額欄位若看到有人宣告成 TYPE f,直接視為 bug。

2. TYPES / DATA / CONSTANTS / STATICS:宣告的四把刀

TYPES 定義的是「型別」,不能賦值;DATA 才是「變數」;CONSTANTS 是一經定義就不能改的常數;STATICS 是「離開副程式仍然記得上次的值」的區域變數。

關鍵字語法骨架重點
TYPESTYPES ty_x TYPE ... .只是型別;不能 ty_x = 1
DATADATA lv_x TYPE ty_x VALUE '...'.真正配置記憶體
CONSTANTSCONSTANTS c_pi TYPE p LENGTH 8 DECIMALS 5 VALUE '3.14159'.必須有 VALUE,執行期不可改
STATICSSTATICS lv_cnt TYPE i.只在副程式/方法內宣告,值跨呼叫保留
LIKEDATA lv_d LIKE sy-datum.參照「既有資料物件」;TYPE 才是參照「型別」
DATA(x)DATA(lv_x) = ...7.40 起的內嵌宣告,型別由右式推導
REPORT zdatatype02.
" 練習 02:字面值、系統變數、日期可運算
CONSTANTS c_pi TYPE p LENGTH 8 DECIMALS 5 VALUE '3.14159'.

DATA lv_c TYPE c LENGTH 10 VALUE 'ABCDEFGHIJK'.   " 超過 10 碼 → 右邊被截掉
DATA lv_n TYPE n LENGTH 10 VALUE '123456789'.     " N 靠右、左補 0 → 0123456789
DATA lv_p TYPE p LENGTH 8 DECIMALS 2 VALUE '1234567890.88'.
"  P 有小數時,VALUE 一定要用單引號括起來,否則語法錯誤

DATA(lv_today) = sy-datum.                        " 內嵌宣告,型別自動是 D
DATA(lv_now)   = sy-uzeit.                        " 型別自動是 T

DATA(lv_d30)  = CONV d( lv_today + 30 ).          " 日期 +30 天
DATA(lv_t1h)  = CONV t( lv_now + 3600 ).          " 時間 +3600 秒 = 1 小時

cl_demo_output=>write( |C(截斷)  : { lv_c }| ).
cl_demo_output=>write( |N(補零)  : { lv_n }| ).
cl_demo_output=>write( |P        : { lv_p }| ).
cl_demo_output=>write( |今天      : { lv_today DATE = USER }| ).
cl_demo_output=>write( |30 天後   : { lv_d30 DATE = USER }| ).
cl_demo_output=>write( |現在      : { lv_now TIME = USER }| ).
cl_demo_output=>write( |一小時後  : { lv_t1h TIME = USER }| ).
cl_demo_output=>write( |PI        : { c_pi }| ).
cl_demo_output=>display( ).
講義示範的 TYPES: address(50) TYPE c.DATA: BEGIN OF wa ... OCCURS 0 ... END OF wa.TYPES P_TYPE(3) TYPE P DECIMALS 2. 這種「長度括號」+OCCURSWITH HEADER LINE 的寫法,在 Unicode 與 ABAP Objects 環境已被禁用或列為 obsolete。一律改成 TYPE c LENGTH 50TYPE p LENGTH 3 DECIMALS 2TYPE STANDARD TABLE OF ... WITH EMPTY KEY

靜態變數 STATICS(練習 18):同一支程式裡,STATICS 只初始化一次,之後每次進來都保留上次的值;DATA 則每次進來都重新歸零。這是做「累計計數器」「只執行一次的初始化」最省事的手法。

REPORT zdatatype18.
" 練習 18:STATICS vs DATA — 現代寫法改用區域類別的方法,行為完全相同
CLASS lcl_demo DEFINITION.
  PUBLIC SECTION.
    METHODS with_statics.
    METHODS with_data.
ENDCLASS.

CLASS lcl_demo IMPLEMENTATION.
  METHOD with_statics.
    STATICS lv_val TYPE i.        " 跨呼叫保留
    lv_val = lv_val + 1.
    cl_demo_output=>write( |STATICS: { lv_val }| ).   " 1 2 3
  ENDMETHOD.
  METHOD with_data.
    DATA lv_val TYPE i.           " 每次重新歸零
    lv_val = lv_val + 1.
    cl_demo_output=>write( |DATA   : { lv_val }| ).   " 1 1 1
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  DATA(lo_demo) = NEW lcl_demo( ).
  DO 3 TIMES.
    lo_demo->with_statics( ).
  ENDDO.
  DO 3 TIMES.
    lo_demo->with_data( ).
  ENDDO.
  cl_demo_output=>display( ).

3. 結構、繼承(INCLUDE TYPE)與 CORRESPONDING

結構就是「一列資料」。講義示範三種寫法:TYPES BEGIN OF ... END OF(推薦)、DATA BEGIN OF ... END OF(可用但不建議)、以及參照 SE11 的 Structure。結構要「繼承」欄位時用 INCLUDE

老師特別標紅的坑:自訂型別要用 INCLUDE TYPE,不可用 INCLUDE STRUCTUREINCLUDE STRUCTURE 只能用在 DATA BEGIN OF 裡引用「資料物件或 DDIC 結構」。而且 INCLUDE 那一行結尾是句號不是逗號,前後的 DATA: / TYPES: 要重新起頭 — 這是初學者最常見的語法錯誤。
REPORT zdatatype03.
" 練習 03:型別、結構、繼承(INCLUDE TYPE)
TYPES ty_bu_partner  TYPE c LENGTH 10.
TYPES ty_bu_partner1 TYPE bu_partner.        " 直接參照 DDIC Data Element
TYPES ty_bu_partner2 TYPE ty_bu_partner.     " 型別再參照型別

TYPES: BEGIN OF ty_partner_info,
         nam TYPE c LENGTH 10,
         age TYPE i,
         tel TYPE n LENGTH 12,
       END OF ty_partner_info.

" 繼承:把 ty_partner_info 的欄位整批納入,再加自己的欄位
TYPES: BEGIN OF ty_customer.
         INCLUDE TYPE ty_partner_info.      " 注意:自訂型別用 INCLUDE TYPE
TYPES:   adr TYPE c LENGTH 40,
       END OF ty_customer.

START-OF-SELECTION.
  " VALUE #( ) 一次把整個結構填好,取代逐欄位 = 的舊寫法
  DATA(ls_info) = VALUE ty_partner_info( nam = 'RICKY'
                                         age = 30
                                         tel = '0930463002' ).

  " 欄位完全相同 → 直接用 =;欄位不同 → 用 CORRESPONDING #( )
  DATA(ls_cust) = CORRESPONDING ty_customer( ls_info ).
  ls_cust-nam = '繼承的'.
  ls_cust-adr = '桃園市中大路 300 號'.

  cl_demo_output=>write_data( ls_info ).
  cl_demo_output=>write_data( ls_cust ).
  cl_demo_output=>display( ).

結構賦值的三種情境,老師的總結是:「完全相同用 =,否則用 CORRESPONDING」MOVE-CORRESPONDING 仍可用(它不是 obsolete),但新程式建議用建構式版本,因為它能寫在運算式裡、還能改欄位對應。

" 三種等價寫法(由舊到新)
MOVE-CORRESPONDING ls_from TO ls_to.               " 舊:只能當敘述句
ls_to = CORRESPONDING #( ls_from ).                " 新:可寫在任何運算式裡

" 欄位名稱不同時,用 MAPPING 對應、用 EXCEPT 排除
ls_to = CORRESPONDING #( ls_from
                         MAPPING kunnr = customer_id
                                 name1 = customer_name
                         EXCEPT  mandt ).
CORRESPONDING / MOVE-CORRESPONDING 只比對欄位名稱,不管順序、也不管型別。若來源是 CHAR20、目標是 CHAR10,會靜靜地截掉後面 10 碼而不報錯 — 這是報表資料「莫名其妙變短」的頭號元兇。

4. 賦值、offset+length、指標(FIELD-SYMBOLS / REF)

ABAP 給值的方式,講義列了三種:=MOVEWRITE ... TO ...。其中 MOVE 已是 obsolete,WRITE ... TO ... 是「帶格式的賦值」(不是輸出),只在需要套用輸出格式時才用。

offset + length 寫法是 ABAP 最有特色的語法:lv_field+3(5) 表示「跳過 3 個字元,取 5 個字元」,+3(*) 表示「跳過 3 個之後全取」。它可以出現在等號左邊(部分覆寫),這在處理日期字串時非常好用。

REPORT zdatatype05.
" 練習 05:offset+length、WRITE TO、日期部分覆寫
DATA(lv_date1) = sy-datum.
DATA(lv_date2) = sy-datum.

lv_date2+6(2) = '30'.            " 把 YYYYMMDD 的 DD 換成 30(等號左邊也能用 offset)

DATA(lv_ymd) = CONV char8( '20220126' ).
DATA(lv_yyyy)   = lv_ymd+0(4).   " 2022
DATA(lv_mm)     = lv_ymd+4(2).   " 01
DATA(lv_dd)     = lv_ymd+6(2).   " 26
DATA(lv_yymmdd) = lv_ymd+2(*).   " 220126 ← (*) 表示後面全取

" 現代等價寫法:substring( ),可讀性更好、也能用在字串型別
DATA(lv_yyyy2) = substring( val = lv_ymd off = 0 len = 4 ).
DATA(lv_ymd2)  = substring_from( val = lv_ymd off = 2 ).

" WRITE ... TO ... 是「賦值」不是輸出,可帶格式
DATA lv_target TYPE c LENGTH 20.
WRITE sy-datum TO lv_target DD/MM/YYYY.

cl_demo_output=>write( |{ lv_date1 } / { lv_date2 } / { lv_yyyy }-{ lv_mm }-{ lv_dd } / { lv_yymmdd } / { lv_target }| ).
cl_demo_output=>display( ).
用 offset+length 時,整段運算式中間不可以有空格lv_a+3(5) 對,lv_a + 3(5) 錯,後者會被當成加法)。另外對 TYPE string 使用 +off(len) 在 Unicode 系統會有限制,字串請一律改用 substring( )

FIELD-SYMBOLS 是指標(分身):它本身不是變數,存的是別的變數的位址。改 <fs> 就等於改本尊。IS ASSIGNED 用來檢查有沒有指到東西 — 沒指到就用,會 dump。

REPORT zdatatype06.
" 練習 06:FIELD-SYMBOLS 指向結構,改分身=改本尊
TYPES: BEGIN OF ty_body,
         name   TYPE c LENGTH 30,
         high   TYPE p LENGTH 5 DECIMALS 2,
         weight TYPE p LENGTH 5 DECIMALS 2,
       END OF ty_body.

DATA(ls_body) = VALUE ty_body( name = 'CASPER' high = '1.73' weight = '80' ).

FIELD-SYMBOLS <fs_body> TYPE ty_body.
ASSIGN ls_body TO <fs_body>.

IF <fs_body> IS ASSIGNED.
  <fs_body>-name   = 'RESET'.       " 改分身
  <fs_body>-high   = '0.00'.
  <fs_body>-weight = '0.00'.
ENDIF.

cl_demo_output=>write_data( ls_body ).   " 本尊也跟著變了
" 內表迴圈的現代用法:直接內嵌宣告 field symbol,省一次搬資料
"   LOOP AT lt_x ASSIGNING FIELD-SYMBOL(<ls_x>).
"     <ls_x>-flag = 'X'.               " 直接改內表,不必 MODIFY
"   ENDLOOP.
cl_demo_output=>display( ).
S/4HANA 的三種「指到別人」的手段:FIELD-SYMBOLASSIGNING,最快、可直接改內表)、資料參考 REF TOREFERENCE INTO DATA(lr_x),可存起來傳來傳去)、值拷貝INTO DATA(ls_x),最安全但最慢)。百萬筆迴圈務必用前兩者。

5. 型別轉換的陷阱(老師說一定要小心驗算)

不同型別互相賦值時,ABAP 會「自動轉換」,但規則很多而且不一定符合直覺。講義的三個經典陷阱:

陷阱一:C → N → I 的資訊遺失。C 轉 N 時只保留數字字元,其他丟掉;N 再轉 I 時若超出範圍會 dump。

REPORT zdatatype08a.
" 陷阱一:'ABC1234567' 轉 N 只剩數字,且靠右對齊補 0
DATA lv_c TYPE c LENGTH 10 VALUE 'ABC1234567'.
DATA lv_n TYPE n LENGTH 10.
DATA lv_i TYPE i.

lv_n = lv_c.        " → 0001234567(字母被丟棄,右靠齊左補 0)
lv_i = lv_n.        " → 1234567

cl_demo_output=>write( |C={ lv_c } N={ lv_n } I={ lv_i }| ).
cl_demo_output=>display( ).

陷阱二:整數除法會四捨五入,不是無條件捨去。這是 ABAP 跟 C / Java 最大的差別。

REPORT zdatatype08b.
" 陷阱二:/ 、DIV 、MOD 的差別(結果型別是 N 或 I 時會先四捨五入)
DATA lv_r TYPE n LENGTH 2.

lv_r = 45 / 7.      cl_demo_output=>write( |45 /   7 = { lv_r }| ).  " 6  (6.43 四捨五入)
lv_r = 45 DIV 7.    cl_demo_output=>write( |45 DIV 7 = { lv_r }| ).  " 6  (整數商)
lv_r = 45 MOD 7.    cl_demo_output=>write( |45 MOD 7 = { lv_r }| ).  " 3  (餘數)

" 更容易中招的:5 / 2 = 3 而不是 2
DATA(lv_i) = 5 / 2.
cl_demo_output=>write( |5 / 2 存成 I = { lv_i }| ).                  " 3
" 要得到 2.5 必須讓結果型別有小數
DATA(lv_p) = CONV decfloat34( 5 ) / 2.
cl_demo_output=>write( |改用 DECFLOAT34 = { lv_p }| ).               " 2.5
cl_demo_output=>display( ).
ABAP 的 MOD 對負數的結果永遠是「非負數」(-5 MOD 3 = 1),跟 C / Java 的 %(會得到 -2)不同。從其他語言移植演算法時務必驗算。

陷阱三:先轉型再運算,還是先運算再轉型?講義的 sqrt( 5 ) + sqrt( 6 ) 例子非常經典:sqrt() 回傳的是 F 型別,直接相加後才轉整數得 5(2.236+2.449=4.685→5);若各自先 CONV i( ) 變成 2+2 就只有 4。

REPORT zdatatype09.
" 練習 09:CONV 的位置決定答案
DATA(lv_f1) = sqrt( 5 ).                          " 2.2360679...
DATA(lv_f2) = sqrt( 6 ).                          " 2.4494897...

DATA lv_int1 TYPE i.
DATA lv_int2 TYPE i.

lv_int1 = sqrt( 5 ) + sqrt( 6 ).                  " 先加再轉 → 4.685 → 5
lv_int2 = CONV i( sqrt( 5 ) ) + CONV i( sqrt( 6 ) ).  " 先各自轉 → 2 + 2 → 4

cl_demo_output=>write( |F1={ lv_f1 } F2={ lv_f2 }| ).
cl_demo_output=>write( |先加再轉 = { lv_int1 },先轉再加 = { lv_int2 }| ).

" EXACT #( ) :不允許失真,一旦要四捨五入就丟例外 → 財務欄位建議用它
TRY.
    DATA(lv_exact) = EXACT i( '2.5' ).
  CATCH cx_sy_conversion_rounding INTO DATA(lx_round).
    cl_demo_output=>write( |EXACT 攔到失真:{ lx_round->get_text( ) }| ).
ENDTRY.
cl_demo_output=>display( ).

數學函數一覽(ABAPDOCU 的 Numeric Calculations 節點共 17 個,最常用的是這些):

函數意義-5.55 的結果
abs( )絕對值5.55
sign( )正負號(1 / 0 / -1)-1
ceil( )無條件進位(往 +∞)-5
floor( )無條件捨去(往 -∞)-6
trunc( )去掉小數(往 0)-5
frac( )只留小數部分-0.55
round( val = x dec = n mode = ... )指定小數位四捨五入,mode 見 CL_ABAP_MATH依 mode
sqrt( ) / exp( ) / log( ) / log10( )開根號、指數、對數(回傳 F)
sin( ) cos( ) tan( ) acos( ) …三角函數(回傳 F)
ipow( base = x exp = n )整數次方;一般次方用 **
REPORT zmath_demo.
" 四捨五入的五種模式(cl_abap_math 常數),財務報表一定要指定 mode
DATA(lv_val) = CONV decfloat34( '2.345' ).

cl_demo_output=>write( |half_up   : { round( val = lv_val dec = 2 mode = cl_abap_math=>round_half_up ) }| ).
cl_demo_output=>write( |half_down : { round( val = lv_val dec = 2 mode = cl_abap_math=>round_half_down ) }| ).
cl_demo_output=>write( |half_even : { round( val = lv_val dec = 2 mode = cl_abap_math=>round_half_even ) }| ).  " 銀行家捨入
cl_demo_output=>write( |ceiling   : { round( val = lv_val dec = 2 mode = cl_abap_math=>round_ceiling ) }| ).
cl_demo_output=>write( |floor     : { round( val = lv_val dec = 2 mode = cl_abap_math=>round_floor ) }| ).

" 計算順序:函數 > 次方(**) > 乘除(* / DIV MOD) > 加減(+ -)
" 運算子與運算元之間「一定要有空格」,這是 ABAP 的硬規定
DATA(lv_dist) = ( ( 2 - -2 ) ** 2 + ( -1 - 2 ) ** 2 ) ** '0.5'.   " 歐幾里德距離 = 5
cl_demo_output=>write( |歐氏距離 = { lv_dist }| ).
cl_demo_output=>display( ).
P 型態注意事項(講義第 08 節):P 是「壓縮定點數」,兩個十進位數字壓進一個位元組,正負號與小數點另佔半個位元組。在 ABAP 程式裡,有效數字可到 1~31 位、小數最多 14 位,例如 DATA lv_p TYPE p LENGTH 16 DECIMALS 14. 可以放 '12345678901234567.89012345678901' 這種數字;但在 SE11 的 DOMAIN 裡,DEC 型別的「長度」欄位填的是含小數在內的總位數(例如整數 9 位+小數 2 位要填長度 11、小數位 2),兩邊的計數方式不一樣,這是初學者對照 SE11 時最容易搞混的地方。(依講義截圖步驟整理)
講義使用的 CALL FUNCTION 'ROUND'(參數 decimalsinputsign'X'=四捨五入、'+'=無條件進位、'-'=無條件捨去)仍然可用,但新程式直接用內建 round( ) 即可,不必呼叫 FM、也不用處理 overflow / input_invalid 例外。

6. 判斷式:IF / CASE / COND / SWITCH

比較運算子與邏輯運算子:

符號文字意義備註
=EQ等於老師要求:一律用符號,不要用 EQ / NE 這種文字寫法
<>NE不等於舊式的 >< 禁用
<LT小於
<=LE小於等於舊式的 =< 禁用
>GT大於
>=GE大於等於舊式的 => 禁用
AND / OR / NOT且 / 或 / 非NOT 的優先權最高
IS INITIAL是否為型別初始值判斷空值一律用它,不要跟 = ''
IS NOT INITIAL非初始值NOT ... IS INITIAL 好讀
IS ASSIGNED / IS BOUND / IS SUPPLIEDfield symbol 已指定 / 參考已建立 / 參數有傳入用前先檢查,避免 dump
BETWEEN a AND b區間含頭含尾
IN itab_range符合 RANGE / SELECT-OPTIONS空的 range 恆為真;且「用區間」與「用列舉」的結果可能不同,列舉較精確
CO / CN / CA / NA / CS / NS / CP / NP字元類比較CS=包含子字串、CP=萬用字元比對(*+
REPORT zdatatype14.
" 練習 14:IF 判斷奇偶數、CASE 判斷餘數
DATA(lv_cnt) = 6.

IF lv_cnt MOD 2 = 0.
  cl_demo_output=>write( |{ lv_cnt } 是偶數| ).
ELSEIF lv_cnt < 0.
  cl_demo_output=>write( |{ lv_cnt } 是負數| ).
ELSE.
  cl_demo_output=>write( |{ lv_cnt } 是奇數| ).
ENDIF.

DATA(lv_rec) = 8.
CASE lv_rec MOD 3.
  WHEN 0.       cl_demo_output=>write( |{ lv_rec } mod 3 餘 0| ).
  WHEN 1.       cl_demo_output=>write( |{ lv_rec } mod 3 餘 1| ).
  WHEN OTHERS.  cl_demo_output=>write( |{ lv_rec } mod 3 餘 2| ).
ENDCASE.

" CASE TYPE OF:物件導向常用,依實際類別分支
" CASE TYPE OF lo_obj.
"   WHEN TYPE cl_a INTO DATA(lo_a).  ...
"   WHEN OTHERS.                     ...
" ENDCASE.
cl_demo_output=>display( ).

COND 與 SWITCH(7.40 起)是「運算式版」的 IF / CASE,可以直接寫在等號右邊、參數裡、字串範本裡,讓程式短一半。

REPORT zdatatype07.
" 練習 07:COND #( ) 與 SWITCH #( )
" COND = 條件式 IF;每個 WHEN 是完整條件
DATA(lv_role1) = COND string( WHEN sy-uname = 'CASPER' THEN 'TEACHER'
                              WHEN sy-uname = 'IT03'   THEN 'IT'
                              ELSE 'STUDENT' ).

" SWITCH = 單一運算元的 CASE;WHEN 後面只寫值
DATA(lv_role2) = SWITCH string( sy-uname
                                WHEN 'CASPER' THEN 'TEACHER'
                                WHEN 'IT03'   THEN 'IT'
                                ELSE 'STUDENT' ).

" 直接嵌在字串範本裡,連中間變數都省了
cl_demo_output=>write( |使用者 { sy-uname } 的角色是 { lv_role1 } / { lv_role2 }| ).

" COND 也可以 THROW 例外
" DATA(lv_x) = COND i( WHEN lv_a > 0 THEN lv_a ELSE THROW cx_sy_conversion_error( ) ).
cl_demo_output=>display( ).
口訣:「多條件用 COND,單變數用 SWITCH」。兩者都必須有 ELSE(或確定條件涵蓋全部),否則沒命中時回傳型別初始值,容易產生「金額變 0」的隱形錯誤。

7. 迴圈:DO / WHILE / LOOP 與 CHECK / EXIT / CONTINUE

ABAP 共有五大迴圈:DOWHILELOOP ATSELECT ... ENDSELECT(禁用)、PROVIDE(幾乎沒人用)。控制指令四個:

指令作用用在哪
CONTINUE無條件結束本圈,直接跑下一圈所有迴圈
CHECK cond條件不成立時等同 CONTINUE(跳過本圈)迴圈內;在副程式/方法內則是直接離開
EXIT離開最內層迴圈;不在迴圈裡則離開該處理區塊所有迴圈
STOP中止 START-OF-SELECTION,直接跳到 END-OF-SELECTION只在 Report 事件區塊;已列 obsolete
RETURN立刻離開整個副程式/方法方法內建議用它取代 EXIT
最常見的誤解:CHECK 在迴圈裡是「跳過這一圈」,不是「跳出迴圈」;而 EXIT 只跳出一層。雙層迴圈想一次跳出兩層,只能靠旗標變數或把內層抽成方法用 RETURN
REPORT zdatatype15.
" 練習 15:DO / 巢狀 DO / 無 TIMES 的 DO / WHILE
DO 2 TIMES.
  cl_demo_output=>write( |This Index is { sy-index }| ).
ENDDO.

" 巢狀迴圈:SY-INDEX 只代表「當前這一層」的圈數,外層要自己存起來
DO 3 TIMES.
  DATA(lv_outer) = sy-index.
  DO 2 TIMES.
    cl_demo_output=>write( |外層 { lv_outer } / 內層 { sy-index }| ).
  ENDDO.
ENDDO.

" 沒有 TIMES 的 DO 是無窮迴圈,一定要自己 EXIT,否則 timeout
DO.
  IF sy-index = 3.
    EXIT.
  ENDIF.
ENDDO.
cl_demo_output=>write( |離開迴圈後 SY-INDEX 歸零 = { sy-index }| ).

" WHILE:1+2+...+10
DATA(lv_i) = 1.
DATA(lv_sum) = 0.
WHILE lv_i <= 10.
  lv_sum = lv_sum + lv_i.
  lv_i   = lv_i + 1.
ENDWHILE.
cl_demo_output=>write( |1+2+...+10 = { lv_sum }| ).

" 現代等價:REDUCE 一行搞定
DATA(lv_sum2) = REDUCE i( INIT s = 0 FOR n = 1 UNTIL n > 10 NEXT s = s + n ).
cl_demo_output=>write( |REDUCE 版 = { lv_sum2 }| ).
cl_demo_output=>display( ).

雙迴圈的改寫:講義的 ZMMB0021 × EKES 雙層 LOOP,是把採購收貨排程(EKES)的最新一筆補回主表。雙層 LOOP AT ... WHERE 在資料量大時是 O(n×m),S/4HANA 上要改成「先排序+READ TABLE ... BINARY SEARCH」或「用 SORTED / HASHED 內表」或「LOOP AT ... GROUP BY」。

REPORT zloop_two_level.
" 講義的雙迴圈(PO 收貨排程 EKES 回填)改寫成 SORTED 內表 + 單層迴圈
TYPES: BEGIN OF ty_ekes,
         ebeln TYPE ekes-ebeln,
         ebelp TYPE ekes-ebelp,
         etens TYPE ekes-etens,
         eindt TYPE ekes-eindt,
         menge TYPE ekes-menge,
         dabmg TYPE ekes-dabmg,
       END OF ty_ekes.

DATA lt_ekes TYPE SORTED TABLE OF ty_ekes
             WITH NON-UNIQUE KEY ebeln ebelp etens.

SELECT ebeln, ebelp, etens, eindt, menge, dabmg
  FROM ekes
  INTO TABLE @lt_ekes
 WHERE ebeln IN @s_ebeln.               " SELECT-OPTIONS,主機變數一律加 @

LOOP AT lt_po ASSIGNING FIELD-SYMBOL(<ls_po>).
  " 只掃描 key 相符的區段,SORTED 表用二分搜尋定位,不會全表掃
  LOOP AT lt_ekes ASSIGNING FIELD-SYMBOL(<ls_ekes>)
       WHERE ebeln = <ls_po>-ebeln
         AND ebelp = <ls_po>-ebelp
         AND etens <> <ls_po>-ekes_etens.
    CHECK <ls_ekes>-etens IS NOT INITIAL.   " 條件不成立就跳下一圈
    <ls_po>-ekes_eindt = <ls_ekes>-eindt.
    <ls_po>-ekes_etens = <ls_ekes>-etens.
    <ls_po>-ekes_menge = <ls_ekes>-menge.
    <ls_po>-ekes_dabmg = <ls_ekes>-dabmg.
  ENDLOOP.
ENDLOOP.
" LOOP AT ... GROUP BY:取代「排序後自己比 key 換組」的老寫法
LOOP AT lt_items INTO DATA(ls_item)
     GROUP BY ( matnr = ls_item-matnr werks = ls_item-werks )
     ASCENDING
     ASSIGNING FIELD-SYMBOL(<group>).

  DATA(lv_qty) = REDUCE menge_d(
                   INIT q = 0
                   FOR <m> IN GROUP <group>
                   NEXT q = q + <m>-menge ).

  cl_demo_output=>write( |{ <group>-matnr } / { <group>-werks } 合計 { lv_qty }| ).
ENDLOOP.
禁用 SELECT ... ENDSELECT(尤其放在 LOOP 裡)。講義第 10-5 節的反例:迴圈裡逐筆查價格,10 000 筆就是 10 000 次資料庫往返。正解是一次 SELECT ... FOR ALL ENTRIESJOIN 撈進內表,再用 READ TABLE ... BINARY SEARCH / HASHED 表對應。並且 FOR ALL ENTRIES 的驅動內表若為空,會撈出整張表 — 前面一定要加 IF lt_items IS NOT INITIAL.

8. 日期與時間運算

D 型別可以直接加減,單位是;T 型別可以直接加減,單位是。兩個日期相減得到天數(型別要用 I),兩個時間相減得到秒數。

REPORT zdate_basic.
" 日期/時間的基本運算
DATA(lv_today) = sy-datum.

" 1. 本月月初:把 DD 改成 01
DATA(lv_first) = lv_today.
lv_first+6(2) = '01'.

" 2. 上月月底 = 本月月初 - 1
DATA(lv_prev_last) = CONV d( lv_first - 1 ).

" 3. 本月月底 = 下月月初 - 1(+31 天必定落在下個月的 1~4 日)
DATA(lv_next_first) = CONV d( lv_first + 31 ).
lv_next_first+6(2) = '01'.
DATA(lv_last) = CONV d( lv_next_first - 1 ).

" 4. 兩日期相差幾天:直接相減
DATA(lv_days) = CONV i( lv_last - lv_first ) + 1.   " 本月天數

" 5. 時間相減得秒數
DATA lv_t1 TYPE t VALUE '183000'.
DATA lv_t2 TYPE t VALUE '200000'.
DATA(lv_sec)  = CONV i( lv_t2 - lv_t1 ).            " 5400
DATA(lv_hour) = lv_sec / 3600.                      " 注意:存成 I 會四捨五入成 2
DATA(lv_hour_exact) = CONV decfloat34( lv_sec ) / 3600.   " 1.5

cl_demo_output=>write( |月初 { lv_first } 月底 { lv_last } 上月底 { lv_prev_last } 本月 { lv_days } 天| ).
cl_demo_output=>write( |時差 { lv_sec } 秒 = { lv_hour_exact } 小時| ).
cl_demo_output=>display( ).
跨月/跨年陷阱lv_date+4(2) = lv_date+4(2) + 1(月份直接加 1)在 12 月會變成 '13',成為無效日期,後續運算會 dump 或得到怪值。「加 N 個月」一定要用 RP_CALC_DATE_IN_INTERVALCCM_GO_BACK_MONTHS,不要自己算。另外「1/31 加一個月」是 2/28 還是 3/3,各家 FM 行為不同,需求要先講清楚。

系統日期時間欄位的差別(做全球化系統時最容易出錯的地方):

欄位意義使用時機
SY-DATUM / SY-UZEIT應用伺服器的日期/時間(多半是 UTC 或系統時區)寫入 DB 的建檔時間戳
SY-DATLO / SY-TIMLO使用者本地日期/時間(依 SU3 的時區)顯示給使用者看的畫面、報表抬頭
SY-ZONLO使用者的時區代碼(如 UTC+8、CST)做時區轉換的來源
SY-TZONE系統時區與 UTC 的秒差粗略換算
TIMESTAMP (DEC 15)YYYYMMDDHHMMSS,UTC 短時間戳CDS / 變更文件
TIMESTAMPL (DEC 21,7)含 7 位小數秒的長時間戳高頻交易、序號
REPORT ztimestamp_demo.
" 時間戳與時區轉換(S/4HANA 標準做法)
GET TIME STAMP FIELD DATA(lv_ts).                     " UTC 短時間戳 TIMESTAMP
GET TIME STAMP FIELD DATA(lv_tsl).                    " 若宣告為 TIMESTAMPL 則含小數秒

" UTC 時間戳 → 使用者時區的日期時間
CONVERT TIME STAMP lv_ts TIME ZONE sy-zonlo
        INTO DATE DATA(lv_loc_date) TIME DATA(lv_loc_time).

" 日期時間 → UTC 時間戳
CONVERT DATE lv_loc_date TIME lv_loc_time
        INTO TIME STAMP DATA(lv_ts2) TIME ZONE sy-zonlo.

" 兩個時間戳相差幾秒(跨日跨年都正確)
DATA(lv_diff_sec) = cl_abap_tstmp=>subtract( tstmp1 = lv_ts2 tstmp2 = lv_ts ).

" 時間戳加 8 小時
DATA(lv_ts_plus) = cl_abap_tstmp=>add( tstmp = lv_ts secs = 8 * 3600 ).

cl_demo_output=>write( |UTC 時間戳 { lv_ts TIMESTAMP = ISO }| ).
cl_demo_output=>write( |本地時間  { lv_loc_date DATE = USER } { lv_loc_time TIME = USER }(時區 { sy-zonlo })| ).
cl_demo_output=>write( |相差 { lv_diff_sec } 秒| ).
cl_demo_output=>display( ).
ABAP Cloud / RAP 環境不允許直接讀 SY- 欄位,改用 cl_abap_context_info=>get_system_date( )get_system_time( )get_user_technical_name( )get_user_time_zone( )。新專案建議一開始就用這組 API,日後上雲不必改。

工作天(Factory Calendar):SAP 的「幾號到幾號中間有幾個工作天」不能自己算,要走工廠日曆(SCAL 維護,表 TFACD / TFACS,假日表 THOC / THOL)。

REPORT zworkday_demo.
PARAMETERS: p_from TYPE d DEFAULT sy-datum,
            p_to   TYPE d,
            p_cal  TYPE wfcid DEFAULT 'TW'.   " 工廠日曆代碼

START-OF-SELECTION.
  " 1) 判斷某天是不是工作天,並取得工廠日曆序號
  DATA lv_factory_from TYPE fabkl_num.
  DATA lv_wd_ind       TYPE c LENGTH 1.

  CALL FUNCTION 'DATE_CONVERT_TO_FACTORYDATE'
    EXPORTING
      correct_option               = '+'          " 非工作天時往後找
      date                         = p_from
      factory_calendar_id          = p_cal
    IMPORTING
      factorydate                  = lv_factory_from
      workingday_indicator         = lv_wd_ind    " 空白=是工作天
    EXCEPTIONS
      calendar_buffer_not_loadable = 1
      correct_option_invalid       = 2
      date_after_range             = 3
      date_before_range            = 4
      date_invalid                 = 5
      factory_calendar_not_found   = 6
      OTHERS                       = 7.
  IF sy-subrc <> 0.
    MESSAGE |工廠日曆 { p_cal } 讀取失敗 subrc={ sy-subrc }| TYPE 'E'.
  ENDIF.

  " 2) 兩日期之間的工作天數 = 兩個工廠日曆序號相減
  DATA lv_factory_to TYPE fabkl_num.
  CALL FUNCTION 'DATE_CONVERT_TO_FACTORYDATE'
    EXPORTING correct_option      = '-'
              date                = p_to
              factory_calendar_id = p_cal
    IMPORTING factorydate         = lv_factory_to
    EXCEPTIONS OTHERS             = 1.

  DATA(lv_workdays) = CONV i( lv_factory_to - lv_factory_from ) + 1.
  cl_demo_output=>write( |{ p_from } ~ { p_to } 共 { lv_workdays } 個工作天| ).
  cl_demo_output=>display( ).

9. 常用日期/時間函數(FM)速查表

講義第 06 節列的清單全部保留,這是老師要求「用 SE37 這個超級 TCODE 直接測試」的重點資產。

SE37 超級 TCODE 的用法(講義 6-1 節,以 DATE_GET_WEEKFIMA_DAYS_AND_MONTHS_AND_YEARS 為例)(依講義截圖步驟整理)

  1. SE37,函數模組欄位輸入函數名(不確定名字就用 *DATE*WEEK* 之類的萬用字元按 F4 找)。
  2. 按「顯示」看 Import / Export / Changing / Tables / Exceptions 四個頁籤,先搞清楚要餵什麼、會吐什麼。
  3. 執行(F8)直接測試:畫面會列出所有 Import 參數,填好值再按 F8。
  4. 結果畫面會顯示 Export 參數的值、以及 RUNTIME 執行時間(微秒),可以順便比較兩支函數誰快。
  5. 測完滿意後,回到 SE38 程式中按 Ctrl+F6(插入 CALL FUNCTION 樣板),輸入函數名,SAP 會自動把完整的 CALL FUNCTION 骨架連同註解貼進來,只要填值即可。
函數模組用途關鍵參數
RP_CALC_DATE_IN_INTERVAL取輸入日期前/後的年月日(最常用)date, days, months, years, signum('+'/'-') → calc_date
FIMA_DATE_CREATE同上,FI 版本,可指定日曆與月底規則i_date, i_flg_end_of_month, i_months → e_date
RP_LAST_DAY_OF_MONTHS取當月最後一天day_in → last_day_of_month
LAST_DAY_OF_MONTHS同上(另一個名字)day_in → last_day_of_month
BKK_GET_MONTH_LASTDAY同上(銀行模組版)i_date → e_date
HR_JP_MONTH_BEGIN_END_DATE一次拿到月初+月底,最省事iv_date → ev_month_begin_date, ev_month_end_date
CCM_GO_BACK_MONTHS取 N 個月前的日期currdate, backmonths → newdate
MONTH_PLUS_DETERMINE取 N 個月後的日期months, olddate → newdate
DATE_GET_WEEK取該日期是該年第幾週(YYYYWW)date → week
WEEK_GET_FIRST_DAY取某週的第一天week → date
DAY_IN_WEEK該天是星期幾(1=一 … 7=日)datum → wotnr
GET_WEEK_INFO_BASED_ON_DATE該年週數+該週週一/週日日期(週起始=週一)date → week, monday, sunday
HR_GBSSP_GET_WEEK_DATES同上但週起始=週日,另回傳星期幾iv_date → ev_week, ev_start_date, ev_end_date
HOLIDAY_CHECK_AND_GET_INFO判斷某天是否假日date, holiday_calendar_id → holiday_found
DAY_ATTRIBUTES_GET日期屬性批次查(星期幾、第幾天、是否公共假期)date_from, date_to, factory_calendar, holiday_calendar → TABLES day_attributes
DATE_CONVERT_TO_FACTORYDATE日期 → 工廠日曆序號(算工作天)date, factory_calendar_id → factorydate, workingday_indicator
FACTORYDATE_CONVERT_TO_DATE工廠日曆序號 → 日期factorydate, factory_calendar_id → date
CONVERT_DATE_TO_INTERNAL使用者格式日期字串 → 內部 YYYYMMDDdate_external → date_internal(例外 date_external_is_invalid)
CONVERT_DATE_TO_EXTERNAL內部 YYYYMMDD → 使用者格式字串date_internal → date_external
HR_99S_INTERVAL_BETWEEN_DATES兩日期間的天/週/月/年數,含各月起訖日begda, endda → 多個輸出
FIMA_DAYS_AND_MONTHS_AND_YEARS兩日期間的天數/月數/年數i_date_from, i_date_to → e_days, e_months, e_years
FIMA_DATE_COMPARE比較兩日期,回傳 GT / EQ / LTi_date_1, i_date_2 → e_result
SD_DATETIME_DIFFERENCE兩個「日期+時間」相減date1,time1,date2,time2 → datediff, timediff
HRVE_CONVERT_TIME12 小時制 ↔ 24 小時制type, input_time → output_time
MONTH_NAMES_GET取所有月份名稱(依語言)language → TABLES month_names
WEEKDAY_GET取所有星期名稱language → TABLES weekday
F4_DATE彈出日曆讓使用者挑日期date_for_first_month → select_date
F4_CLOCK彈出時鐘讓使用者挑時間→ select_time
POPUP_TO_SELECT_MONTH彈出年月選擇視窗actual_month → selected_month
STRING_REVERSE字串反轉(作業三要求「不准用它」自己寫)string, lang → rstring
CONVERSION_EXIT_ALPHA_INPUT / OUTPUT前置補零 / 去掉前置零input → output
REPORT zdatatype16.
" 練習 16:呼叫日期 FM 取「本月/上月/去年同期」
DATA(lv_today) = sy-datum.

" 上個月同日
CALL FUNCTION 'RP_CALC_DATE_IN_INTERVAL'
  EXPORTING date      = lv_today
            days      = 0
            months    = 1
            signum    = '-'          " '+' 未來、'-' 過去
            years     = 0
  IMPORTING calc_date = DATA(lv_prev_month).

" 去年同期
CALL FUNCTION 'RP_CALC_DATE_IN_INTERVAL'
  EXPORTING date      = lv_today
            days      = 0
            months    = 0
            signum    = '-'
            years     = 1
  IMPORTING calc_date = DATA(lv_last_year).

" 一次拿到本月月初與月底
CALL FUNCTION 'HR_JP_MONTH_BEGIN_END_DATE'
  EXPORTING iv_date              = lv_today
  IMPORTING ev_month_begin_date  = DATA(lv_bom)
            ev_month_end_date    = DATA(lv_eom).

" 標準日期字串 → 內部格式(處理 EXCEL / CSV 匯入最常用)
DATA lv_int_date TYPE d.
CALL FUNCTION 'CONVERT_DATE_TO_INTERNAL'
  EXPORTING date_external            = '2025/09/30'
  IMPORTING date_internal            = lv_int_date
  EXCEPTIONS date_external_is_invalid = 1
             OTHERS                   = 2.
IF sy-subrc <> 0.
  MESSAGE '外部日期格式不合法' TYPE 'E'.
ENDIF.

cl_demo_output=>write( |本月 { lv_today+0(6) }/上月 { lv_prev_month+0(6) }/去年同期 { lv_last_year+0(6) }| ).
cl_demo_output=>write( |月初 { lv_bom } 月底 { lv_eom } 轉入日期 { lv_int_date }| ).
cl_demo_output=>display( ).
日期的外部格式取決於使用者主檔SU3 → 預設值 → 日期格式/小數點格式)。所以同一支程式,A 使用者看到 30.09.2025、B 使用者看到 2025/09/30資料庫裡永遠是 YYYYMMDD;要比較、要排序、要存檔一律用內部格式,只有畫面輸出才轉外部格式。

10. 字串處理:八大指令與現代寫法

講義整理的「字串處理八大指令」,配上 S/4HANA 建議的現代替代方案:

指令作用現代替代 / 備註
FIND搜尋字串(可 REGEX、可全找)取代 SEARCH;或用函數 find( )count( )
REPLACE字串置換函數版 replace( val = sub = with = occ = )
TRANSLATE大小寫轉換/逐字元對應置換函數版 to_upper( ) to_lower( ) translate( )
SHIFT整串位移、刪頭刪尾函數版 shift_left( ) shift_right( )
CONDENSE去前置空白並壓縮中間空白函數版 condense( val = del = from = to = )
OVERLAY用另一串填補空白位置少用,保留認識即可
CONCATENATE字串相接改用 && 或字串範本 | |
SPLIT字串切割切成內表用 SPLIT ... INTO TABLE;取第 n 段用 segment( )
REPORT zdatatype10.
" 練習 10:SHIFT / REPLACE / TRANSLATE(保留敘述式,再對照函數式)
DATA(lv_str) = |abcdefg|.

SHIFT lv_str BY 3 PLACES LEFT.       " → defg
SHIFT lv_str BY 3 PLACES RIGHT.      " → 前面補 3 個空白
SHIFT lv_str LEFT DELETING LEADING space.   " 刪掉前置空白 = CONDENSE

lv_str = |abcdefg|.
SHIFT lv_str LEFT  DELETING LEADING  'abc'.   " 刪掉左邊的 abc → defg
SHIFT lv_str RIGHT DELETING TRAILING 'efg'.   " 刪掉右邊的 efg
" 實務常見用途:清掉動態 WHERE 條件尾巴的 ' AND '
"(不過老師建議:條件開頭直接寫 1 = 1,就不必砍尾巴了)

DATA lv_rep TYPE c LENGTH 10 VALUE 'ABCDEFGHI'.
REPLACE 'DEF' WITH '123' INTO lv_rep.          " → ABC123GHI

DATA lv_city TYPE c LENGTH 6 VALUE 'taipei'.
TRANSLATE lv_city TO UPPER CASE.               " → TAIPEI
TRANSLATE lv_city TO LOWER CASE.               " → taipei

" === 同樣的事,用函數式一行完成(不改動原變數,可寫在運算式裡) ===
DATA(lv_f1) = shift_left(  val = |  abcdefg| sub = ` ` ).   " 去左空白
DATA(lv_f2) = replace( val = |ABCDEFGHI| sub = 'DEF' with = '123' occ = 1 ).
DATA(lv_f3) = to_upper( |taipei| ).
DATA(lv_f4) = to_mixed( val = 'TAIPEI CITY' sep = ' ' ).    " → Taipei City

cl_demo_output=>write( |{ lv_f1 } / { lv_f2 } / { lv_f3 } / { lv_f4 }| ).
cl_demo_output=>display( ).

SEARCH(舊)vs FIND(新):講義的練習 11 用 SEARCH ... FOR ...,成功時 SY-SUBRC = 0SY-FDPOS 是起始位置(從 0 開始算),失敗時 SY-SUBRC = 4SY-FDPOS = 0。搜尋 *@. 這類特殊符號時,要在前後各加一個句點(如 '.*.')。

REPORT zdatatype11.
" 練習 11:SEARCH(舊)改寫成 FIND(新)
DATA(lv_text) = |Welcome to SAP world!|.

" --- 舊寫法(僅供辨識別人的程式) ---
" SEARCH lv_text FOR 'rld'.    "  SY-SUBRC=0、SY-FDPOS=17(位置從 0 起算)
" SEARCH lv_text FOR '.*.'.    "  找特殊字元 * 要前後加句點

" --- 新寫法:FIND,位置與次數都用具名輸出,不必看 SY- ---
FIND FIRST OCCURRENCE OF 'rld' IN lv_text
     MATCH OFFSET DATA(lv_off)
     MATCH LENGTH DATA(lv_len).
IF sy-subrc = 0.
  cl_demo_output=>write( |找到 'rld',位置 { lv_off },長度 { lv_len }| ).
ELSE.
  cl_demo_output=>write( |找不到| ).
ENDIF.

" 只想知道「有沒有」→ 用 CS 或 contains( )
IF lv_text CS 'SAP'.
  cl_demo_output=>write( |CS:字串含有 SAP| ).
ENDIF.
IF contains( val = lv_text sub = 'SAP' ).
  cl_demo_output=>write( |contains( ):字串含有 SAP| ).
ENDIF.

" 想知道「有幾個」→ count( );想拿位置 → find( )
cl_demo_output=>write( |字母 o 出現 { count( val = lv_text sub = 'o' ) } 次| ).
cl_demo_output=>write( |'world' 位置 { find( val = lv_text sub = 'world' ) }| ).

" 找全部出現位置 → FIND ALL OCCURRENCES ... RESULTS
FIND ALL OCCURRENCES OF 'o' IN lv_text RESULTS DATA(lt_matches).
LOOP AT lt_matches INTO DATA(ls_match).
  cl_demo_output=>write( |第 { sy-tabix } 個 o 在 offset { ls_match-offset }| ).
ENDLOOP.
cl_demo_output=>display( ).

正規表示式FIND / REPLACE 都可以加 REGEX,字串函數也有 matches( ) / match( ) / count( ) 的 regex 版本。

REPORT zregex_demo.
" 正規表示式:抓出字串中所有數字段落、驗證 Email、遮罩身分證
DATA(lv_src) = |PO 4500001234 於 2025/09/30 收貨 120 EA|.

" 1) 全部數字段落
FIND ALL OCCURRENCES OF REGEX '\d+' IN lv_src RESULTS DATA(lt_nums).
LOOP AT lt_nums INTO DATA(ls_num).
  cl_demo_output=>write( |數字段:{ substring( val = lv_src off = ls_num-offset len = ls_num-length ) }| ).
ENDLOOP.

" 2) 驗證格式(matches 回傳 abap_bool)
IF matches( val = |casper@ncu.edu.tw| regex = '^[\w\.\-]+@[\w\-]+(\.[\w\-]+)+$' ) = abap_true.
  cl_demo_output=>write( |Email 格式正確| ).
ENDIF.

" 3) 遮罩:把身分證中間四碼換成 ****
DATA(lv_id) = replace( val   = |A123456789|
                       regex = '^(.{4})(.{4})(.*)$'
                       with  = '$1****$3'
                       occ   = 1 ).
cl_demo_output=>write( |遮罩後 { lv_id }| ).
cl_demo_output=>display( ).
S/4HANA 1809 的核心是 ABAP 7.52/7.53,REGEX 用的是 SAP 自家的 POSIX 風格引擎。ABAP 7.55(S/4HANA 2020)之後才新增 PCRE 附加項(FIND ... PCRE ...matches( val = ... pcre = ... )),效能與相容性都比較好,SAP 也建議新程式改用 PCRE。寫程式前先確認系統版本,1809 上寫 PCRE 會語法錯誤。

11. 字串相接、切割、壓縮

相接有四種寫法,老師要求新程式一律用後兩種:

REPORT zdatatype12.
" 練習 12:CONCATENATE / & / && / 字串範本 | |
DATA(lv_cash) = CONV p( '123.456' ).   " 實務上宣告 TYPE p LENGTH 8 DECIMALS 3

" 1. 舊寫法(仍合法,處理固定長度 C 欄位時仍好用)
DATA lv_r1 TYPE string.
CONCATENATE 'Hello' 'SAP' INTO lv_r1 SEPARATED BY space.

" 2. & :只能接「字面值」,編譯期就併好(不能接變數)
DATA(lv_r2) = `string1 ` & ` string2`.

" 3. && :執行期串接,可接變數,會自動轉字串
DATA(lv_r3) = `string1 ` && lv_cash && ` string2`.

" 4. 字串範本 |...{ }...| :最推薦,可帶格式化選項
DATA(lv_r4) = |string1 { lv_cash } string2|.
DATA(lv_r5) = |金額右靠齊 [{ lv_cash WIDTH = 12 ALIGN = RIGHT }]|.
DATA(lv_r6) = |置中 [{ lv_cash WIDTH = 12 ALIGN = CENTER PAD = '.' }]|.

" 字串範本中的跳脫字元:\| \{ \} \\
DATA(lv_r7) = |跳脫示範 \| \{ \} \\|.

cl_demo_output=>write( lv_r1 ).
cl_demo_output=>write( lv_r3 ).
cl_demo_output=>write( lv_r4 ).
cl_demo_output=>write( lv_r5 ).
cl_demo_output=>write( lv_r6 ).
cl_demo_output=>write( lv_r7 ).
cl_demo_output=>display( ).

字串範本的格式化選項是把數字/日期漂亮印出來的關鍵,常用如下:

選項作用範例
NUMBER = USER依使用者的小數點/千分位設定|{ lv_amt NUMBER = USER }|
NUMBER = RAW原始值,不做在地化寫檔/介面用
DATE = USER / ISO / ENVIRONMENT日期格式|{ sy-datum DATE = ISO }| → 2025-09-30
TIME = USER / ISO時間格式|{ sy-uzeit TIME = ISO }|
TIMESTAMP = ISO時間戳格式
CURRENCY = 'TWD'依幣別小數位輸出金額|{ lv_amt CURRENCY = 'JPY' }|
DECIMALS = n指定小數位|{ lv_p DECIMALS = 2 }|
WIDTH / ALIGN / PAD欄寬、對齊、填補字元做固定寬度報表/文字檔
SIGN = LEFT / LEFTPLUS / RIGHT正負號位置負數顯示成 -100 而非 100-
ALPHA = IN / OUT前置補零 / 去前置零|{ lv_matnr ALPHA = OUT }| 取代 CONVERSION_EXIT_ALPHA_OUTPUT
CASE = UPPER / LOWER大小寫
XSD = XXML/JSON 標準格式介面用
REPORT zdatatype13.
" 練習 13:SPLIT / CONDENSE / CLEAR,並附作業六、七的完整解
DATA(lv_names) = |HELLO,CASPER,SAP|.

" 1) 切成固定幾個變數(剩下的全部塞給最後一個)
SPLIT lv_names AT ',' INTO DATA(lv_1) DATA(lv_2).
cl_demo_output=>write( |lv_1={ lv_1 } lv_2={ lv_2 }| ).

" 2) 切成內表(最常用)
SPLIT |ONE,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINTH,TEN,ELEVEN,TWELVE|
      AT ',' INTO TABLE DATA(lt_words).

" 3) 只想取第 n 段 → segment( ),不必先切成內表
cl_demo_output=>write( |第 3 段 = { segment( val = lv_names sep = ',' index = 3 ) }| ).

" === 作業六+作業七:輸入 '  pad , book ,phone ,  earphone ',去空白後排序輸出 ===
DATA(lv_input) = |  pad   , book ,phone ,  earphone |.
SPLIT lv_input AT ',' INTO TABLE DATA(lt_items).

LOOP AT lt_items ASSIGNING FIELD-SYMBOL(<lv_item>).
  CONDENSE <lv_item>.                 " 去前後空白並壓縮中間空白
ENDLOOP.
DELETE lt_items WHERE table_line IS INITIAL.
SORT lt_items ASCENDING.               " 作業七:遞增排序

LOOP AT lt_items INTO DATA(lv_item).
  cl_demo_output=>write( lv_item ).     " book / earphone / pad / phone
ENDLOOP.

" 4) CONDENSE 的兩種模式
DATA lv_name TYPE c LENGTH 30.
lv_name+0(10)  = '  Dr.'.
lv_name+10(10) = 'Michael'.
lv_name+20(10) = 'Hofmann'.
CONDENSE lv_name.                       " → 'Dr. Michael Hofmann'(中間留一格)
cl_demo_output=>write( |CONDENSE      : [{ lv_name }]| ).

DATA lv_t TYPE c LENGTH 10 VALUE ' abc abc '.
CONDENSE lv_t NO-GAPS.                  " → 'abcabc'(中間空白也刪光)
cl_demo_output=>write( |CONDENSE NO-GAPS: [{ lv_t }]| ).

CLEAR lv_t.                             " 清成初始值(常數不能 CLEAR)
cl_demo_output=>write( |CLEAR 之後     : [{ lv_t }]| ).
cl_demo_output=>display( ).
STRLEN( )C 型別會忽略尾端空白char20 只放 13 個字,strlen 回傳 13),但對 STRING 型別會如實計算含尾端空白的長度。做「補到固定寬度」的文字檔輸出時,這個差異會造成欄位錯位。要算「佔幾個位元組」請用 numofchar( )xstrlen( ) 區分。

字串函數速查(全部可寫在運算式裡,不改動原變數):

函數作用
strlen( ) / numofchar( ) / xstrlen( )字元數 / 有效字元數 / 位元組數
substring( val = off = len = )取子字串
substring_before / after / from / to依分隔子字串取前段/後段
segment( val = sep = index = )取第 n 段(等同 SPLIT 後 READ)
to_upper / to_lower / to_mixed / from_mixed大小寫與駝峰轉換
shift_left / shift_right( val = sub = places = circular = )位移/刪頭尾
condense( val = del = from = to = )壓縮空白,可指定要刪的字元
replace( val = sub = with = occ = )置換,occ = 0 表示全部
translate( val = from = to = )逐字元對應置換
insert( val = sub = off = )插入
repeat( val = occ = )重複 n 次(做分隔線很方便)
reverse( )字串反轉(作業三的標準答案)
concat_lines_of( table = sep = )把內表所有列串成一個字串
escape( val = format = )跳脫(URL / XML / JSON / SQL)
contains( ) / matches( ) / count( ) / find( )包含判斷 / 格式驗證 / 計次 / 找位置
cl_abap_codepage=>convert_to / convert_fromSTRING ↔ XSTRING(取代 CL_ABAP_CONV_OUT_CE)
REPORT zstring_reverse.
" 作業三:不准用 STRING_REVERSE,自己把 'I-LOVE-ABAP' 反轉成 'PABA-EVOL-I'
DATA(lv_s) = |I-LOVE-ABAP|.

" 解法 A:講義的 DO + offset 逐字取(最貼近作業要求)
DATA lv_out TYPE string.
DATA(lv_n) = strlen( lv_s ).
DO lv_n TIMES.
  DATA(lv_pos) = lv_n - sy-index.
  lv_out = lv_out && substring( val = lv_s off = lv_pos len = 1 ).
ENDDO.
cl_demo_output=>write( |解法 A:{ lv_out }| ).

" 解法 B:內建函數(知道就好,作業不准用)
cl_demo_output=>write( |解法 B:{ reverse( lv_s ) }| ).

" 解法 C:REDUCE 一行版
cl_demo_output=>write( |解法 C:{ REDUCE string( INIT r = `` FOR i = 1 UNTIL i > strlen( lv_s )
                                                 NEXT r = substring( val = lv_s off = i - 1 len = 1 ) && r ) }| ).
cl_demo_output=>display( ).

12. 金額與數量的小數處理(CURR / QUAN / TCURX)

SAP 的金額欄位(DDIC 型別 CURR)與數量欄位(QUAN)在資料庫裡都是 P 型別,而且一定要搭配一個參考欄位:CURR 參考幣別欄位(CUKY,如 WAERS),QUAN 參考單位欄位(UNIT,如 MEINS)。沒有參考欄位,SE11 不讓你啟用。

TCURX 陷阱(實務上最貴的一個 bug):資料庫裡的 CURR 欄位一律以「2 位小數」儲存。若幣別的實際小數位不是 2(如 JPY 日圓 0 位、KWD 科威特第納爾 3 位,定義在表 TCURX),DB 裡的數字與畫面上的數字會差 10 的次方倍。
例:日圓 1000 元,BSEG-WRBTR 存的是 10.00;你若直接 SELECT 出來加總並輸出,會少 100 倍。只要用 ALV/WRITE 並指定 CURRENCY 參考欄位,SAP 會自動換算;但若你把它丟給 Excel、丟給介面、或轉成 STRING,就必須自己呼叫換算函數。
REPORT zcurrency_demo.
" 幣別小數位換算:DB 內部值 ↔ 外部顯示值
PARAMETERS: p_waers TYPE waers DEFAULT 'JPY',
            p_amt   TYPE bseg-wrbtr DEFAULT '10.00'.   " DB 內部值

START-OF-SELECTION.
  " 1) 查這個幣別有幾位小數(TCURX;查不到就是 2 位)
  CALL FUNCTION 'BAPI_CURRENCY_GETDECIMALS'
    EXPORTING currency        = p_waers
    IMPORTING decimals        = DATA(lv_dec).

  " 2) 內部值 → 外部值(給人看、給介面)
  DATA lv_ext TYPE bapicurr-bapicurr.
  CALL FUNCTION 'BAPI_CURRENCY_CONV_TO_EXTERNAL'
    EXPORTING currency             = p_waers
              amount_internal      = p_amt
    IMPORTING amount_external      = lv_ext.

  " 3) 外部值 → 內部值(要寫回 SAP 表 / 呼叫 BAPI 前)
  DATA lv_int TYPE bseg-wrbtr.
  CALL FUNCTION 'BAPI_CURRENCY_CONV_TO_INTERNAL'
    EXPORTING currency             = p_waers
              amount_external      = lv_ext
              max_number_of_digits = 23
    IMPORTING amount_internal      = lv_int.

  cl_demo_output=>write( |{ p_waers } 小數位 = { lv_dec }| ).
  cl_demo_output=>write( |DB 內部值 { p_amt } → 外部值 { lv_ext } → 再轉回 { lv_int }| ).

  " 4) 直接輸出時,用字串範本的 CURRENCY 選項讓系統自己處理
  cl_demo_output=>write( |格式化輸出:{ p_amt CURRENCY = p_waers NUMBER = USER }| ).
  cl_demo_output=>display( ).

同一組函數還有 IDoc / BAPI / 顯示三種變體,處理介面時很常用:

函數方向典型場景
BAPI_CURRENCY_GETDECIMALS查小數位先問清楚再換算
BAPI_CURRENCY_CONV_TO_EXTERNAL內部 → 外部輸出報表、寫 CSV
BAPI_CURRENCY_CONV_TO_INTERNAL外部 → 內部呼叫 BAPI 前的準備
CURRENCY_AMOUNT_SAP_TO_IDOCSAP → IDocEDI / ALE 介面
CURRENCY_AMOUNT_IDOC_TO_SAPIDoc → SAP介面回寫
CURRENCY_AMOUNT_SAP_TO_DISPLAYSAP → 顯示Dialog 畫面
CONVERT_TO_LOCAL_CURRENCY外幣 → 本位幣依 TCURR 匯率換算
CONVERT_TO_FOREIGN_CURRENCY本位幣 → 外幣同上,反方向
UNIT_CONVERSION_SIMPLE數量單位換算KG ↔ G、EA ↔ CAR(表 T006)
MD_CONVERT_MATERIAL_UNIT物料專屬單位換算依 MARM 的物料換算率
老師的實務口訣:「金額三件事 — 型別用 P、一定帶幣別、輸出前先問小數位」。另外「加總金額」的內表欄位務必宣告得比來源長(例如來源 CURR 13,2,加總欄位用 TYPE p LENGTH 13 DECIMALS 2 甚至更長),否則百萬筆加總會 CX_SY_ARITHMETIC_OVERFLOW

13. SY 系統欄位:一定要記住的十個

系統欄位放在結構 SYSTSE11 可查全部)。老師點名的十個:

#欄位意義使用要點
1SY-SUBRC上一個指令的回傳碼,0=成功資料庫/READ/CALL FUNCTION 之後必檢查;下一個指令就會被覆寫,要用就馬上存起來
2SY-UNAME登入者帳號權限判斷、記錄異動者
3SY-DATUM系統日期寫入 DB 用;顯示給人看用 SY-DATLO
4SY-UZEIT系統時間同上,本地時間用 SY-TIMLO
5SY-TCODE目前執行的交易碼增強/BAdI 裡判斷「從哪個畫面進來的」
6SY-INDEXDO / WHILE 目前跑第幾圈巢狀時只代表最內層;離開迴圈後歸零
7SY-TABIXLOOP AT / READ TABLE 目前是內表第幾筆只對 INDEX TABLE 有意義;HASHED 表恆為 0
8SY-MANDT用戶端(Client)開表時第一個欄位;ABAP SQL 會自動加條件
9SY-ULINE橫線字串WRITE 報表畫線
10SY-VLINE直線字元WRITE 報表畫格線

另外五個高頻但講義未列的:SY-DBCNT(上個 SQL 影響/讀到幾筆)、SY-REPID(目前程式名)、SY-LANGU(登入語言)、SY-DYNNR(目前畫面號碼)、SY-BATCH(是否背景執行,'X'=是)、SY-LINNO / SY-PAGNO(清單列號/頁碼)、SY-FDPOS(SEARCH 找到的位置)、SY-LISEL(游標所在那一列的內容)。

REPORT zsy_fields.
" SY-SUBRC 的正確用法:立刻檢查、立刻存起來
SELECT SINGLE matnr, maktx
  FROM makt
  INTO @DATA(ls_makt)
 WHERE matnr = @p_matnr
   AND spras = @sy-langu.

DATA(lv_rc) = sy-subrc.        " 馬上存起來,後面的指令會蓋掉它
IF lv_rc <> 0.
  MESSAGE |物料 { p_matnr } 找不到描述| TYPE 'S' DISPLAY LIKE 'W'.
ELSE.
  cl_demo_output=>write( |{ ls_makt-matnr ALPHA = OUT } { ls_makt-maktx }| ).
ENDIF.

" SY-DBCNT:上一個 SQL 讀到/異動幾筆
SELECT matnr, mtart FROM mara INTO TABLE @DATA(lt_mara) UP TO 10 ROWS
  ORDER BY matnr.                       " 沒有 ORDER BY 就沒有順序保證
cl_demo_output=>write( |讀到 { sy-dbcnt } 筆(lines( ) = { lines( lt_mara ) })| ).

" SY-BATCH:背景執行時不要跳對話框
IF sy-batch = abap_false.
  " 只有前景才彈視窗
ENDIF.
cl_demo_output=>display( ).
SELECT SINGLE 在沒有指定完整主鍵時,不保證取到哪一筆(HANA 上的結果順序可能每次都不同)。若語意是「取最新一筆」,一定要寫 SELECT ... ORDER BY ... UP TO 1 ROWS 或用 MAX( ) 子查詢。這正是專案裡「成本一下抓到 H、一下抓到 E」這類不穩定結果的根本原因。

14. 資料輸出:WRITE 與現代替代方案

WRITE 是傳統 List 輸出。老師的態度很明確:「會 ALV 的話 WRITE 不必太深入,遇到再查」。但看懂舊程式、做套版列印(對位報表)時還是需要。

語法:WRITE AT [/] [<pos>] [(<len>)] <資料項> <格式參數>/=換行、pos=第幾欄開始、(len)=顯示長度。

格式參數作用
LEFT-JUSTIFIED / CENTERED / RIGHT-JUSTIFIED靠左 / 置中 / 靠右
UNDER <g>對齊到欄位 g 的起始欄位置
NO-GAP緊接著上一個欄位,不留空格
USING EDIT MASK <m> / USING NO EDIT MASK套用內嵌字元遮罩,如 '__:__:__'
NO-ZERO前置 0 不顯示
NO-SIGN不顯示正負號
DECIMALS <d>顯示 d 位小數
EXPONENT <e>浮點數指數位
ROUND <r>以 10^r 為單位四捨五入(如 ROUND 3=以千為單位)
CURRENCY <c>依幣別小數位顯示金額(金額欄位務必加
UNIT <u>依單位小數位顯示數量
DD/MM/YY、MM/DD/YY、DD/MM/YYYY、MM/DD/YYYY、DDMMYY、MMDDYY、YYMMDD日期格式
AS CHECKBOX / AS SYMBOL / AS ICON / AS LINE輸出成勾選框 / 符號 / 圖示 / 線條
COLOR n、INTENSIFIED ON/OFF、INVERSE ON/OFF、HOTSPOT ON顏色、加深、反白、熱點
SKIP [n]、SKIP TO LINE n、NEW-PAGE、ULINE、NEW-LINE空行 / 跳到第 n 行 / 換頁 / 畫橫線 / 換行
REPORT zdatatype04 LINE-SIZE 120.
" 練習 04:CHECKBOX、SYMBOL、ICON、顏色(TCODE: ICON 可查所有圖示名稱;
"          SE11 → Type Group 輸入 SYM / ICON 也看得到)
INCLUDE <symbol>.
INCLUDE <icon>.

DATA(lv_flag1) = ' '.
DATA(lv_flag2) = 'X'.

WRITE: /12 'CHECK FLAG 1:', lv_flag1 AS CHECKBOX.
WRITE: /12 'CHECK FLAG 2:', lv_flag2 AS CHECKBOX.
WRITE: /12 'Phone Symbol:', sym_phone  AS SYMBOL.
WRITE: /12 'Alarm  Icon :', icon_alarm AS ICON.

" 八種背景色 + 三種字體效果
DO 8 TIMES.
  DATA(lv_col) = sy-index - 1.
  FORMAT COLOR = lv_col.
  WRITE: / lv_col           COLOR OFF,
           'INTENSIFIED ON'  INTENSIFIED ON,
           'INTENSIFIED OFF' INTENSIFIED OFF,
           'INVERSE ON'      INVERSE ON.
ENDDO.
FORMAT COLOR OFF.

基本報表(BASIC LIST REPORT)的骨架:講義第 10 節示範 TOP-OF-PAGE / END-OF-PAGE / AT LINE-SELECTION 與內表控制中斷(AT FIRST / AT NEW / AT END OF / AT LAST),並用 READ LINE ... OF PAGE ... + MODIFY CURRENT LINE 回頭把總頁數 '***' 換成實際頁數。這是套版對位列印的經典技巧。

REPORT zdatatype20 NO STANDARD PAGE HEADING LINE-SIZE 160 LINE-COUNT 40(3).
" 基本報表:ACDOCA 傳票明細(S/4HANA 已用 ACDOCA 取代 BKPF/BSEG 的查詢角色)
TYPES: BEGIN OF ty_out,
         bldat TYPE acdoca-bldat,   " 憑證日期
         usnam TYPE acdoca-usnam,   " 輸入人員
         belnr TYPE acdoca-belnr,   " 傳票號碼
         blart TYPE acdoca-blart,   " 文件類型
         bschl TYPE acdoca-bschl,   " 過帳代碼
         racct TYPE acdoca-racct,   " 會計科目
         rwcur TYPE acdoca-rwcur,   " 交易幣別
         tsl   TYPE acdoca-tsl,     " 交易金額
       END OF ty_out.

DATA gt_out TYPE STANDARD TABLE OF ty_out WITH EMPTY KEY.

SELECT-OPTIONS: s_bldat FOR sy-datum,
                s_usnam FOR sy-uname.
PARAMETERS      p_blart TYPE acdoca-blart DEFAULT 'WA'.

START-OF-SELECTION.
  " 明列欄位、主機變數加 @、用列舉式 SELECT-OPTIONS(區間與列舉的結果可能不同)
  SELECT bldat, usnam, belnr, blart, bschl, racct, rwcur, tsl
    FROM acdoca
    INTO TABLE @gt_out
   WHERE rldnr = '0L'                 " 帳簿:只取主要分類帳
     AND bldat IN @s_bldat
     AND blart = @p_blart
     AND usnam IN @s_usnam
   ORDER BY bldat, usnam, belnr.      " HANA 上沒有 ORDER BY 就沒有順序保證

  IF gt_out IS INITIAL.
    MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  LOOP AT gt_out INTO DATA(ls_out).
    AT NEW usnam.
      WRITE: / |── 輸入人員 { ls_out-usnam } ──|.
    ENDAT.

    WRITE: /010 sy-vline,
            014 sy-tabix,
            030 sy-vline,
            034 ls_out-belnr,
            050 sy-vline,
            054 ls_out-racct,
            070 sy-vline,
            074 ls_out-rwcur,
            090 sy-vline,
            094 ls_out-tsl CURRENCY ls_out-rwcur,   " 金額一定要帶 CURRENCY
            160 sy-vline.

    AT END OF usnam.
      SUM.                                " 自動加總數值欄位
      WRITE: /034 '小計', 094 ls_out-tsl CURRENCY ls_out-rwcur.
    ENDAT.
  ENDLOOP.

TOP-OF-PAGE.
  WRITE: /070 'χχ科技股份有限公司'.
  WRITE: /076 '傳票明細表'.
  WRITE: /135 'Page:', sy-pagno.
  WRITE: /010 sy-uline(151).

END-OF-PAGE.
  WRITE: /060 '--------------------- END OF PAGE ---------------------'.

AT LINE-SELECTION.
  " 雙擊某列時,用視窗顯示該列內容
  WINDOW STARTING AT 21 10 ENDING AT 150 18.
  WRITE: /003 sy-lisel.
控制中斷四兄弟AT FIRST(第一筆,只跑一次)→ AT NEW f(欄位 f 換值時)→ 明細 → AT END OF f(欄位 f 即將換值前,可搭 SUM 自動小計)→ AT LAST(最後一筆)。使用前必須先 SORT,而且 AT NEW f 是看「f 及其左邊所有欄位」的組合有沒有變,不是只看 f。
現代替代:全螢幕報表請優先用 cl_salv_table=>factory( )(三行就有排序/篩選/匯出/版面配置);需要編輯與事件才用 CL_GUI_ALV_GRIDREUSE_ALV_GRID_DISPLAYTYPE-POOLS: slis 仍然存在(講義附錄的 ZUSERS_002 就是),但只建議用來讀懂舊程式。純除錯輸出用 cl_demo_output 最快。
REPORT zsalv_quick.
" 講義附錄 ZUSERS_002(VBAK/VBAP 主子檔)的現代版:一次 JOIN,三行 ALV
SELECT-OPTIONS s_vbeln FOR ('VBAK-VBELN').

START-OF-SELECTION.
  SELECT p~vbeln,
         p~posnr,
         p~matnr,
         p~kwmeng,
         p~netwr,
         p~waerk,
         k~erdat,
         k~kunnr
    FROM vbap AS p
    INNER JOIN vbak AS k ON k~vbeln = p~vbeln
    INTO TABLE @DATA(lt_out)
   WHERE p~vbeln IN @s_vbeln
   ORDER BY p~vbeln, p~posnr.

  IF lt_out IS INITIAL.
    MESSAGE '查無訂單資料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_out ).
      lo_alv->get_functions( )->set_all( abap_true ).      " 開啟所有工具列功能
      lo_alv->get_columns( )->set_optimize( abap_true ).   " 自動調欄寬
      lo_alv->get_display_settings( )->set_striped_pattern( abap_true ).
      lo_alv->get_display_settings( )->set_list_header( |ZUSERS 訂單列表(VBAK / VBAP)| ).
      lo_alv->display( ).
    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.

15. 實用小案例(講義第 07 / 09 / 10 節)

案例 1:用帳號前四碼區分權限(第 09-1 節)。

" 舊寫法用 substring( ),新寫法可再簡化成一個 COND 或內表比對
DATA(lv_user4) = substring( val = sy-uname off = 0 len = 4 ).
DATA(lv_switch) = COND abap_bool( WHEN lv_user4 = 'T1MC' OR lv_user4 = 'T1PC'
                                  THEN abap_true ELSE abap_false ).

" 更好:把允許清單抽成常數內表,日後維護不必改程式邏輯
DATA(lt_allow) = VALUE string_table( ( `T1MC` ) ( `T1PC` ) ( `T1SD` ) ).
DATA(lv_ok) = xsdbool( line_exists( lt_allow[ table_line = lv_user4 ] ) ).

案例 2:西元轉民國年(作業四)。

REPORT zroc_date.
" 20250930 → 1140930;重點在 N / I / C 的型別轉換
DATA(lv_today) = sy-datum.

DATA(lv_year_ad)  = CONV i( lv_today+0(4) ).      " C → I
DATA(lv_year_roc) = lv_year_ad - 1911.            " 民國年

" 用字串範本 + WIDTH/PAD 補零,不必宣告一堆中間變數
DATA(lv_roc) = |{ lv_year_roc WIDTH = 3 ALIGN = RIGHT PAD = '0' }{ lv_today+4(4) }|.

cl_demo_output=>write( |西元 { lv_today } → 民國 { lv_roc }| ).

" 反向:民國轉西元
DATA(lv_roc_in) = |1140930|.
DATA(lv_ad) = CONV d( |{ CONV i( lv_roc_in+0(3) ) + 1911 }{ lv_roc_in+3(4) }| ).
cl_demo_output=>write( |民國 { lv_roc_in } → 西元 { lv_ad }| ).
cl_demo_output=>display( ).

案例 3:Z-score 與歐幾里德距離(作業一、二、十)。Z-score=某數值距離平均值幾個標準差,用途是「不同單位的數值也能互相比較」,在資料探勘用來找異常值。

Z-score解釋
0正好是平均值
> 0高於平均值
< 0低於平均值
> 2 或 < -2比較「極端」的值(超出約 95% 區間)
REPORT zscore_demo.
" 作業一/二/十:平均、母體標準差、Z-score、歐氏距離
TYPES ty_num_tab TYPE STANDARD TABLE OF decfloat34 WITH EMPTY KEY.
DATA(lt_val) = VALUE ty_num_tab( ( 10 ) ( 12 ) ( 23 ) ( 23 ) ( 16 ) ( 23 ) ( 21 ) ( 16 ) ).

DATA(lv_n)   = CONV decfloat34( lines( lt_val ) ).
DATA(lv_sum) = REDUCE decfloat34( INIT s = 0 FOR v IN lt_val NEXT s = s + v ).
DATA(lv_avg) = lv_sum / lv_n.                                   " = AVERAGE( )

" 變異數 = 「標準差還沒開根號的值」(母體 VAR.P)
DATA(lv_var) = REDUCE decfloat34( INIT s = 0 FOR v IN lt_val
                                  NEXT s = s + ( v - lv_avg ) ** 2 ) / lv_n.
DATA(lv_std) = sqrt( lv_var ).                                  " = STDEV.P( )

cl_demo_output=>write( |筆數 { lv_n } 總和 { lv_sum } 平均 { lv_avg DECIMALS = 4 }| ).
cl_demo_output=>write( |變異數 { lv_var DECIMALS = 4 } 標準差 { lv_std DECIMALS = 4 }| ).

LOOP AT lt_val INTO DATA(lv_v).
  DATA(lv_z) = COND decfloat34( WHEN lv_std > 0 THEN ( lv_v - lv_avg ) / lv_std ELSE 0 ).
  cl_demo_output=>write( |值 { lv_v } 的 Z-score = { lv_z DECIMALS = 3 }| &&
                         COND string( WHEN abs( lv_z ) > 2 THEN '  ← 極端值' ELSE `` ) ).
ENDLOOP.

" 作業一:歐幾里德距離(找異常/找相似的勾稽用)
DATA(lv_dist) = sqrt( ( 2 - -2 ) ** 2 + ( -1 - 2 ) ** 2 ).      " = 5
cl_demo_output=>write( |歐幾里德距離 = { lv_dist }| ).
cl_demo_output=>display( ).

案例 4:勞保費級距計算(第 10-3 節 PU 期中專題 ZPU300_01)。表 ZPU300 用 SE11 建、SE54 產生維護畫面、SM30 維護資料。投保級距簡化成五級,費率 11%,雇主 70%/員工 20%/政府 10%。

REPORT zpu300_01.
" 勞保費級距:找出「第一個大於薪資」的級距,算出三方分攤
TYPES: BEGIN OF ty_grade,
         num       TYPE i,
         insurance TYPE p LENGTH 9 DECIMALS 2,
       END OF ty_grade.

CONSTANTS c_rate TYPE p LENGTH 5 DECIMALS 4 VALUE '0.1100'.   " 費率 11%

START-OF-SELECTION.
  " 級距表:SORTED 內表保證由小到大,找到第一個就是答案
  DATA lt_grade TYPE SORTED TABLE OF ty_grade WITH UNIQUE KEY insurance.
  lt_grade = VALUE #( ( num = 1 insurance = '19047' )
                      ( num = 2 insurance = '28800' )
                      ( num = 3 insurance = '30300' )
                      ( num = 4 insurance = '40100' )
                      ( num = 5 insurance = '50600' ) ).

  SELECT num, name, birthday, payroll
    FROM zpu300
    INTO TABLE @DATA(lt_emp).

  LOOP AT lt_emp ASSIGNING FIELD-SYMBOL(<ls_emp>).
    " 找第一個「級距 >= 薪資」的級距;超過最高級就用最高級
    DATA(lv_base) = VALUE #( lt_grade[ 1 ]-insurance OPTIONAL ).
    LOOP AT lt_grade INTO DATA(ls_grade).
      lv_base = ls_grade-insurance.
      IF <ls_emp>-payroll <= ls_grade-insurance.
        EXIT.                      " 找到就跳出內層迴圈
      ENDIF.
    ENDLOOP.

    " 保費四捨五入到整數(財務欄位一定要指定 mode)
    DATA(lv_fee) = round( val  = lv_base * c_rate
                          dec  = 0
                          mode = cl_abap_math=>round_half_up ).

    cl_demo_output=>write(
      |{ <ls_emp>-num } { <ls_emp>-name } 薪資 { <ls_emp>-payroll } | &&
      |投保 { lv_base } 保費 { lv_fee } | &&
      |雇主 { round( val = lv_fee * '0.7' dec = 0 mode = cl_abap_math=>round_half_up ) } | &&
      |員工 { round( val = lv_fee * '0.2' dec = 0 mode = cl_abap_math=>round_half_up ) } | &&
      |政府 { round( val = lv_fee * '0.1' dec = 0 mode = cl_abap_math=>round_half_up ) }| ).
  ENDLOOP.
  cl_demo_output=>display( ).
案例 5:採購單「交貨已完成」議題(第 10-4 節)。EKPO 的「交貨已完成標識(ELIKZ)」控制採購項目是否還算未清項:勾了之後 MRP 就不再把未收數量當作待供應。
方案一(配置):OMCQ 把訊息號 163 設為 E,再用 ME22N 手動勾「交貨已完成」並把「最近交貨日期」設為過去日期。
方案二(增強):加判斷條件,一旦勾了「已完成收貨」就不允許再收貨。

案例 6:結構的整批運算(第 09-2 節)。MULTIPLY-CORRESPONDINGADD-CORRESPONDINGSUBTRACT-CORRESPONDINGDIVIDE-CORRESPONDING 這四個指令會把兩個結構中「同名欄位」逐一運算。它們已被列為 obsolete,在 ABAP Objects 內不可使用。

REPORT zstruct_calc.
" 舊:MULTIPLY-CORRESPONDING gs_2 BY gs_1.(obsolete,Class 內不可用)
" 新:欄位少就直接寫,欄位多就用 RTTS 動態跑欄位清單
TYPES: BEGIN OF ty_rate,
         n1 TYPE decfloat34,
         n2 TYPE decfloat34,
       END OF ty_rate.

DATA(ls_rate) = VALUE ty_rate( n1 = '0.8' n2 = '0.9' ).
DATA(ls_qty)  = VALUE ty_rate( n1 = 10    n2 = 20 ).

ls_qty = VALUE #( n1 = ls_qty-n1 * ls_rate-n1
                  n2 = ls_qty-n2 * ls_rate-n2 ).

cl_demo_output=>write( |結果:{ ls_qty-n1 } / { ls_qty-n2 }| ).   " 8 / 18
cl_demo_output=>display( ).

16. 從「寫對」到「寫好」:18 條實務守則

講義第 10-5 節的清單,這是老師整章最想留下來的東西。

  1. 命名規則lv_(區域變數)、gv_(全域變數)、iv_(輸入參數)、ev_(輸出參數)、cv_(可改參數)、rv_(回傳值)、lt_/gt_(內表)、ls_/wa_(工作區)、lo_(物件)、lr_(參考)、<fs_>(field symbol)。
  2. 常數與程序名一眼看得懂CONSTANTS c_max_attempts TYPE i VALUE 3.;程序名用「模組+物件+動作」如 ZMM_PO_CREATE_AND_VALIDATE
  3. 模組化:一個方法只做一件事,超過一個畫面高度就該拆。
  4. 一定要寫註解:註解寫「為什麼」,不要寫「做什麼」。
  5. 內縮與對齊:ABAP Editor 的 Ctrl+Shift+F1(Pretty Printer)養成習慣。
  6. 迴圈中少用 SELECT SINGLE:改成一次撈進內表再對應(FOR ALL ENTRIES 或 JOIN)。
  7. 少用 SELECT *:只取需要的欄位,HANA 是欄式儲存,多取一欄就多一份成本。
  8. 注意索引:WHERE 條件的欄位若不在索引上,效能會斷崖式下降;用 ST05 / SAT 驗證。
  9. 內表操作注意排序READ TABLE ... BINARY SEARCH 之前一定要先 SORT 同樣的 key,否則會讀不到。
  10. HASHED 表加速查詢DATA lt_cust TYPE HASHED TABLE OF ... WITH UNIQUE KEY customer_id.(只能用完整 key 讀,但 O(1))。
  11. 批次取代逐筆APPEND LINES OF 取代逐行 APPENDINSERT ... FROM TABLE 取代逐筆 INSERT
  12. 用 LOOP AT ... GROUP BY 取代雙迴圈
  13. 重要片段做例外處理TRY ... CATCH cx_...
  14. SY-SUBRC 檢查:資料庫操作後必加。
  15. 訊息用 SE91 訊息類別MESSAGE e001(zmm_po) WITH lv_ebeln. 才能多語言,不要硬編中文字串。
  16. OO 可以不會寫,但要看得懂PUBLIC(對外開放)、PROTECTED(子類可用)、PRIVATE(只有自己用)。
  17. 禁止 SELECT ... ENDSELECT,尤其放在迴圈裡。
  18. 多表操作用內表與透明表交互:先 FOR ALL ENTRIES 撈價格表,再用 READ TABLE 對應,不要在迴圈裡查 DB。
REPORT zbest_practice.
" 守則 6 / 17 / 18 的正確做法 + 守則 13 的例外處理
IF lt_items IS INITIAL.
  RETURN.                       " FOR ALL ENTRIES 空表 = 撈全表,務必先擋
ENDIF.

SELECT item_id, price, waers
  FROM zitem_price
  INTO TABLE @DATA(lt_prices)
  FOR ALL ENTRIES IN @lt_items
 WHERE item_id = @lt_items-item_id.

" 用 SORTED / HASHED 表或 line_exists( ) 做對應,不再回資料庫
DATA lv_total TYPE p LENGTH 15 DECIMALS 2.

LOOP AT lt_items INTO DATA(ls_item).
  IF line_exists( lt_prices[ item_id = ls_item-item_id ] ).
    DATA(ls_price) = lt_prices[ item_id = ls_item-item_id ].
    lv_total = lv_total + ls_item-quantity * ls_price-price.
  ENDIF.
ENDLOOP.

" 例外處理:除以 0
TRY.
    DATA(lv_avg) = lv_total / lines( lt_items ).
    cl_demo_output=>write( |合計 { lv_total } 平均 { lv_avg }| ).
  CATCH cx_sy_zerodivide INTO DATA(lx_zero).
    MESSAGE lx_zero->get_text( ) TYPE 'E'.
  CATCH cx_sy_arithmetic_overflow INTO DATA(lx_ovf).
    MESSAGE lx_ovf->get_text( ) TYPE 'E'.
ENDTRY.
cl_demo_output=>display( ).
老師的結語:ABAP 世界裡同時存在 1992~2024 將近三個世代的語法。你自己寫新程式一定要用現代寫法,但一定要看得懂舊寫法 — 因為職場上要維護的,多半是別人十年前寫的。學會用 SE38 輸入 DEMO* 按 F4 查 SAP 內建的示範程式、用 ABAPDOCU 查語法、用 SE37 直接試跑函數,這三招比背語法有用。

練習與自我檢核

講義的 20 支練習程式(ZDATATYPE01~ZDATATYPE20)主題對照:01 型別初始值/02 SY-DATUM 與字面值/03 結構與繼承/04 顏色與 ICON/05 MOVE 與 FIELD-SYMBOLS/06 FIELD-SYMBOLS 結構版/07 COND 與 SWITCH/08 STRLEN 與型別轉換/09 CONV 位置/10 SHIFT REPLACE TRANSLATE/11 SEARCH/12 CONCATENATE 新用法/13 SPLIT CONDENSE/14 IF CASE/15 DO WHILE 雙迴圈/16 CALL FUNCTION 日期/17 LOOP AT 與 KNA1/18 STATICS/19 MULTIPLY-CORRESPONDING/20 WRITE 報表。

  1. 作業一(歐幾里德距離):設 x1,y1,x2,y2 四個變數,把距離存入 dist。提示:** '0.5'sqrt( );用途是找異常值/相似度勾稽。答案見第 15 節。
  2. 作業二(Z-score):設 x、u(平均)、stdv(標準差),求 z = ( x - u ) / stdv。想想為什麼型別要用 DECFLOAT34 而不是 I。
  3. 作業三(字串反轉):禁用 STRING_REVERSE,把 'I-LOVE-ABAP' 變成 'PABA-EVOL-I'。提示:strlen( ) + DO n TIMES + substring( );注意 offset 從 0 起算,所以 pos = 11 - sy-index
  4. 作業四(民國年):把 '20220315' 顯示成 '1110315'。提示:C→I 轉型、減 1911、再補零成 3 碼。踩雷點:直接用 N 型別相減會失去型別長度控制。
  5. 作業五(本月/上月/去年同期):上網找出標準函數並實作。答案:RP_CALC_DATE_IN_INTERVAL(signum '+'/'-')、HR_JP_MONTH_BEGIN_END_DATERP_LAST_DAY_OF_MONTHS
  6. 作業六(字串逐行輸出):輸入 80 碼、逗號分隔的字串,一列一列輸出。提示:SPLIT ... INTO TABLE + CONDENSE
  7. 作業七(排序輸出):同作業六,但存入內表並遞增排序。提示:SORT lt ASCENDING.;別忘了先 DELETE lt WHERE table_line IS INITIAL.
  8. 作業八(KNA1 前 5 筆):建內表 IT_KNA1,把 KNA1 依 KUNNR 排序取前 5 筆的 KUNNR、NAME1。現代解SELECT kunnr, name1 FROM kna1 INTO TABLE @DATA(lt_kna1) UP TO 5 ROWS ORDER BY kunnr. — 注意 UP TO n ROWS 若沒有 ORDER BY,在 HANA 上每次結果可能不同。
  9. 作業九(學會線上搜尋):練習用 SE37 直接執行函數、用 SE38 輸入 DEMO* 按 F4 找 SAP 示範程式、用 ABAPDOCU 查語法樹。
  10. 作業十(標準差複習):用 Excel 對 10,12,23,23,16,23,21,16 算 =AVERAGE( )=VAR.P( )=STDEV.P( )=SQRT(144/8),再用 ABAP 寫出同樣結果(見第 15 節)。記住:變異數就是標準差還沒開根號的值
  11. 自我檢核 ADATA(lv_x) = 5 / 2. 的結果是多少?為什麼?要得到 2.5 該怎麼改?
  12. 自我檢核 B-5 MOD 3 在 ABAP 是多少?跟 C / Java 有何不同?
  13. 自我檢核 C:日圓 1000 元在 BSEG-WRBTR 裡存的是什麼數字?要用哪個 BAPI 換算?表 TCURX 的作用是什麼?
  14. 自我檢核 DCHECK 寫在 LOOP 裡跟寫在 FORM 裡,行為有什麼不同?
  15. 自我檢核 Estrlen( )TYPE c LENGTH 20 但只放 13 個字的變數回傳多少?換成 TYPE string 呢?
  16. 自我檢核 FSY-TABIX 在 HASHED 內表的 LOOP 裡是多少?為什麼?
  17. 自我檢核 G:為什麼 SELECT SINGLE 沒帶完整主鍵時結果不穩定?跟本專案 ZKEPH「一下抓 H 一下抓 E」的成本不穩定,是不是同一類問題?

第 2-2 章 內表 / 工作區 / ABAP SQL 與 7.40+ 新語法

這是整本教材最核心的一章。ABAP 的世界只有兩件事:把資料從資料庫撈進記憶體(ABAP SQL),以及在記憶體裡把資料整理成報表要的樣子(內表)。這一章把內表的三種型別與 KEY 設計、增刪改查排序去重加總十大技巧、ABAP SQL 完整 SELECT 語法(JOIN/子查詢/聚合/CTE/視窗函數)、FOR ALL ENTRIES 的陷阱、S/4HANA 的 code pushdown 效能觀念,以及 7.40~7.55 的現代語法(VALUE / FOR / REDUCE / CORRESPONDING / COND / SWITCH / 表達式讀取)一次講完。學完之後,你寫出來的 SELECT 與 LOOP 應該可以直接上 S/4HANA 1809 以後的正式系統。

核心 TCODESE38SE11SE16NSE16HSE80ST05SATSE30SDBESQLMSWLTDBACOCKPITST12SCIABAPDOCUSE37VA03ME23NCO03FB03
關鍵物件MARA、MAKT、MARC、MARD、MBEW、MATDOC、ACDOCA、BKPF、BSEG、EKKO、EKPO、EKBE、EKES、EINA、LFA1、VBAK、VBAP、KNA1、T001、T458B、SPFLI、SFLIGHT、SBOOK、SCARR、SCUSTOM、USR21、STXH/STXL、CL_SQL_STATEMENT、CL_SQL_CONNECTION、CL_SALV_TABLE、RMMATH1_VARIANCE_DEVIATION、READ_TEXT/SAVE_TEXT

1. 內表是什麼:記憶體中的臨時表

內表(internal table)是程式執行期間在記憶體中建立的一張臨時表。你可以對它做插入、修改、刪除、查詢,程式跑完它就自動被釋放,不必像 T-SQL 的 #table 還要記得 DROP(雖然 T-SQL 現在也有 DECLARE @table 了)。

老師的第一句話:掌握內表是成為 ABAPer 的關鍵一步。除了寫程式之外,小到 SELECT-OPTIONS、大到 Function Module 的 TABLES 參數、ALV 的資料來源,到處都是 internal table 的影子。

口訣:內表要掌握十大技巧 —— 種類、建立、附加、合併、更改、刪除、查詢、去重、排序、加總。這十個動作學會了,ABAP 的資料處理就過關了。

內表、表格列(table line)與工作區(work area)

  • 內表:多筆資料的集合(相當於 SQL 的 table)。
  • table line:內表中的一筆記錄,很容易跟工作區搞混。
  • 工作區 work area:內表是「按行存取」的,必須有一塊區域當作與內表互相傳輸的介面。從內表讀資料時,被定址的那一行內容會覆蓋工作區;要寫資料進內表時,必須先把資料填進工作區。

換句話說:工作區=一列的結構,內表=很多列。在現代 ABAP 中,工作區往往不需要事先宣告,直接用 INTO DATA(ls_x)ASSIGNING FIELD-SYMBOL(<ls_x>) 在迴圈裡就地產生。

DESCRIBE TABLE itab LINES n. 就等於 SQL 的 SELECT COUNT(*) FROM table。7.02 之後更建議直接用內建函數 lines( itab ),可以寫在運算式裡。

2. 三種內表型別與 KEY 設計(STANDARD / SORTED / HASHED)

內表共三種類型,差別在「怎麼找到那一列」:

型別關鍵字索引主鍵存取複雜度適用時機
標準表STANDARD TABLE有主表索引(可用 INDEX / sy-tabix)Non-unique循序 O(n);有排序+BINARY SEARCH 才 O(log n)一般報表、資料量小(<1 萬筆)。簡單環境用它就夠了
排序表SORTED TABLE有邏輯索引,依主鍵昇冪自動排序後存放UNIQUE 或 NON-UNIQUEO(log n)(二分搜尋,系統自動做)邊新增邊排序、需要頻繁 READ 的中量資料(>1 萬筆)
雜湊表HASHED TABLE沒有主表索引,只能用鍵值存取必須 UNIQUEO(1),定址時間與筆數無關巨量資料(>10 萬筆)的鍵值查表、對照檔

SAP 官方對 hashed table 的定義:資料列依 hash 演算法存放、以唯一 hash key 存取;沒有主表索引,但可以另外掛 secondary table index(透過 secondary key)。

老師的實測結論(用 SE30/SAT 量):小資料量時排序表比較快;資料量很大時雜湊表勝出。不過有了 HANA 之後,大多數效能問題在 DB 層就解掉了,HASH 表用到的時機相對變少——但知道有這個工具,遇到極端狀況才有牌可打。

主鍵、次鍵與 USING KEY

  • Primary keyWITH [UNIQUE|NON-UNIQUE] KEY f1 f2。若用 TYPE TABLE OF struct 而不指定 KEY,預設是「所有非數值欄位(c/n/d/t/string)當 non-unique 標準鍵」——這通常不是你要的,也會拖慢比對。
  • WITH EMPTY KEY:7.40 起可宣告空鍵,明確表示「我不靠 key 找資料,只用 index」,比預設標準鍵乾淨。
  • Secondary key:一張內表可以同時掛多組次鍵(SORTED 或 HASHED),讓同一份資料有多種查法。存取時用 USING KEY <key_name> 指定要走哪一把鍵。
  • DEFAULT KEY:舊式寫法,等同標準鍵,效能差,新程式不建議。
REPORT zdemo_itab_types.

TYPES: BEGIN OF ty_result,
         vbeln  TYPE vbak-vbeln,
         posnr  TYPE vbap-posnr,
         kunnr  TYPE vbak-kunnr,
         matnr  TYPE vbap-matnr,
         kwmeng TYPE vbap-kwmeng,
         netwr  TYPE vbap-netwr,
         waerk  TYPE vbap-waerk,
       END OF ty_result.

" 1) 標準表:明確宣告空鍵,只靠 index 存取(最乾淨)
DATA lt_std TYPE STANDARD TABLE OF ty_result WITH EMPTY KEY.

" 2) 排序表:主鍵唯一,插入時自動維持排序
DATA lt_sorted TYPE SORTED TABLE OF ty_result
                    WITH UNIQUE KEY vbeln posnr.

" 3) 雜湊表:主鍵一定要 UNIQUE,沒有主表索引
DATA lt_hash TYPE HASHED TABLE OF ty_result
                  WITH UNIQUE KEY vbeln posnr.

" 4) 一張標準表掛兩把次鍵:一把查客戶、一把查料號
DATA lt_multi TYPE STANDARD TABLE OF ty_result
                   WITH EMPTY KEY
                   WITH NON-UNIQUE SORTED KEY by_kunnr COMPONENTS kunnr
                   WITH NON-UNIQUE SORTED KEY by_matnr COMPONENTS matnr.

" 用次鍵讀取/迴圈,速度會比全表掃描快非常多
READ TABLE lt_multi WITH KEY by_kunnr COMPONENTS kunnr = '0000100001'
                    INTO DATA(ls_one).

LOOP AT lt_multi USING KEY by_matnr INTO DATA(ls_line)
     WHERE matnr = 'DXTR1000'.
  " ... 處理
ENDLOOP.
HASHED TABLE 不能用 READ TABLE ... INDEX n、不能 SORT、不能 APPEND(只能 INSERT)。SORTED TABLE 也不能 APPEND(除非附加的值剛好維持排序,否則會 dump),也不能 SORT。APPEND 與 SORT 只適用於 STANDARD TABLE

3. 內表的宣告:從舊語法到現代寫法

講義列了很多 3.0 時代的宣告方式,這裡整理成「看得懂舊的、寫得出新的」對照。

舊寫法(講義原文,僅供讀懂舊程式)現代寫法(S/4HANA 請用這個)
DATA: BEGIN OF itab OCCURS 0, ... END OF itab.TYPES 定義結構 + DATA itab TYPE STANDARD TABLE OF ty WITH EMPTY KEY.
DATA itab LIKE spfli OCCURS 0 WITH HEADER LINE.DATA itab TYPE STANDARD TABLE OF spfli WITH EMPTY KEY.(不要 header line)
TYPES itabt TYPE line OCCURS 10.TYPES itabt TYPE STANDARD TABLE OF line WITH EMPTY KEY.
INCLUDE STRUCTURE spfli.TYPES: BEGIN OF ty. INCLUDE TYPE spfli. TYPES: name TYPE char20, END OF ty.(仍可用,但盡量改成明列欄位)
DATA wa LIKE LINE OF itab.仍然正確、常用;或直接 LOOP AT itab INTO DATA(wa) 就地宣告
DESCRIBE TABLE itab LINES n.n = lines( itab ).
DESCRIBE TABLE itab KIND k. / OCCURS n.用 RTTI:cl_abap_tabledescr=>describe_by_data( itab )

(講義第 12 個練習:內表的定義,程式 Z202202_12,示範 OCCURS 0 宣告與 BREAK-POINT 進入除錯)這裡把它整支改寫成現代版:

*&---------------------------------------------------------------------*
*& Report ZDEMO_02_12  內表的定義(原講義 Z202202_12 現代化改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_02_12.

" 定義列結構
TYPES: BEGIN OF ty_person,
         name TYPE char10,
         age  TYPE int2,
       END OF ty_person.

" 定義表型別(可重複使用,也可放進 SE11 的 Table Type)
TYPES ty_t_person TYPE STANDARD TABLE OF ty_person WITH EMPTY KEY.

DATA: lt_person1 TYPE ty_t_person,          " 由表型別宣告
      ls_person  TYPE ty_person,            " 工作區
      lt_person2 LIKE lt_person1.           " 參照已宣告好的內表

" 參照 DDIC 透明表 / 結構
DATA lt_sflight  TYPE STANDARD TABLE OF sflight WITH EMPTY KEY.
DATA lt_sorted   TYPE SORTED TABLE  OF sflight WITH UNIQUE KEY carrid connid fldate.
DATA lt_hashed   TYPE HASHED TABLE  OF sflight WITH UNIQUE KEY carrid connid fldate.

" 取筆數:內嵌宣告+內建函數,取代 DESCRIBE
SELECT carrid, connid, fldate, seatsmax, seatsocc
  FROM sflight
  INTO TABLE @DATA(lt_flight).

DATA(lv_cnt) = lines( lt_flight ).
WRITE: / |sflight 共 { lv_cnt } 筆|.

" BREAK-POINT 會與程式共存、每次執行都停;除外部程式請用 SE38 的 External Breakpoint
BREAK-POINT.
除錯快捷鍵複習:F5 單步、F6 單步但不進入副程式、F7 回到上層、F8 執行到下一個中斷點或結束。點變數可觀察內容;若工作區與內表同名(header line),要看內表內容記得加 []。BREAK-POINT 與 External Breakpoint 的共同點是:程式關閉就消失。

4. WITH HEADER LINE:為什麼不要再用

WITH HEADER LINE 是讓工作區與內表同名的舊機制。itab-field1 = 1. APPEND itab. 看起來很省事,但它是 ABAP 3.0 時代的產物。

講義引用的英文說明重點:header line 就是一個隱含的介面(interface),沒有它就必須自己宣告工作區;但在新版 SAP 上,header line 已經是 obsolete,而且物件導向 ABAP(class / method)根本不支援

指令對「無 header line」的內表對「有 header line」的內表
CLEAR it.清空內表只清空工作區(陷阱!)
CLEAR it[].清空內表清空內表
REFRESH it.清空內表清空內表,不會清工作區
FREE it.清空內表並釋放記憶體同左
*&---------------------------------------------------------------------*
*& Report ZDEMO_HEADERLINE  header line 的陷阱示範(僅供理解舊程式)
*&---------------------------------------------------------------------*
REPORT zdemo_headerline.

" ==== 反例:舊寫法,自己的程式請勿模仿 ====
" DATA it_marah TYPE TABLE OF mara WITH HEADER LINE.
" it_marah-matnr = 'A'.
" APPEND it_marah.            " 把工作區附加進內表
" CLEAR  it_marah.            " 只清工作區,內表還在!

" ==== 正解:工作區與內表分開,語意清楚 ====
DATA lt_mara TYPE STANDARD TABLE OF mara WITH EMPTY KEY.
DATA ls_mara TYPE mara.

ls_mara-matnr = 'A'.
APPEND ls_mara TO lt_mara.

CLEAR ls_mara.                " 清工作區
CLEAR lt_mara.                " 清內表(無 header line 時 CLEAR 就是清整張表)

" ==== 更好:連工作區都不用宣告 ====
DATA lt_mat TYPE STANDARD TABLE OF mara WITH EMPTY KEY.
APPEND VALUE #( matnr = 'A' mtart = 'FERT' ) TO lt_mat.

LOOP AT lt_mat INTO DATA(ls_line).
  WRITE: / ls_line-matnr, ls_line-mtart.
ENDLOOP.
遇到舊程式看不出某個名稱是內表還是工作區時,先 debug:在 Debugger 裡打 itab 看到的是工作區,打 itab[] 看到的才是內表。另外 OCCURS 0 的宣告形同 WITH HEADER LINE。

5. 內表的新增:APPEND / INSERT / COLLECT / VALUE

講義的口訣:內表新增有五個方法 —— INSERT、COLLECT(累加數值)、APPEND、MOVE、=(賦值),再加上 SELECT ... INTO TABLE。使用時機:

  • 只要存資料、要速度 → APPEND(附加到最後,只能用在 STANDARD TABLE)。
  • 要數字欄位自動加總、或要保證沒有重複條目 → COLLECT(等同 GROUP BY 的效果)。
  • 要插到既有列的前面、或依 key 插入 SORTED/HASHED → INSERT。
  • 整張表複製覆蓋lt_a = lt_b.(MOVE 已過時,直接用等號)。

COLLECT 的行為要記牢:比對所有非數值欄位,找到相同的那一列就把數值欄位累加上去;找不到就新增一列。非常適合做小計、合計、總計。

*&---------------------------------------------------------------------*
*& Report ZDEMO_02_13  內表的新增(原講義第 13 個練習 Z202202_13 改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_02_13.

TYPES: BEGIN OF ty_student,
         name TYPE char10,
         age  TYPE int2,
       END OF ty_student.

DATA: lt_stu TYPE STANDARD TABLE OF ty_student WITH EMPTY KEY,
      lt_tot TYPE STANDARD TABLE OF ty_student WITH EMPTY KEY.

" 1) APPEND:附加到表尾(最快)
DO 2 TIMES.
  APPEND VALUE #( name = 'CASPER' age = sy-index ) TO lt_stu.
ENDDO.

" 2) INSERT ... INDEX:插到指定位置之前
INSERT VALUE #( name = 'NCU' age = 3 ) INTO lt_stu INDEX 1.

" 3) INSERT INITIAL LINE:插一列空白列
"    常用在 SELECT-OPTIONS 要先塞一列空行進 range 表時
INSERT INITIAL LINE INTO lt_stu INDEX 1 ASSIGNING FIELD-SYMBOL(<ls_blank>).
<ls_blank>-name = 'BLANK'.

" 4) COLLECT:非數值欄位相同就累加數值欄位
DATA ls_stu TYPE ty_student.
ls_stu = VALUE #( name = 'NCU' age = 3 ).
COLLECT ls_stu INTO lt_stu.        " NCU 已存在 → age 變成 6

" 5) 多筆附加/插入
APPEND LINES OF lt_stu TO lt_tot.
APPEND LINES OF lt_stu FROM 2 TO 3 TO lt_tot.   " 注意:是第 2~3 列,不是 1~2
INSERT LINES OF lt_stu INTO lt_tot INDEX 1.

" 6) 整表覆蓋(MOVE 已過時,用等號)
DATA(lt_copy) = lt_stu.

" 7) 一次建整張表:VALUE 建構式(7.40+,最現代)
DATA(lt_new) = VALUE ty_t_dummy( ( name = 'A' age = 1 )
                                 ( name = 'B' age = 2 )
                                 ( name = 'C' age = 3 ) ).

LOOP AT lt_stu INTO DATA(ls_out).
  WRITE: / ls_out-name, ls_out-age.
ENDLOOP.
效能口訣:APPEND LINES OF it_a TO it_b 取代 LOOP AT it_a ... APPEND ... ENDLOOP。一行指令在核心層搬資料,比迴圈逐列搬快好幾個數量級。

把資料庫資料直接灌進內表,才是最常用的「新增」:

" 一次讀進內表(現代寫法:明列欄位 + 內嵌宣告 + 逸出字元 @)
SELECT bname AS name,
       persnumber AS telnum,
       addrnumber AS addr
  FROM usr21
  INTO TABLE @DATA(lt_emp)
  UP TO 10 ROWS.

" 追加到已存在的內表(不清空原內容)
SELECT bname AS name,
       persnumber AS telnum,
       addrnumber AS addr
  FROM usr21
  APPENDING TABLE @lt_emp
  WHERE bname LIKE 'Z%'.
欄位名稱不同名時,用 AS 重新命名,就不必再靠 INTO CORRESPONDING FIELDS OF 硬湊。CORRESPONDING 的比對是執行期做的,欄位一多就會慢,而且欄位名打錯不會報錯、只會靜靜地漏資料。

6. 內表的修改:MODIFY ... TRANSPORTING ... WHERE

語法骨架:

MODIFY itab FROM wa [INDEX idx] [TRANSPORTING f1 f2 ...] [WHERE cond].
  • 只給 INDEX:改指定那一列(在 LOOP 裡常搭配 sy-tabix)。
  • TRANSPORTING:只更新列出的欄位,其他欄位保持原值。
  • WHERE:整批更新(此時一定要有 TRANSPORTING)。
*&---------------------------------------------------------------------*
*& Report ZDEMO_02_14  內表的更改(原講義第 14 個練習 Z202202_14 改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_02_14.

TYPES: BEGIN OF ty_student,
         name TYPE char10,
         age  TYPE int2,
       END OF ty_student.

DATA lt_stu TYPE STANDARD TABLE OF ty_student WITH EMPTY KEY.

DO 2 TIMES.
  APPEND VALUE #( name = 'CASPER' age = sy-index ) TO lt_stu.
ENDDO.

DATA(ls_stu) = VALUE ty_student( name = 'NCU' age = 3 ).

" 1) 依 index 整列覆蓋
MODIFY lt_stu FROM ls_stu INDEX 2.

" 2) 依 index 只更新 NAME 欄位
ls_stu = VALUE #( name = 'NCUBA' age = 1 ).
MODIFY lt_stu FROM ls_stu INDEX 1 TRANSPORTING name.

" 3) 整批更新:所有 name = 'NCUBA' 的列,把 age 改成 99
ls_stu-age = 99.
MODIFY lt_stu FROM ls_stu TRANSPORTING age WHERE name = 'NCUBA'.

" 4) 最現代的做法:用 field-symbol 就地改,不需要 MODIFY 回寫
LOOP AT lt_stu ASSIGNING FIELD-SYMBOL(<ls_stu>) WHERE name = 'CASPER'.
  <ls_stu>-age = <ls_stu>-age + 1.
ENDLOOP.

LOOP AT lt_stu INTO DATA(ls_out).
  WRITE: / ls_out-name, ls_out-age.
ENDLOOP.
批次改內表,用 MODIFY ... TRANSPORTING ... WHERELOOP AT ... ASSIGNING,不要寫「LOOP → 改工作區 → MODIFY FROM wa INDEX sy-tabix」的三段式,那是最慢的寫法。
SORTED / HASHED 內表上,不可以修改主鍵欄位,執行期會拋出例外。要改鍵值只能 DELETE 再 INSERT。

7. 內表的刪除與去重:DELETE / ADJACENT DUPLICATES

DELETE TABLE itab WITH TABLE KEY k1 = v1 k2 = v2.  " 依主鍵刪除
DELETE TABLE itab FROM wa.                          " 依工作區的鍵值刪除
DELETE itab INDEX idx.                              " 依索引刪除
DELETE itab FROM idx1 TO idx2.                      " 依索引區間刪除
DELETE itab WHERE cond.                             " 依條件刪除(最常用)
DELETE ADJACENT DUPLICATES FROM itab COMPARING f1 f2.
DELETE ADJACENT DUPLICATES FROM itab COMPARING ALL FIELDS.
DELETE ADJACENT DUPLICATES 只刪「相鄰」的重複列。所以執行前一定要先 SORT,而且 SORT 的欄位必須涵蓋 COMPARING 的欄位;用 COMPARING ALL FIELDS 就要對所有欄位排序。沒排序就去重,是新手最常見的 bug —— 資料看起來少了一些、又沒少乾淨。
*&---------------------------------------------------------------------*
*& Report ZDEMO_02_15  內表的刪除與去重(原講義第 15 個練習 Z202202_15 改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_02_15.

TYPES: BEGIN OF ty_emp,
         name TYPE usr21-bname,
       END OF ty_emp.

DATA lt_emp TYPE STANDARD TABLE OF ty_emp WITH EMPTY KEY.

lt_emp = VALUE #( ( name = 'JERY' ) ( name = 'TOM' ) ( name = 'TOM' ) ).

DELETE lt_emp INDEX 1.                 " 刪第一筆

" 去重的正確順序:先排序,再去重
SORT lt_emp BY name DESCENDING.
DELETE ADJACENT DUPLICATES FROM lt_emp COMPARING name.

" 依條件刪除
DELETE lt_emp WHERE name IS INITIAL.

LOOP AT lt_emp INTO DATA(ls_emp).
  WRITE: / ls_emp-name.
ENDLOOP.

實務案例:在 LOOP 中刪除自己 —— USING KEY loop_key

講義收錄了一段工作上遇到的真實案例:把系統資料與 Excel 對照,Excel 裡沒有的就把系統那一列刪掉。在 LOOP 內刪除正在迴圈的那張表是很危險的動作(索引會位移),所以必須加 USING KEY loop_key,讓系統知道刪除要走「目前迴圈用的那把鍵」,避免跳列。

" 原講義寫法(FORM clear_data)的現代化版本
METHOD clear_data.

  CHECK it_ekes_xls IS NOT INITIAL.        " 對照表是空的就不要動作

  LOOP AT it_zmmb0021 ASSIGNING FIELD-SYMBOL(<ls_zmm>).

    " 用 line_exists 取代 READ TABLE + sy-subrc 判斷
    IF NOT line_exists( it_ekes_xls[ ebeln = <ls_zmm>-ebeln
                                     ebelp = <ls_zmm>-ebelp
                                     etens = <ls_zmm>-ekes_etens ] ).
      " 迴圈中刪除自己,一定要指定 loop_key,避免索引錯位
      DELETE it_zmmb0021 USING KEY loop_key.
    ENDIF.

  ENDLOOP.

ENDMETHOD.
更安全的替代寫法:不要在迴圈裡刪,改成「先蒐集要刪的 key → 迴圈結束後一次 DELETE」,或直接用 DELETE lt_a WHERE ...。若對照表可以做成 HASHED / SORTED KEY,判斷成本是 O(1)/O(log n),比 READ TABLE 全掃快很多。

8. 內表的讀取:READ TABLE、表達式讀取與 line_exists

READ TABLE itab INTO wa INDEX i.
READ TABLE itab INTO wa WITH KEY f1 = v1 f2 = v2.        " 自訂鍵,只能用 =
READ TABLE itab INTO wa WITH TABLE KEY k1 = v1.          " 走宣告的主鍵
READ TABLE itab INTO wa WITH KEY f1 = v1 BINARY SEARCH.  " 需先 SORT!
READ TABLE itab TRANSPORTING NO FIELDS WITH KEY ...      " 只想知道有沒有
  • WITH TABLE KEY:使用內表宣告時定義的鍵;WITH KEY:自訂欄位,但條件只能用等號。
  • BINARY SEARCH 可以大幅加速,但使用前必須依相同欄位順序排序,否則會讀不到(sy-subrc = 4)卻不報錯。
  • 讀完永遠要判斷 sy-subrc = 0。READ TABLE 就是內表版的 SELECT。

7.40 之後有更漂亮的寫法。講義 04-8 用左右對照的方式列出新舊語法,整理如下:

需求舊寫法7.40+ 新寫法
有條件讀出一列並取欄位READ TABLE it WITH KEY field1 = '台中' INTO wa. IF sy-subrc = 0. WRITE wa-fielda. ENDIF.IF line_exists( it[ field1 = '台中' ] ). WRITE it[ field1 = '台中' ]-fielda. ENDIF.
多條件讀出並指派 field-symbolREAD TABLE it WITH KEY field1 = '台幣' fielda = '台灣' ASSIGNING <fs>.ASSIGN it[ field1 = '台幣' fielda = '台灣' ] TO <fs>.
讀第 n 列READ TABLE it INDEX 99 INTO wa. IF sy-subrc = 0. ...TRY. WRITE it[ 99 ]-field1. CATCH cx_sy_itab_line_not_found. ENDTRY.
讀不到給預設值READ ... IF sy-subrc <> 0. wa = ... ENDIF.DATA(ls) = VALUE #( it[ key = x ] OPTIONAL ).
DATA(lv) = VALUE #( it[ key = x ]-amount DEFAULT 0 ).
*&---------------------------------------------------------------------*
*& Report ZDEMO_02_16  內表的讀取(原講義第 16 個練習 Z202202_16 改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_02_16.

TYPES: BEGIN OF ty_emp,
         name TYPE usr21-bname,
       END OF ty_emp.

DATA lt_emp TYPE STANDARD TABLE OF ty_emp WITH EMPTY KEY.

lt_emp = VALUE #( ( name = 'CASPER' ) ( name = 'RICKY' )
                  ( name = 'EDDIE'  ) ( name = 'JAMIE' ) ).

" 1) 讀第一筆
READ TABLE lt_emp INTO DATA(ls_emp) INDEX 1.
IF sy-subrc = 0.
  WRITE: / ls_emp-name.
ENDIF.

" 2) 讀符合條件的第一筆(表達式寫法,讀不到會拋例外)
TRY.
    WRITE: / lt_emp[ name = 'EDDIE' ]-name.
  CATCH cx_sy_itab_line_not_found.
    WRITE: / 'EDDIE MISSING'.
ENDTRY.

" 3) 只想判斷存不存在
IF line_exists( lt_emp[ name = 'HAZEL' ] ).
  WRITE: / 'HAZEL FOUND'.
ELSE.
  WRITE: / 'MISSING'.
ENDIF.

" 4) 讀不到就給預設值,完全不必 TRY
DATA(ls_opt) = VALUE ty_emp( lt_emp[ name = 'HAZEL' ] OPTIONAL ).

" 5) 取得列號(相當於 READ ... TRANSPORTING NO FIELDS 後看 sy-tabix)
DATA(lv_idx) = line_index( lt_emp[ name = 'JAMIE' ] ).
WRITE: / |JAMIE 在第 { lv_idx } 列|.

" 6) 大量資料就地存取,不複製資料 → 最快
ASSIGN lt_emp[ name = 'RICKY' ] TO FIELD-SYMBOL(<ls_emp>).
IF sy-subrc = 0.
  <ls_emp>-name = 'RICKY2'.
ENDIF.
表達式 it[ ... ] 讀不到會直接丟 CX_SY_ITAB_LINE_NOT_FOUND 讓程式 dump。所以:確定一定有 → 直接用;不確定 → 用 line_exists( ) 先問、或 VALUE #( ... OPTIONAL )、或包 TRY ... CATCH。

9. LOOP 的完整用法:WHERE、FROM/TO、GROUP BY、AT NEW / AT END OF / SUM

LOOP AT itab INTO wa [FROM n1] [TO n2] [WHERE cond] [USING KEY key].
LOOP AT itab ASSIGNING FIELD-SYMBOL(<fs>).      " 就地存取,可直接改
LOOP AT itab REFERENCE INTO DATA(lr).           " 取得參考,適合大量資料
LOOP AT itab INTO wa GROUP BY ( k = wa-key ).   " 7.40 內表分組

控制中斷(control break)四兄弟:AT FIRST / AT NEW f / AT END OF f / AT LAST,中間可用 SUM 做小計。

AT NEW / AT END OF 的三大陷阱,講義特別強調:
必須先依該欄位排序,否則群組會被切得亂七八糟。
比對的是「該欄位以及它左邊的所有欄位」,所以結構中的欄位順序會影響結果 —— 想用哪些欄位分組,就把它們排在結構最前面。
③ 在 AT ... ENDAT 區塊內,該欄位右邊的所有欄位會被填成 *(字元型)或 0(數值型),不要在區塊內引用它們。AT 區塊不能用 field-symbol 版的 LOOP(LOOP ... ASSIGNING 不支援 AT)。
*&---------------------------------------------------------------------*
*& Report ZDEMO_AT_EVENTS  AT FIRST / AT NEW / SUM / AT END OF / AT LAST
*&---------------------------------------------------------------------*
REPORT zdemo_at_events.

TYPES: BEGIN OF ty_t001,
         land1 TYPE t001-land1,      " 分組欄位放最左邊
         waers TYPE t001-waers,
         bukrs TYPE t001-bukrs,
         cnt   TYPE i,
       END OF ty_t001.

DATA lt_t001 TYPE STANDARD TABLE OF ty_t001 WITH EMPTY KEY.

SELECT land1, waers, bukrs
  FROM t001
  INTO CORRESPONDING FIELDS OF TABLE @lt_t001
  UP TO 9 ROWS.

" 每一列先給 1,方便示範 SUM
LOOP AT lt_t001 ASSIGNING FIELD-SYMBOL(<ls>).
  <ls>-cnt = 1.
ENDLOOP.

" 關鍵:AT 事件之前一定要排序
SORT lt_t001 BY land1 waers bukrs.

LOOP AT lt_t001 INTO DATA(ls_t001).

  AT FIRST.
    WRITE: / '===== 報表開始 ====='.
  ENDAT.

  AT NEW land1.
    WRITE: / |國家:{ ls_t001-land1 }|.
  ENDAT.

  WRITE: / ls_t001-waers, ls_t001-bukrs.

  AT END OF land1.
    SUM.                                   " 把數值欄位加總填回工作區
    WRITE: / |小計筆數:{ ls_t001-cnt }|.
    ULINE.
  ENDAT.

  AT LAST.
    SUM.
    WRITE: / |總計筆數:{ ls_t001-cnt }|.
  ENDAT.

ENDLOOP.

IF sy-subrc <> 0.
  WRITE: / '沒有資料'.
ENDIF.

7.40 的 LOOP AT ... GROUP BY:取代 AT NEW 的現代寫法

" 依採購單號分組,一組一組處理,且不必事先排序、不受欄位順序影響
LOOP AT lt_ekpo INTO DATA(ls_ekpo)
     GROUP BY ( ebeln = ls_ekpo-ebeln
                size  = GROUP SIZE
                idx   = GROUP INDEX )
     ASCENDING
     INTO DATA(ls_group).

  WRITE: / |採購單 { ls_group-ebeln },共 { ls_group-size } 項|.

  " 走訪該組的成員
  LOOP AT GROUP ls_group INTO DATA(ls_member).
    WRITE: / '   ', ls_member-ebelp, ls_member-matnr, ls_member-netwr.
  ENDLOOP.

  " 或直接用 REDUCE 把該組加總,一行搞定
  DATA(lv_sum) = REDUCE ekpo-netwr(
                   INIT s = 0
                   FOR ls IN GROUP ls_group
                   NEXT s = s + ls-netwr ).
  WRITE: / |   小計:{ lv_sum CURRENCY = 'TWD' }|.

ENDLOOP.
講義 07-2 有一段舊程式寫 LOOP AT it WHERE ... GROUP BY it-matnr 並在迴圈裡下 UPDATE。這種「迴圈中打 DB」的寫法在 S/4HANA 上是效能殺手,正確做法是:先在內表算完,再用 UPDATE dbtab FROM TABLE @lt_x 一次寫回。

10. 排序、去重、計數、清空與動態 SORT

SORT itab [BY f1 [ASCENDING|DESCENDING] f2 ...] [AS TEXT] [STABLE].
  • 不寫 BY 就依「標準鍵」排序,通常不是你要的,請一律明寫 BY
  • AS TEXT:依語言的字典順序排(處理中文/重音字元時用)。
  • STABLE:保持相同鍵值列的原有相對順序(做多層排序時很重要)。
  • SORT 只能用在 STANDARD TABLE;SORTED 本來就有序、HASHED 沒有順序。
老師在 ZKEPH 成本報表踩過的真實坑:沒有排序的內表,抓到哪一筆是「不保證」的。成本評價之後會有兩種角度(item category H 與 cost component E),兩者的成本欄位數量不同,程式若沒排序就 READ,有時抓到 H、有時抓到 E,成本結果就不穩定。結論:只要「只取一筆」,就必須先確定排序或明確過濾條件(例如固定取 H)。同樣的道理也適用於 SELECT 沒有 ORDER BY 的情況。
*&---------------------------------------------------------------------*
*& Report ZDEMO_SORT  動態排序欄位(原講義 ZSORT 改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_sort.

DATA lt_makt TYPE STANDARD TABLE OF makt WITH EMPTY KEY.

PARAMETERS p_sort TYPE char20 DEFAULT 'SORT:MAKTX'.

START-OF-SELECTION.

  " VALUE 建構式:MATNR 只寫一次,後面括號的列共用
  lt_makt = VALUE #(
      matnr = 'APPLE' ( spras = 'M' maktx = 'APPLE-M' )
                      ( spras = 'E' maktx = 'APPLE-E' )
      matnr = 'GOOGL' ( spras = 'M' maktx = 'GOOGL-M' )
                      ( spras = 'E' maktx = 'GOOGL-E' ) ).

  " 動態排序:括號裡放「欄位名稱的變數」
  " p_sort+5 表示取 p_sort 從第 6 個字元起的內容(offset 從 0 算)
  DATA(lv_field) = CONV fieldname( p_sort+5 ).
  SORT lt_makt BY (lv_field).

  LOOP AT lt_makt ASSIGNING FIELD-SYMBOL(<ls_makt>).
    WRITE: /1 <ls_makt>-matnr, <ls_makt>-spras, <ls_makt>-maktx.
  ENDLOOP.

清空與計數的四個指令要分清楚:

指令作用T-SQL 類比
CLEAR itab.清空內表(無 header line 時)TRUNCATE TABLE
REFRESH itab.清空內表,保留已配置的記憶體TRUNCATE TABLE
FREE itab.清空內表並釋放記憶體DROP TABLE
lines( itab ) / DESCRIBE TABLE itab LINES n.取筆數SELECT COUNT(*)

11. 工作區 vs Field-Symbol vs 參考:Call by value / by reference

LOOP 有三種取值方式,差別在「有沒有複製資料」:

寫法語意能否直接改內表效能
LOOP AT it INTO wa.call by value,把整列複製到工作區不行,要 MODIFY 回寫最慢(每列一次複製)
LOOP AT it ASSIGNING FIELD-SYMBOL(<fs>).call by reference,<fs> 就是那一列可以,改了就生效
LOOP AT it REFERENCE INTO DATA(lr).取得資料參考(物件導向友善)可以,用 lr->field快,適合傳給方法
*&---------------------------------------------------------------------*
*& Report ZDEMO_BYREF  call by value vs call by reference
*&---------------------------------------------------------------------*
REPORT zdemo_byref.

SELECT bukrs, butxt, ort01, land1, waers
  FROM t001
  INTO TABLE @DATA(lt_t001)
  UP TO 9 ROWS.

" (A) call by value:改工作區後必須 MODIFY 回內表
LOOP AT lt_t001 INTO DATA(ls_t001).
  ls_t001-bukrs = 'VAL'.
  MODIFY lt_t001 FROM ls_t001 INDEX sy-tabix.
ENDLOOP.

" (B) call by reference:直接改,不必 MODIFY,效能較佳
LOOP AT lt_t001 ASSIGNING FIELD-SYMBOL(<ls_t001>).
  <ls_t001>-bukrs = 'REF'.
ENDLOOP.

" (C) REFERENCE INTO:拿到資料參考,適合大結構或要傳給方法
LOOP AT lt_t001 REFERENCE INTO DATA(lr_t001).
  lr_t001->bukrs = 'PTR'.
ENDLOOP.

LOOP AT lt_t001 INTO DATA(ls_out).
  WRITE: / ls_out-bukrs, ls_out-butxt.
ENDLOOP.
用 ASSIGNING 時,不要在迴圈內對同一張表做 APPEND / INSERT / DELETE。內表重新配置記憶體後,field-symbol 會指向無效位址,輕則資料錯亂,重則 dump(CX_SY_ITAB_...)。

12. 雙迴圈(巢狀 LOOP)的寫法與優化

講義 04-4 用 ZLOOP 示範巢狀 LOOP:外圈 FROM 2 TO 9、內圈整表跑,並提醒 sy-tabix 在內外圈會互相覆蓋,所以外圈的列號一進迴圈就要先存起來。

*&---------------------------------------------------------------------*
*& Report ZDEMO_NESTED_LOOP  雙迴圈的注意事項與優化
*&---------------------------------------------------------------------*
REPORT zdemo_nested_loop.

SELECT bukrs, butxt, land1
  FROM t001
  INTO TABLE @DATA(lt_t001)
  UP TO 9 ROWS.

" ---- 反例:O(n × m),資料一多就爆 ----
LOOP AT lt_t001 ASSIGNING FIELD-SYMBOL(<ls_out>) FROM 2 TO 9.
  DATA(lv_tabix_out) = sy-tabix.            " sy-tabix 會被內圈覆蓋,先存起來

  LOOP AT lt_t001 ASSIGNING FIELD-SYMBOL(<ls_in>).
    DATA(lv_tabix_in) = sy-tabix.
    WRITE: / lv_tabix_out, lv_tabix_in.
  ENDLOOP.

ENDLOOP.

" ---- 正解 1:把內圈的表改成 SORTED / HASHED,用 key 直接命中 ----
DATA lt_ekpo TYPE SORTED TABLE OF ekpo WITH NON-UNIQUE KEY ebeln.

SELECT ebeln, ebelp, matnr, menge, netwr
  FROM ekpo
  WHERE ebeln IN @s_ebeln
  INTO CORRESPONDING FIELDS OF TABLE @lt_ekpo.

LOOP AT lt_ekko INTO DATA(ls_ekko).
  " 排序表上的 WHERE 會走二分搜尋,不是全表掃描
  LOOP AT lt_ekpo INTO DATA(ls_ekpo) WHERE ebeln = ls_ekko-ebeln.
    " ...
  ENDLOOP.
ENDLOOP.

" ---- 正解 2:能在 DB 做的就在 DB 做(JOIN),根本不要雙迴圈 ----
SELECT k~ebeln, k~lifnr, p~ebelp, p~matnr, p~netwr
  FROM ekko AS k
  INNER JOIN ekpo AS p ON p~ebeln = k~ebeln
  WHERE k~bukrs = @lv_bukrs
  INTO TABLE @DATA(lt_join).
三個效能口訣:① 儘量少用多重迴圈② 要 READ 就先 SORT 再 BINARY SEARCH(或直接改用 SORTED/HASHED 表)③ 批次修改用 MODIFY ... TRANSPORTING ... WHERE,不要迴圈逐列改

13. ABAP SQL 概論:Native SQL、Open SQL 與 ABAP SQL

種類特性風險 / 限制
Native SQL直接下該資料庫的 SQL,能做 Open SQL 做不到的事(預儲程序、游標、DB 專屬函數)綁死資料庫、不做 client 處理、不走表緩衝、繞過 SAP 授權與一致性檢查,有安全與資料一致性風險
Open SQLSAP 定義的資料庫無關標準,執行期由 DB Interface 轉成該平台的 Native SQL寫程式時不必考慮資料庫差異;功能是各家 DB 的交集
ABAP SQL(7.53 起的新名稱)Open SQL 改名而來。名稱改變反映了「某些語法只在 SAP HANA 上支援」,不再完全平台獨立必須用逸出字元 @、欄位間要逗號、嚴格語法檢查

主要指令:SELECT / INSERT / UPDATE / MODIFY / DELETE,執行結果一律看系統變數:SY-SUBRC = 0 表示成功,SY-DBCNT 是實際處理的筆數。

三個世代的寫法對照(講義 ZOPENSQL)

*&---------------------------------------------------------------------*
*& Report ZDEMO_OPENSQL_GEN  三個世代的 ABAP SQL 寫法
*&---------------------------------------------------------------------*
REPORT zdemo_opensql_gen.

SELECT-OPTIONS s_matnr FOR (mara-matnr) NO INTERVALS.  " 說明用;實務見第 3-1 章

" (1) 舊 Open SQL:欄位不加逗號、主機變數不加 @  → S/4HANA 已不建議
" SELECT matnr matkl INTO CORRESPONDING FIELDS OF TABLE gt_mara
"   UP TO 100 ROWS FROM mara WHERE matnr IN s_matnr.

" (2) 新 Open SQL(7.40 SP08+):欄位加逗號、主機變數加 @
SELECT matnr, matkl
  FROM mara
  WHERE matnr IN @s_matnr
  INTO TABLE @DATA(lt_mara)
  UP TO 100 ROWS.

" (3) 未來建議寫法:FROM 先寫、FIELDS 列欄位(語法順序最接近 HANA SQL)
SELECT FROM mara
  FIELDS matnr, matkl, mtart, meins
  WHERE matnr IN @s_matnr
  ORDER BY matnr
  INTO TABLE @DATA(lt_mara2)
  UP TO 100 ROWS.

LOOP AT lt_mara2 INTO DATA(ls_mara).
  WRITE: / ls_mara-matnr, ls_mara-matkl, ls_mara-mtart.
ENDLOOP.
S/4HANA 的嚴格語法檢查會逼你:欄位之間要有逗號、所有 ABAP 變數前要加 @、INTO 子句放最後。若程式沒加 @ 還能通過,是因為程式仍在舊版語法模式(Unicode / non-strict)下;新寫的程式一律加 @。

14. 完整的 SELECT 語法骨架

講義抄錄了 ABAP 官方文件的完整語法。這裡整理成可讀的骨架,寫 SQL 時當檢查清單用:

SELECT [SINGLE [FOR UPDATE]] [DISTINCT]
       FROM data_source [AS alias]
            [ [INNER] JOIN | LEFT|RIGHT [OUTER [MANY TO ONE]] JOIN | CROSS JOIN
              data_source [AS alias] ON join_cond ]
            [ USING CLIENT @lv_mandt | CLIENT SPECIFIED | USING ALL CLIENTS ]
       FIELDS col1, col2, sql_expression AS alias, ...
       [ FOR ALL ENTRIES IN @itab ] WHERE sql_cond
       [ GROUP BY col1, col2 | GROUPING SETS ( ... ) ]
       [ HAVING sql_cond ]
       [ UNION [ALL|DISTINCT] SELECT ... ]
       [ ORDER BY PRIMARY KEY | col1 [ASCENDING|DESCENDING], ... ]
       INTO   { @DATA(wa) | TABLE @DATA(itab) | ( @a, @b, @c )
              | CORRESPONDING FIELDS OF TABLE @itab
              | TABLE @itab PACKAGE SIZE n }
       [ APPENDING TABLE @itab ]
       [ UP TO n ROWS ] [ OFFSET o ]
       [ BYPASSING BUFFER ]
       [ CONNECTION con ].

常用子句速查:

子句意義備註
SELECT SINGLE只取一列必須給齊主鍵才保證唯一;不可與 GROUP BY、FOR ALL ENTRIES 併用
DISTINCT去除重複列不可與 SELECT SINGLE、也不建議與大量欄位併用
UP TO n ROWS / OFFSET o取前 n 列 / 跳過 o 列OFFSET 必須搭配 ORDER BY 才有意義(7.50+)
PACKAGE SIZE n分批讀進內表搭配 SELECT ... ENDSELECT,處理巨量資料避免記憶體爆掉
BYPASSING BUFFER略過表緩衝,直接讀 DB需要絕對最新資料時用
FOR UPDATE讀取時加 DB 鎖SAP 建議改用 ENQUEUE 鎖物件,見資料字典章
CLIENT SPECIFIED自行控制 MANDT7.55 起建議改用 USING CLIENT / USING ALL CLIENTS

15. JOIN 與子查詢

多表查詢有兩條路:在 SE11 建 View(可重複使用),或在程式裡 JOIN(一次性)。老師的思考題:「你的程式到底要用 SQL 關聯多表?還是建成一個 View 方便日後重複使用?」判斷標準:

  • 邏輯會被多支程式重複使用、或要給 Fiori/OData 消費 → 建 CDS View(S/4HANA 首選,取代自建 DB View)。
  • 只有這支程式用、關聯條件會隨選擇畫面變動 → 程式裡 JOIN。

JOIN 三要素:連接哪些表、連接條件(ON)、連接哪些欄位。型態:

  • INNER JOIN:兩表都有對應的資料才出現。
  • LEFT OUTER JOIN:保證左邊的主資料表全部秀出,右表沒對到就是 NULL(在 ABAP 會轉成型別初始值)。
  • RIGHT OUTER JOIN / CROSS JOIN:較少用;CROSS JOIN 是笛卡兒積,非常危險。
*&---------------------------------------------------------------------*
*& Report ZDEMO_JOIN  INNER JOIN / LEFT OUTER JOIN(講義 08-06、08-07 改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_join.

SELECT-OPTIONS s_gstrp FOR (afko-gstrp).

" INNER JOIN:製令主檔 AFKO + 製令項目 AFPO
SELECT a~aufnr,        " 製令編號
       a~gstrp,        " 開始日期
       a~gltrp,        " 完工日期
       a~ftrms,        " 發放日期
       a~gamng,        " 工單數量
       a~gmein,        " 單位
       a~plnbez,       " 成品件號
       b~plnum,        " 計畫工單
       b~pwerk,        " 計畫工廠
       b~dauat,        " 製令類別
       b~berid         " MRP Area
  FROM afko AS a
  INNER JOIN afpo AS b ON b~aufnr = a~aufnr
  WHERE a~gstrp IN @s_gstrp
  ORDER BY a~aufnr
  INTO TABLE @DATA(lt_inner).

" LEFT OUTER JOIN:以 AFPO 為主表,保證項目全出,AFKO 沒對到就填初始值
SELECT b~aufnr, b~plnum, b~pwerk, b~dauat, b~berid,
       a~gstrp, a~gltrp, a~gamng, a~plnbez
  FROM afpo AS b
  LEFT OUTER JOIN afko AS a ON a~aufnr = b~aufnr
  WHERE b~pwerk = @lv_werks
  INTO TABLE @DATA(lt_left).

LOOP AT lt_inner INTO DATA(ls).
  WRITE: / ls-aufnr, ls-plnbez, ls-gamng, ls-gmein, ls-pwerk, ls-berid.
ENDLOOP.
在 LEFT OUTER JOIN 的 WHERE 裡對右表欄位下條件,會把 outer join 「退化成」inner join(因為 NULL 不符合條件)。要對右表限制條件,請寫在 ON 子句裡,不要寫在 WHERE。

子查詢(subquery):INEXISTS、以及純量子查詢。講義 08-13、08-14 的範例現代化如下:

*&---------------------------------------------------------------------*
*& Report ZDEMO_SUBQUERY  IN / EXISTS / 純量子查詢
*&---------------------------------------------------------------------*
REPORT zdemo_subquery.

" (1) IN 子查詢:料號存在於 MARC 中的才取
SELECT matnr, werks, lgort, labst
  FROM mard
  WHERE matnr IN ( SELECT matnr FROM marc WHERE beskz = 'F' )
  ORDER BY matnr, werks, lgort
  INTO TABLE @DATA(lt_in)
  UP TO 100 ROWS.

" (2) EXISTS 相關子查詢:外層每一列去問「內層有沒有對應」
"     通常比 IN 快,因為找到一筆就停
SELECT a~matnr, a~werks, a~lgort, a~labst
  FROM mard AS a
  WHERE EXISTS ( SELECT mandt FROM marc AS b
                  WHERE b~matnr = a~matnr
                    AND b~werks = a~werks
                    AND b~dispo <> @space )
  ORDER BY a~matnr
  INTO TABLE @DATA(lt_exists)
  UP TO 100 ROWS.

" (3) 純量子查詢:把單一值當成欄位用
SELECT matnr, werks,
       ( SELECT SUM( labst ) FROM mard AS d
          WHERE d~matnr = c~matnr AND d~werks = c~werks ) AS total_stock
  FROM marc AS c
  WHERE werks = @lv_werks
  INTO TABLE @DATA(lt_scalar)
  UP TO 100 ROWS.

16. 聚合、GROUP BY、HAVING、ORDER BY 與 SQL 運算式

ABAP SQL 標準聚合函數:COUNT( * ) / COUNT( DISTINCT f ) / SUM( ) / AVG( ) / MAX( ) / MIN( )

講義提到還有 MEDIAN、PRODUCT、STDDEV、VAR、CORR、CORR_SPEARMAN、GROUPING( )、STRING_AGG、ALLOW_PRECISION_LOSS(依講義註記整理)其中:

  • STRING_AGG( col, ',' ):把多列串成一個字串,非常好用(作業 5 就是考它)。在 ABAP SQL 中可用於 HANA;跨平台程式請確認版本支援。
  • ALLOW_PRECISION_LOSS:允許十進位運算做精度捨棄以換取效能。
  • MEDIAN / STDDEV / VAR / CORR 等統計函數屬 HANA 專屬,在純 ABAP SQL 中通常要靠 CDS View(含 table function)AMDP 才能用;ABAP 端則可呼叫 RMMATH1_* 系列函數(例如 RMMATH1_VARIANCE_DEVIATION 回傳變異數、標準差、平均值)。
*&---------------------------------------------------------------------*
*& Report ZDEMO_AGG  GROUP BY / HAVING / ORDER BY 綜合(講義 03-05 改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_agg.

" (1) 本期採購單,依建檔人員統計張數,張數多的排前面
SELECT ernam,
       COUNT( * ) AS cnt
  FROM ekko
  WHERE aedat >= @lv_from_date
  GROUP BY ernam
  ORDER BY cnt DESCENDING
  INTO TABLE @DATA(lt_cnt).

" (2) 再加 HAVING:只留張數大於 7 的人
SELECT ernam,
       COUNT( * )      AS cnt,
       MIN( aedat )    AS first_date,
       MAX( aedat )    AS last_date
  FROM ekko
  WHERE aedat >= @lv_from_date
  GROUP BY ernam
  HAVING COUNT( * ) > 7
  ORDER BY cnt DESCENDING
  INTO TABLE @DATA(lt_having).

LOOP AT lt_having INTO DATA(ls).
  WRITE: / |用戶:{ ls-ernam } 採購單數:{ ls-cnt } 期間:{ ls-first_date DATE = USER } ~ { ls-last_date DATE = USER }|.
ENDLOOP.

" (3) ACDOCA 依科目 + 成本中心加總(講義 Z202202_66 改寫)
SELECT racct,                      " 會計科目
       rcntr,                      " 成本中心
       SUM( tsl ) AS tsl           " 交易幣別金額
  FROM acdoca
  WHERE rldnr = '0L'               " 分類帳,一定要帶
    AND rbukrs = @lv_bukrs         " 公司代碼,一定要帶
    AND gjahr  = @lv_gjahr         " 年度,一定要帶
  GROUP BY racct, rcntr
  ORDER BY racct, rcntr
  INTO TABLE @DATA(lt_acdoca).

WRITE: / '會計科目', 20 '成本中心', 40 '科目餘額'.
LOOP AT lt_acdoca INTO DATA(ls_acdoca).
  WRITE: / ls_acdoca-racct, 20 ls_acdoca-rcntr, 40 ls_acdoca-tsl.
ENDLOOP.
聚合的三個坑: GROUP BY 之後,SELECT 清單裡只能出現「分組欄位」或「聚合函數」。 SUM( ) 的結果型別可能溢位——目標欄位請用足夠大的型別(例如 TYPE p LENGTH 15 DECIMALS 2DEC23_2),不要直接沿用 CURR 欄位。 COUNT / SUM 對 NULL 的處理與 T-SQL 相同:COUNT( * ) 算所有列,COUNT( col ) 不算 NULL。

SQL 運算式:CASE、字串函數、數值函數、CAST

SELECT p~ebeln,
       p~ebelp,
       p~matnr,

       " CASE 運算式(簡單型與搜尋型)
       CASE p~werks
         WHEN '1000' THEN '台北廠'
         WHEN '2000' THEN '台中廠'
         ELSE '其他'
       END AS plant_text,

       CASE WHEN p~netwr >= 1000000 THEN 'A'
            WHEN p~netwr >=  100000 THEN 'B'
            ELSE 'C'
       END AS abc_class,

       " 數值函數
       DIV( p~menge, 12 )              AS per_month,
       DIVISION( p~netwr, p~menge, 2 ) AS unit_price,
       ABS( p~netwr )                  AS abs_amt,
       ROUND( p~netwr, 0 )             AS round_amt,
       CEIL( p~menge )                 AS ceil_qty,
       FLOOR( p~menge )                AS floor_qty,
       MOD( p~ebelp, 10 )              AS mod_item,

       " 字串函數
       CONCAT( p~ebeln, p~ebelp )      AS po_key,
       CONCAT_WITH_SPACE( p~ebeln, p~matnr, 1 ) AS po_mat,
       SUBSTRING( p~matnr, 1, 4 )      AS mat_prefix,
       LENGTH( p~matnr )               AS mat_len,
       LTRIM( p~matnr, '0' )           AS mat_notrail,
       REPLACE( p~matnr, 'X', 'Y' )    AS mat_repl,
       UPPER( p~txz01 )                AS txt_upper,
       LOWER( p~txz01 )                AS txt_lower,

       " 型別轉換與空值處理
       CAST( p~menge AS DEC( 15, 3 ) ) AS menge_dec,
       COALESCE( k~lifnr, '__NONE__' ) AS lifnr_or_none,

       " 貨幣/單位換算(S/4 常用)
       CURRENCY_CONVERSION( amount             = p~netwr,
                            source_currency    = k~waers,
                            target_currency    = 'TWD',
                            exchange_rate_date = k~bedat ) AS netwr_twd

  FROM ekpo AS p
  LEFT OUTER JOIN ekko AS k ON k~ebeln = p~ebeln
  WHERE p~ebeln IN @s_ebeln
  INTO TABLE @DATA(lt_expr).

UNION 與 CTE(WITH)

" UNION:把兩段結果疊起來(欄位數量與型別要相容)
SELECT FROM ekko FIELDS ebeln, lifnr, 'PO' AS doc_type
  WHERE bukrs = @lv_bukrs
UNION DISTINCT
SELECT FROM ekko FIELDS ebeln, lifnr, 'SA' AS doc_type
  WHERE bstyp = 'L'
  INTO TABLE @DATA(lt_union).

" CTE(Common Table Expression,7.51+):把中間結果命名,讓 SQL 好讀
WITH
  +po_head AS (
    SELECT FROM ekko
      FIELDS ebeln, lifnr, bedat, waers
      WHERE bukrs = @lv_bukrs
        AND bedat BETWEEN @lv_from AND @lv_to ),

  +po_sum AS (
    SELECT FROM ekpo
      FIELDS ebeln, SUM( netwr ) AS total_netwr, COUNT( * ) AS item_cnt
      GROUP BY ebeln )

  SELECT FROM +po_head AS h
    INNER JOIN +po_sum AS s ON s~ebeln = h~ebeln
    LEFT OUTER JOIN lfa1 AS v ON v~lifnr = h~lifnr
    FIELDS h~ebeln, h~lifnr, v~name1, h~bedat,
           s~item_cnt, s~total_netwr
    ORDER BY s~total_netwr DESCENDING
    INTO TABLE @DATA(lt_cte)
    UP TO 50 ROWS.
CTE 只能用在 單一 SELECT 中(WITH ... SELECT ... . 是一整句),不能像 T-SQL 那樣定義後給多句用。CTE 名稱一律以 + 開頭。

視窗函數 OVER( )(7.51+)

" 每個廠商的採購金額,以及該廠商累計、排名 —— 一次 SELECT 搞定
SELECT p~ebeln,
       p~ebelp,
       k~lifnr,
       p~netwr,
       SUM( p~netwr ) OVER( PARTITION BY k~lifnr )            AS lifnr_total,
       AVG( p~netwr ) OVER( PARTITION BY k~lifnr )            AS lifnr_avg,
       MAX( p~netwr ) OVER( PARTITION BY k~lifnr )            AS lifnr_max,
       COUNT( * )     OVER( PARTITION BY k~lifnr )            AS lifnr_cnt,
       RANK( )        OVER( PARTITION BY k~lifnr
                            ORDER BY p~netwr DESCENDING )     AS rank_in_lifnr,
       DENSE_RANK( )  OVER( PARTITION BY k~lifnr
                            ORDER BY p~netwr DESCENDING )     AS drank,
       ROW_NUMBER( )  OVER( PARTITION BY k~lifnr
                            ORDER BY p~netwr DESCENDING )     AS rn
  FROM ekpo AS p
  INNER JOIN ekko AS k ON k~ebeln = p~ebeln
  WHERE k~bukrs = @lv_bukrs
    AND k~bedat BETWEEN @lv_from AND @lv_to
  INTO TABLE @DATA(lt_window).

" 取「每個廠商金額最高的那一筆」:把上面結果再過濾 rn = 1
DELETE lt_window WHERE rn <> 1.
視窗函數是 code pushdown 的利器:以前要「撈回來 → 排序 → LOOP 累加 → AT NEW 判斷」的邏輯,現在一句 SQL 在 HANA 裡就算完了,資料量大時差距是數量級的。

17. FOR ALL ENTRIES:四大陷阱與替代方案

FOR ALL ENTRIES(FAE)的用途:先建一張驅動內表,再用它限制第二段 SELECT。經典例子是先從 EKKO 撈出採購單號,再用它去 EKPO 撈明細。

*&---------------------------------------------------------------------*
*& Report ZDEMO_FAE  FOR ALL ENTRIES 正確用法與陷阱
*&---------------------------------------------------------------------*
REPORT zdemo_fae.

" 驅動表:只取需要的鍵欄位
SELECT ebeln, bukrs, lifnr, bedat
  FROM ekko
  WHERE bukrs = @lv_bukrs
  INTO TABLE @DATA(lt_ekko)
  UP TO 10 ROWS.

" ★ 陷阱一:驅動內表若為空,FAE 會回傳「整張表」,而且 WHERE 的其他條件也被忽略
"    所以一定要先判斷 IS NOT INITIAL
IF lt_ekko IS NOT INITIAL.

  " ★ 陷阱二:驅動表要先去重,否則同一個 key 會被送好幾次,白白浪費 DB 往返
  DATA(lt_key) = lt_ekko.
  SORT lt_key BY ebeln.
  DELETE ADJACENT DUPLICATES FROM lt_key COMPARING ebeln.

  SELECT ebeln, ebelp, matnr, werks, menge, netwr
    FROM ekpo
    FOR ALL ENTRIES IN @lt_key
    WHERE ebeln = @lt_key-ebeln
      AND loekz = @space
    INTO TABLE @DATA(lt_ekpo).

ENDIF.
FOR ALL ENTRIES 四大陷阱
空表災難:驅動內表是空的時候,FAE 條件整段被忽略,連同 WHERE 中其他條件也一併失效,結果是「把整張表撈回來」。講義用 EKPO 實測過三種情況(無 FAE/有 FAE/FAE 表為空),第三種筆數會暴增。解法:一律先寫 IF lt_x IS NOT INITIAL.
自動 DISTINCT:FAE 的結果會自動去除完全重複的列。如果 SELECT 清單沒有包含目標表的完整主鍵,會「莫名其妙少資料」。解法:SELECT 清單一定要含齊主鍵欄位。
被拆成多次 DB 呼叫:系統會依 profile 參數 rsdb/max_blocking_factor 把驅動表切成很多段,逐段丟給 DB。驅動表沒去重、或筆數上萬,往返次數就爆掉。
型別必須相符:驅動內表欄位的型別與長度要和 DB 欄位一致,否則轉型時可能靜默地對不到。另外 FAE 不可與 SELECT SINGLE、也不建議與 GROUP BY 併用,而且 FAE 的驅動表不能是 header line。

S/4HANA 的替代方案(優先順序由上而下):

  1. JOIN:兩張表本來就有關聯,直接 JOIN,一次往返。
  2. 子查詢 IN / EXISTS:邏輯是「存在性判斷」時最乾淨。
  3. CTE(WITH):中間結果多、邏輯分層時可讀性最好。
  4. SELECT ... FROM @itab(7.52+):把已在記憶體的內表當成資料來源,直接下 SQL。
  5. CDS View / AMDP:邏輯複雜、會被重複使用時的正解。
*&---------------------------------------------------------------------*
*& Report ZDEMO_ITAB_AS_SOURCE  用內表當 SELECT 資料來源(講義第 65 個練習改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_itab_as_source.

" 第一段:撈出公司代碼 1710 的採購單
SELECT ebeln, bukrs, lifnr, ernam, bedat, waers
  FROM ekko
  WHERE bukrs = '1710'
  INTO TABLE @DATA(lt_ekko).

" 第二段:直接把內表當 FROM 的來源再篩一次(7.52+)
"          可完全取代「再跑一次 SELECT」或「LOOP + DELETE」
SELECT ebeln, bukrs, lifnr, ernam
  FROM @lt_ekko AS k2
  WHERE ernam = 'S4H_MM'
  ORDER BY ebeln
  INTO TABLE @DATA(lt_result).

LOOP AT lt_result INTO DATA(ls).
  WRITE: / |{ ls-ebeln } { ls-bukrs } { ls-lifnr } { ls-ernam }|.
ENDLOOP.

18. SELECT SINGLE vs UP TO 1 ROWS(HANA 沒有排序保證)

講義 08-00 特別開一節講這個差異,重點是主鍵

SELECT SINGLESELECT ... UP TO 1 ROWS
條件要求必須指定全部主鍵才保證取到唯一那筆;缺任何一個主鍵就不該用任意條件、甚至不給條件都可以
可否 ORDER BY不可以可以,因此可以「取最新/最大的那一筆」
回傳一個工作區可 INTO 工作區或內表
沒給齊主鍵時回傳「任意一筆」,結果不保證穩定加 ORDER BY 就穩定
HANA 沒有排序保證:資料在欄式儲存中沒有固定的實體順序,同一段沒有 ORDER BY 的 SQL,今天跟明天、單機與多節點,回傳順序可能不同。所以「取最新一筆」絕不能靠 SELECT SINGLE 或不加 ORDER BY 的 UP TO 1 ROWS。
這正是老師 ZKEPH 成本抓錯的同一類問題:沒有排序,就沒有「第一筆」可言
*&---------------------------------------------------------------------*
*& Report ZDEMO_SINGLE_VS_TOP1
*&---------------------------------------------------------------------*
REPORT zdemo_single_vs_top1.

PARAMETERS: p_matnr TYPE mara-matnr OBLIGATORY,
            p_werks TYPE marc-werks OBLIGATORY.

" (1) 主鍵給齊 → SELECT SINGLE 是正確且最快的
SELECT SINGLE matnr, werks, dispo, beskz, ekgrp
  FROM marc
  WHERE matnr = @p_matnr
    AND werks = @p_werks
  INTO @DATA(ls_marc).

IF sy-subrc = 0.
  WRITE: / ls_marc-matnr, ls_marc-werks, ls_marc-dispo.
ENDIF.

" (2) 主鍵沒給齊、又要「最新的一筆」→ 必須 UP TO 1 ROWS + ORDER BY
SELECT ebeln, ebelp, matnr, werks, menge, netwr
  FROM ekpo
  WHERE matnr = @p_matnr
    AND werks = @p_werks
  ORDER BY ebeln DESCENDING, ebelp DESCENDING   " 排序寫清楚,結果才穩定
  INTO @DATA(ls_last)
  UP TO 1 ROWS.
ENDSELECT.

" (3) 更好的做法:用 MAX( ) 讓 DB 直接算,不必排序整張表
SELECT MAX( ebeln ) AS max_ebeln
  FROM ekpo
  WHERE matnr = @p_matnr
    AND werks = @p_werks
  INTO @DATA(lv_max_ebeln).

" (4) 反例:SELECT ... ENDSELECT 單筆迴圈(效能差,除非搭配 PACKAGE SIZE)
"     除示範外,實務請改成 INTO TABLE 一次取回
" SELECT * FROM marc INTO CORRESPONDING FIELDS OF @ls_marc
"   WHERE matnr = @p_matnr.
"   WRITE ls_marc-matnr.
" ENDSELECT.
關於「區間 vs 列舉」的實務提醒:用 SELECT-OPTIONS 的區間(BT / BETWEEN)與用列舉(一串 EQ),在字元型欄位(例如 HKONT、MATNR、BELNR)上結果可能不同——因為區間比較走的是字典序,會受前置零、ALPHA 轉換、空白補齊影響。老師的結論:不確定時用列舉(逐一列出)比較安全。要用區間,就先把值做好 ALPHA conversion(CONVERSION_EXIT_ALPHA_INPUT)再放進 range。

19. 資料異動:UPDATE / INSERT / MODIFY / DELETE 與 COMMIT WORK

" ---- UPDATE ----
UPDATE dbtab SET f1 = @v1, f2 = @v2 WHERE cond.
UPDATE dbtab FROM @wa.                    " 依主鍵整列更新
UPDATE dbtab FROM TABLE @itab.            " 批次更新

" ---- INSERT ----
INSERT dbtab FROM @wa.
INSERT dbtab FROM TABLE @itab [ACCEPTING DUPLICATE KEYS].
INSERT INTO dbtab VALUES @wa.             " 舊式,仍可用

" ---- MODIFY(有就改、沒有就新增,等同 T-SQL 的 MERGE / UPSERT)----
MODIFY dbtab FROM @wa.
MODIFY dbtab FROM TABLE @itab.

" ---- DELETE ----
DELETE FROM dbtab WHERE cond.
DELETE dbtab FROM @wa.                    " 依主鍵刪一列
DELETE dbtab FROM TABLE @itab.            " 依內表的鍵值批次刪
*&---------------------------------------------------------------------*
*& Report ZDEMO_DML  資料異動與 LUW(講義 08-15 ~ 08-19 整併改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_dml.

DATA lt_scarr TYPE STANDARD TABLE OF scarr WITH EMPTY KEY.

lt_scarr = VALUE #( ( carrid   = '000'
                      carrname = 'CASPER'
                      currcode = 'TWD'
                      url      = 'www.cerps.com.tw' ) ).

" 1) 批次新增;重複鍵不要 dump 就加 ACCEPTING DUPLICATE KEYS
INSERT scarr FROM TABLE @lt_scarr ACCEPTING DUPLICATE KEYS.
DATA(lv_ins) = sy-dbcnt.

" 2) 局部更新(一定要寫 WHERE,否則整張表被改掉)
UPDATE scarr SET url = 'www.cerps.com.tw'
             WHERE carrid = '000'.

" 3) MODIFY = INSERT + UPDATE(T-SQL 的 MERGE)。效率較低,資料量大時分開寫
MODIFY scarr FROM TABLE @lt_scarr.

" 4) 刪除
DELETE FROM scarr WHERE carrid = '000'.

" 5) 一律確認結果再決定 COMMIT 或 ROLLBACK
IF sy-subrc = 0.
  COMMIT WORK AND WAIT.        " AND WAIT:等更新任務完成才往下(報表要立刻讀回時必用)
  MESSAGE '異動成功' TYPE 'S'.
ELSE.
  ROLLBACK WORK.
  MESSAGE '異動失敗,已回復' TYPE 'E'.
ENDIF.
正式系統的鐵律
不要直接對 SAP 標準表下 INSERT / UPDATE / DELETE。標準表背後有一致性檢查、狀態欄位、變更文件、後續帳務,必須走 BAPI / BDC / 標準交易。自建的 Z 表才可以直接寫。
UPDATE 一定要寫 WHERE,講義原文就標「這是危險的做法」。
③ 呼叫 BAPI 之後要自己下 BAPI_TRANSACTION_COMMIT;在 update task 或 BAdI 裡不可以亂下 COMMIT WORK,會把別人的 LUW 一起提交掉。
COMMIT WORK AND WAITCOMMIT WORK 的差別:前者等非同步更新完成,後者不等。報表寫完馬上要讀回來時,用 AND WAIT。

20. 動態 SQL:動態表名、動態欄位、動態 WHERE

ABAP SQL 支援把「表名/欄位清單/WHERE 條件」放進括號,用變數或內表帶入。講義 08-02、08-03、08-05、08-08、08-09、02-10 都在講這件事。

*&---------------------------------------------------------------------*
*& Report ZDEMO_DYNSQL  動態表名 / 動態欄位 / 動態條件
*&---------------------------------------------------------------------*
REPORT zdemo_dynsql.

PARAMETERS: p_tname TYPE tabname DEFAULT 'MARC'.

FIELD-SYMBOLS: <lt_any> TYPE STANDARD TABLE,
               <ls_any> TYPE any.

" (1) 動態表名:資料物件也要動態建立
CREATE DATA DATA(lr_tab) TYPE STANDARD TABLE OF (p_tname).
ASSIGN lr_tab->* TO <lt_any>.

SELECT * FROM (p_tname)
  INTO TABLE @<lt_any>
  UP TO 100 ROWS.

" (2) 動態欄位清單:單一字串
DATA(lv_cols) = `MATNR, WERKS`.
SELECT DISTINCT (lv_cols)
  FROM marc
  INTO CORRESPONDING FIELDS OF TABLE @DATA(lt_marc1)
  UP TO 100 ROWS.

" (3) 動態欄位清單:用內表帶(欄位多時比較好維護)
DATA lt_cols TYPE TABLE OF string.
APPEND `MATNR, WERKS` TO lt_cols.
APPEND `, EISBE, PSTAT` TO lt_cols.

SELECT DISTINCT (lt_cols)
  FROM marc
  INTO CORRESPONDING FIELDS OF TABLE @DATA(lt_marc2)
  UP TO 100 ROWS.

" (4) 動態 WHERE:用字串範本組條件,避免 CONCATENATE
DATA(lv_matnr) = CONV mara-matnr( 'DXTR1000' ).
DATA(lv_werks) = CONV marc-werks( 'MI00' ).

DATA lt_where TYPE TABLE OF string.
APPEND |MATNR = '{ lv_matnr }'| TO lt_where.
APPEND |AND WERKS = '{ lv_werks }'| TO lt_where.

SELECT SINGLE lgort
  FROM mard
  WHERE (lt_where)
  INTO @DATA(lv_lgort).

" (5) 動態 JOIN:整段 FROM 用字串組出來
DATA(lv_from) =
  `spfli AS p ` &&
  `INNER JOIN sflight AS f ON p~carrid = f~carrid AND p~connid = f~connid ` &&
  `INNER JOIN sbook   AS b ON b~carrid = f~carrid AND b~connid = f~connid ` &&
  `                       AND b~fldate = f~fldate`.

SELECT p~carrid, p~connid, f~fldate, b~bookid
  FROM (lv_from)
  WHERE p~cityfrom = 'FRANKFURT'
    AND p~cityto   = 'NEW YORK'
    AND f~seatsmax > f~seatsocc
  ORDER BY p~carrid, p~connid, f~fldate, b~bookid
  INTO TABLE @DATA(lt_dyn).
動態 SQL 有 SQL Injection 風險。凡是把使用者輸入串進 WHERE 或欄位清單,都必須先驗證(白名單、cl_abap_dyn_prg=>check_table_name_str( )quote( ) 逸出)。SCI/ATC 的安全檢查會抓這一項。

21. Native SQL、ADBC、AMDP 與 CDS 的定位

當 ABAP SQL 做不到(要用 HANA 專屬函數、要呼叫預儲程序、要跑游標)時,才往下走。

EXEC SQL(已過時,只為讀懂舊程式)

講義 02-09 用 EXEC SQL ... ENDEXEC 示範查詢、建立 Oracle 函數與游標。三個語法規則要記得:語句結尾不加句點、區塊內不可有註解(星號與雙引號都不行)、參數用冒號 :var 而不是問號;欄位與條件都用逗號分開。

EXEC SQL 在 S/4HANA 已被宣告為 obsolete,新程式一律改用 ADBC(ABAP Database Connectivity,CL_SQL_STATEMENT / CL_SQL_CONNECTION / CL_SQL_RESULT_SET),因為 ADBC 支援參數綁定、例外處理與連線管理。Native SQL 不做 client(MANDT)處理、不走表緩衝、不檢查 SAP 授權,用之前務必三思。
*&---------------------------------------------------------------------*
*& Report ZDEMO_ADBC  ADBC 取代 EXEC SQL(講義 08-20 CL_SQL_STATEMENT 現代化)
*&---------------------------------------------------------------------*
REPORT zdemo_adbc.

PARAMETERS p_carr TYPE spfli-carrid DEFAULT 'LH'.

TYPES: BEGIN OF ty_row,
         connid   TYPE spfli-connid,
         cityfrom TYPE spfli-cityfrom,
         cityto   TYPE spfli-cityto,
       END OF ty_row.

DATA lt_row TYPE STANDARD TABLE OF ty_row WITH EMPTY KEY.

TRY.
    " 取得標準連線(要連別的 DB 就傳 DBCON 的連線名稱)
    DATA(lo_con) = cl_sql_connection=>get_connection( ).
    DATA(lo_stmt) = lo_con->create_statement( ).

    " 用參數綁定,不要把變數串進 SQL 字串(防 injection)
    DATA(lo_params) = NEW cl_sql_statement( ).
    DATA(lv_sql) = |SELECT connid, cityfrom, cityto FROM spfli WHERE carrid = ?|.

    DATA(lo_result) = lo_stmt->execute_query(
                        statement = lv_sql
                        parameters = VALUE #( ( REF #( p_carr ) ) ) ).

    " 把結果集直接倒進內表
    DATA(lr_tab) = REF #( lt_row ).
    lo_result->set_param_table( lr_tab ).
    DATA(lv_cnt) = lo_result->next_package( ).
    lo_result->close( ).
    lo_con->close( ).

  CATCH cx_sql_exception INTO DATA(lx_sql).
    MESSAGE lx_sql->get_text( ) TYPE 'E'.
ENDTRY.

LOOP AT lt_row INTO DATA(ls_row).
  WRITE: / ls_row-connid, ls_row-cityfrom, ls_row-cityto.
ENDLOOP.

" 若只要執行 DDL / DML(相當於講義 ZOPENSQL20 的 EXECUTE_UPDATE)
" DATA(lv_rows) = lo_stmt->execute_update( |UPDATE ztab SET flag = 'X'| ).

AMDP 與 CDS:真正的 code pushdown

  • CDS View(ABAP Core Data Services):在 ADT/Eclipse 用 DDL 定義的視圖,取代 SE11 的 DB View。支援 association、annotation、參數、聚合、CASE、視窗函數,可直接被 ABAP SQL、OData、Fiori、Analytics 消費。S/4HANA 的第一選擇
  • AMDP(ABAP Managed Database Procedure):把 SQLScript 寫在 ABAP class 的 method 裡,由 ABAP Server 管理生命週期,適合「CDS 表達不出來的程序式邏輯」——迴圈、暫存表、複雜統計函數(MEDIAN、STDDEV、CORR)。
  • 選擇順序:ABAP SQL → CDS View → AMDP → Native SQL/ADBC。越往後越強、也越難維護與升級。
" AMDP 骨架(放在 ADT / Eclipse 建立的 class 中)
CLASS zcl_amdp_demo DEFINITION PUBLIC.
  PUBLIC SECTION.
    INTERFACES if_amdp_marker_hdb.        " 標記為 AMDP class
    CLASS-METHODS get_stddev
      IMPORTING VALUE(iv_bukrs) TYPE bukrs
                VALUE(iv_gjahr) TYPE gjahr
                VALUE(iv_racct) TYPE racct
      EXPORTING VALUE(et_out)   TYPE ztt_acct_stat.
ENDCLASS.

CLASS zcl_amdp_demo IMPLEMENTATION.
  METHOD get_stddev BY DATABASE PROCEDURE FOR HDB
                    LANGUAGE SQLSCRIPT
                    OPTIONS READ-ONLY
                    USING acdoca.
    " SQLScript:可用 ABAP SQL 沒有的 STDDEV / MEDIAN / VAR
    et_out = SELECT racct,
                    COUNT(*)      AS cnt,
                    AVG( hsl )    AS avg_amt,
                    STDDEV( hsl ) AS std_amt,
                    MEDIAN( hsl ) AS med_amt
               FROM acdoca
              WHERE rbukrs = :iv_bukrs
                AND gjahr  = :iv_gjahr
                AND racct  = :iv_racct
                AND rldnr  = '0L'
              GROUP BY racct;
  ENDMETHOD.
ENDCLASS.

22. 效能觀念:Code Pushdown 與 S/4HANA 的巨表

講義 02-11 整理的 Open SQL 注意事項,是 ABAP 效能的基本功。這裡逐條保留並補上 S/4HANA 的現況:

講義守則S/4HANA 的補充
避免在迴圈中寫 SELECT,改用 FOR ALL ENTRIES 一次抽進內表更好的做法是 JOIN/子查詢/CDS View;FAE 只是次佳解
JOIN 不要關聯太多表,順序小表→大表HANA 的 optimizer 會自行決定順序;但表數量多仍會讓執行計畫變複雜,建議拆成 CTE
只查自己需要的欄位,少用 SELECT *在欄式資料庫上這條更重要:HANA 只讀被指定的欄位,少一欄就少一份 I/O
WHERE 多用索引、主鍵HANA 主要靠欄式掃描,但 驅動欄位(如 MANDT、BUKRS、GJAHR、WERKS)仍要帶;SE11 可建 secondary index,S/4 也常用 CDS 的 index-like 設計
不要用 SELECT ... ENDSELECT正確;要處理巨量資料請用 PACKAGE SIZE 分批,或 OFFSET / UP TO 分頁
不要用 <>(不走索引),改用 > 或 <HANA 上影響已較小,但仍建議把條件寫成正向(IN / BETWEEN)比較好被最佳化
不要用 ORDER BY,撈回來再在內表排這一條在 HANA 上要反過來:HANA 排序很快,且沒有 ORDER BY 就沒有順序保證。需要順序就在 SQL 排
內表求和:能在 SQL 做就在 SQL 做;否則用 AT END OF;COLLECT 在大量資料時較慢正確。優先用 GROUP BY 讓 DB 加總,其次視窗函數,最後才回到內表
APPEND LINES OF 取代 LOOP+APPEND正確,永遠成立
Code Pushdown 一句話:能讓資料庫算的,就不要搬回 ABAP 算。搬 100 萬列回應用伺服器再 LOOP 加總,和一句 GROUP BY 的差距,是分鐘與秒的差距。

S/4HANA 的兩張巨表:ACDOCA 與 MATDOC

  • ACDOCA(Universal Journal):S/4 把 BKPF/BSEG、FAGLFLEXA、COEP、ANEP、MLIT 等總帳/管理會計/獲利分析明細通通合併到這一張表,欄位數超過 300、資料量以億計。查詢時 RLDNR(分類帳)、RBUKRS(公司代碼)、GJAHR(年度)幾乎是必帶條件,再加期間 POPER 或傳票號 BELNR。BSEG/BKPF 在 S/4 仍存在,但報表請改讀 ACDOCA 或其 CDS View(I_JournalEntryItem 等)。
  • MATDOC:S/4 把 MKPF+MSEG 合併成 MATDOC,所有物料文件內容都存在這張表。MKPF/MSEG 變成相容性 CDS View(讀得到、但效能不好)。庫存數量表 MARD-LABST、MBEW 等在 S/4 也已改成由 MATDOC 聚合而來的相容視圖,不要對它們做大量迴圈式查詢,改用 NSDM 系列 CDS View 或標準 FM。
  • 物料主檔仍是 MARA(一般)/MAKT(描述)/MARC(工廠)/MARD(儲位);MARA-MATNR 在 S/4 長度為 40(原 18),寫程式不要再寫死 18 碼。

效能檢查工具

TCODE用途
ST05SQL Trace:看程式實際下了哪些 SQL、跑幾次、花多久(最常用)
SATABAP Runtime Analysis,取代舊的 SE30;看哪一行 ABAP 最花時間
SDBEDisplay Explain:把一段 SQL 的執行計畫叫出來看,判斷有沒有走索引(講義此處僅有標題,依 SAP 標準補述)
SQLM / SWLTSQL Monitor 與 SQL Performance Tuning Worklist:在正式機收集實際 SQL 負載,配合 ATC 找出待優化程式(S/4 遷移必用)
ST12 / STAD單一交易的整合追蹤/單筆交易統計
DBACOCKPIT資料庫層的整體監控、表大小、索引
SE11 > Indexes建立/檢視次要索引
SCI / ATC靜態程式碼檢查,含效能與安全規則

23. 7.40 ~ 7.55 新語法總覽

講義 06-2 逐條列出了新語法。這裡整理成一張速查表與一支可以直接跑的示範程式。

語法作用取代了什麼
DATA( )內嵌宣告,SQL 與應用層都支援先 TYPES、再 DATA、再 SELECT 的三段式
@ 逸出字元標示主機變數新版 ABAP SQL 強制
VALUE #( )建構結構/內表的值逐欄指派 + APPEND
FOR ... IN ... 在建構式裡跑迴圈LOOP ... APPEND ... ENDLOOP
REDUCE把內表歸納成單一值(加總、計數、找最大)LOOP 累加
CORRESPONDING #( )依名稱搬欄位,可 MAPPING/EXCEPT,7.50 起支援動態映射MOVE-CORRESPONDING
COND #( )條件運算式IF ... ELSE ... ENDIF 只為了給一個值
SWITCH #( )多值分支運算式CASE ... ENDCASE 只為了給一個值
NEW #( )建立匿名資料物件或類別實例CREATE DATA / CREATE OBJECT
REF #( )取得資料參考GET REFERENCE OF
CONV #( )明確型別轉換宣告中間變數再指派
CAST向下轉型(物件參考)?=(仍可用)
EXACT #( )轉換時不允許失真,失真就丟例外手動檢查
FILTER #( )依條件過濾出新內表LOOP ... WHERE ... APPEND
LET ... IN在建構式中定義區域變數額外宣告輔助變數
BASE在 VALUE/CORRESPONDING 中保留原內容再疊加先複製再修改
line_exists( ) / line_index( )判斷列是否存在 / 取得列號READ TABLE + sy-subrc / sy-tabix
itab[ ... ]表達式讀取(table expression)READ TABLE ... INTO
| ... { } ... |字串範本,可帶格式選項CONCATENATE / WRITE TO
&&字串串接運算子CONCATENATE
ALPHA = IN / OUT在字串範本中做前置零轉換CONVERSION_EXIT_ALPHA_INPUT/OUTPUT
LOOP AT ... GROUP BY內表分組SORT + AT NEW / AT END OF
MOVE-CORRESPONDING ... EXPANDING/KEEPING NESTED TABLES深層結構(含內表欄位)的搬移逐層手工搬
SELECT ... FROM @itab把內表當 SQL 資料來源(7.52+)再 SELECT 一次 DB
WITH(CTE)/UNION/OVER( )CTE、聯集、視窗函數(7.51+)多段 SELECT + 內表加工
日期時間 SQL 函數TIMS_IS_VALID、TSTMP_IS_VALID、TSTMP_CURRENT_UTCTIMESTAMP、TSTMP_SECONDS_BETWEEN、TSTMP_ADD_SECONDS、TSTMP_TO_DATS/TO_TIMS/TO_DST、ABAP_USER_TIMEZONEABAP 端逐一計算(用了會啟動 7.53 嚴格語法檢查)
*&---------------------------------------------------------------------*
*& Report ZDEMO_740_SYNTAX  7.40 ~ 7.55 新語法總覽(可直接執行)
*&---------------------------------------------------------------------*
REPORT zdemo_740_syntax.

TYPES: BEGIN OF ty_item,
         ebeln TYPE ekpo-ebeln,
         ebelp TYPE ekpo-ebelp,
         matnr TYPE ekpo-matnr,
         menge TYPE ekpo-menge,
         netwr TYPE ekpo-netwr,
       END OF ty_item,
       ty_t_item TYPE STANDARD TABLE OF ty_item WITH EMPTY KEY.

" ---- 1) VALUE:一次建好整張內表 ----
DATA(lt_item) = VALUE ty_t_item(
  ( ebeln = '4500000001' ebelp = '00010' matnr = 'M001' menge = 10 netwr = 1000 )
  ( ebeln = '4500000001' ebelp = '00020' matnr = 'M002' menge =  5 netwr =  800 )
  ( ebeln = '4500000002' ebelp = '00010' matnr = 'M001' menge = 20 netwr = 2100 ) ).

" ---- 2) FOR:用迴圈產生內表(含 LET 定義區域變數)----
DATA(lt_double) = VALUE ty_t_item(
  FOR ls IN lt_item
  LET lv_rate = CONV f( 1.05 ) IN
  ( ebeln = ls-ebeln
    ebelp = ls-ebelp
    matnr = ls-matnr
    menge = ls-menge
    netwr = ls-netwr * lv_rate ) ).

" ---- 3) FOR ... WHERE:只挑符合條件的 ----
DATA(lt_m001) = VALUE ty_t_item(
  FOR ls2 IN lt_item WHERE ( matnr = 'M001' ) ( ls2 ) ).

" ---- 4) FILTER:更簡潔的過濾(來源表需有對應 key)----
DATA lt_src TYPE SORTED TABLE OF ty_item WITH NON-UNIQUE KEY matnr.
lt_src = lt_item.
DATA(lt_filtered) = FILTER #( lt_src WHERE matnr = 'M001' ).

" ---- 5) REDUCE:把內表歸納成單一值 ----
DATA(lv_total) = REDUCE ekpo-netwr(
                   INIT s = CONV ekpo-netwr( 0 )
                   FOR ls3 IN lt_item
                   NEXT s = s + ls3-netwr ).

DATA(lv_cnt_m001) = REDUCE i(
                      INIT c = 0
                      FOR ls4 IN lt_item WHERE ( matnr = 'M001' )
                      NEXT c = c + 1 ).

" ---- 6) BASE:保留原內容再疊加 ----
DATA(lt_more) = VALUE ty_t_item(
  BASE lt_item
  ( ebeln = '4500000003' ebelp = '00010' matnr = 'M003' menge = 1 netwr = 99 ) ).

" ---- 7) CORRESPONDING:欄位對應搬移,可改名、可排除 ----
TYPES: BEGIN OF ty_out,
         po_no  TYPE ekpo-ebeln,
         item   TYPE ekpo-ebelp,
         matnr  TYPE ekpo-matnr,
         amount TYPE ekpo-netwr,
       END OF ty_out.
DATA lt_out TYPE STANDARD TABLE OF ty_out WITH EMPTY KEY.

lt_out = CORRESPONDING #( lt_item
                          MAPPING po_no  = ebeln
                                  item   = ebelp
                                  amount = netwr ).

" 深層結構(含內表欄位)搬移
" MOVE-CORRESPONDING ls_deep_src TO ls_deep_dst EXPANDING NESTED TABLES.
" MOVE-CORRESPONDING ls_deep_src TO ls_deep_dst KEEPING TARGET LINES.

" ---- 8) COND / SWITCH:條件運算式 ----
DATA(lv_period) = COND char10( WHEN sy-timlo < '120000' THEN 'AM'
                               WHEN sy-timlo < '180000' THEN 'PM'
                               ELSE 'NIGHT' ).

DATA(lv_grade) = SWITCH char1( lv_total
                               WHEN 0 THEN 'Z'
                               ELSE COND #( WHEN lv_total > 3000 THEN 'A' ELSE 'B' ) ).

" ---- 9) NEW / REF / CONV / CAST ----
DATA(lr_item) = NEW ty_item( ebeln = '4500000009' menge = 1 ).
DATA(lr_ref)  = REF #( lt_item ).
DATA(lv_f)    = CONV f( '3.14159' ).
DATA(lo_salv) = CAST cl_salv_table( lo_any_object ).

" ---- 10) 字串範本:取代 CONCATENATE,並帶格式選項 ----
DATA(lv_msg) = |採購單 { lt_item[ 1 ]-ebeln ALPHA = OUT } 共 { lines( lt_item ) } 項,| &&
               |總金額 { lv_total CURRENCY = 'TWD' },| &&
               |日期 { sy-datum DATE = USER },時間 { sy-uzeit TIME = USER }|.

" 補零 / 去零:ALPHA = IN 補零、ALPHA = OUT 去零
DATA(lv_matnr_in)  = |{ 'M001' ALPHA = IN }|.
DATA(lv_matnr_out) = |{ '000000000000M001' ALPHA = OUT }|.

" ---- 11) 表達式讀取 ----
IF line_exists( lt_item[ matnr = 'M002' ] ).
  WRITE: / lt_item[ matnr = 'M002' ]-netwr.
ENDIF.

WRITE: / lv_msg,
       / |合計 { lv_total },M001 共 { lv_cnt_m001 } 筆,時段 { lv_period }|.
講義提到「例 1/例 2 舊語法 vs 新語法」處原文只有截圖。(依講義結構整理)老師的總結值得抄下來:以前要先建工作區、建內表、再 SELECT;有了 DATA( ) 之後,對照欄位寫 SELECT 就好,不必再構建各式各樣的內表與表型別,這非常符合敏捷開發的思路。

24. 綜合練習一:歐幾里德距離(Euclidean distance)與 Z-score

這是講義的期中題 Z00_MIDTERM:從自建表 ZEUCLIDEAN(欄位 ID、HEIGHT、HOBBY、AGE)讀出所有人,先把三個欄位標準化成 Z-score,再算每個人與「使用者輸入的那個 ID」之間的歐幾里德距離,最後標出最適伙伴(自己標 O、最近的人標 X)。

公式:z = ( x − 平均 ) / 標準差距離 = √( Σ ( z_i − z_target,i )² )。標準差可以呼叫標準函數 RMMATH1_VARIANCE_DEVIATION(回傳 E_VARIANCE、E_DEVIATION、E_AVERAGE),或自己算。

*&---------------------------------------------------------------------*
*& Report ZDEMO_EUCLIDEAN  歐幾里德距離+Z-score(原 Z00_MIDTERM 現代化改寫)
*&---------------------------------------------------------------------*
REPORT zdemo_euclidean.

TYPES: BEGIN OF ty_zeuc,
         id            TYPE zeuclidean-id,
         height        TYPE zeuclidean-height,
         hobby         TYPE zeuclidean-hobby,
         age           TYPE zeuclidean-age,
         z_height      TYPE p LENGTH 8 DECIMALS 4,
         z_hobby       TYPE p LENGTH 8 DECIMALS 4,
         z_age         TYPE p LENGTH 8 DECIMALS 4,
         distance      TYPE p LENGTH 8 DECIMALS 4,
         best_fit      TYPE char1,
       END OF ty_zeuc.

PARAMETERS p_id TYPE zeuclidean-id OBLIGATORY.

" 1) 一次讀進內表
SELECT id, height, hobby, age
  FROM zeuclidean
  ORDER BY id                                  " 一定要排序,結果才穩定
  INTO CORRESPONDING FIELDS OF TABLE @DATA(lt_zeuc).

IF lt_zeuc IS INITIAL.
  MESSAGE '沒有資料' TYPE 'E'.
ENDIF.

DATA(lv_n) = lines( lt_zeuc ).

" 2) 用 REDUCE 算平均,不必寫迴圈
DATA(lv_avg_h) = REDUCE f( INIT s = 0 FOR ls IN lt_zeuc NEXT s = s + ls-height ) / lv_n.
DATA(lv_avg_o) = REDUCE f( INIT s = 0 FOR ls IN lt_zeuc NEXT s = s + ls-hobby  ) / lv_n.
DATA(lv_avg_a) = REDUCE f( INIT s = 0 FOR ls IN lt_zeuc NEXT s = s + ls-age    ) / lv_n.

" 3) 母體標準差 = sqrt( Σ(x-平均)² / n )
DATA(lv_std_h) = sqrt( REDUCE f( INIT s = 0 FOR ls IN lt_zeuc
                                 NEXT s = s + ( ls-height - lv_avg_h ) ** 2 ) / lv_n ).
DATA(lv_std_o) = sqrt( REDUCE f( INIT s = 0 FOR ls IN lt_zeuc
                                 NEXT s = s + ( ls-hobby  - lv_avg_o ) ** 2 ) / lv_n ).
DATA(lv_std_a) = sqrt( REDUCE f( INIT s = 0 FOR ls IN lt_zeuc
                                 NEXT s = s + ( ls-age    - lv_avg_a ) ** 2 ) / lv_n ).

" 4) 算 Z-score:用 field-symbol 就地寫回,不必 MODIFY
LOOP AT lt_zeuc ASSIGNING FIELD-SYMBOL(<ls>).
  <ls>-z_height = COND #( WHEN lv_std_h > 0 THEN ( <ls>-height - lv_avg_h ) / lv_std_h ELSE 0 ).
  <ls>-z_hobby  = COND #( WHEN lv_std_o > 0 THEN ( <ls>-hobby  - lv_avg_o ) / lv_std_o ELSE 0 ).
  <ls>-z_age    = COND #( WHEN lv_std_a > 0 THEN ( <ls>-age    - lv_avg_a ) / lv_std_a ELSE 0 ).
ENDLOOP.

" 5) 定位使用者輸入的那一筆
IF NOT line_exists( lt_zeuc[ id = p_id ] ).
  MESSAGE |查無 ID { p_id }| TYPE 'E'.
ENDIF.
DATA(ls_target) = lt_zeuc[ id = p_id ].

" 6) 算每一筆與 target 的歐幾里德距離
LOOP AT lt_zeuc ASSIGNING FIELD-SYMBOL(<ls2>).
  <ls2>-distance = sqrt( ( <ls2>-z_height - ls_target-z_height ) ** 2
                       + ( <ls2>-z_hobby  - ls_target-z_hobby  ) ** 2
                       + ( <ls2>-z_age    - ls_target-z_age    ) ** 2 ).
ENDLOOP.

" 7) 找最小距離(排除自己:distance = 0)
DATA(lv_min) = REDUCE f( INIT m = CONV f( '9999' )
                         FOR ls IN lt_zeuc WHERE ( distance > 0 )
                         NEXT m = COND #( WHEN ls-distance < m THEN ls-distance ELSE m ) ).

" 8) 標記:自己 O、最適伙伴 X
LOOP AT lt_zeuc ASSIGNING FIELD-SYMBOL(<ls3>).
  <ls3>-best_fit = COND #( WHEN <ls3>-id = ls_target-id  THEN 'O'
                           WHEN <ls3>-distance = lv_min  THEN 'X'
                           ELSE space ).
ENDLOOP.

" 9) 用 CL_SALV_TABLE 輸出,取代一堆 WRITE
TRY.
    cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_salv)
                            CHANGING  t_table      = lt_zeuc ).
    lo_salv->get_functions( )->set_all( abap_true ).
    lo_salv->get_columns( )->set_optimize( abap_true ).
    lo_salv->display( ).
  CATCH cx_salv_msg INTO DATA(lx_salv).
    MESSAGE lx_salv->get_text( ) TYPE 'E'.
ENDTRY.
母體標準差(除以 n)與樣本標準差(除以 n−1)不同。講義的 ZSTDDEV_DEMO 用的是 除以 n(母體);RMMATH1_VARIANCE_DEVIATION 的回傳值請自行以測試資料比對確認,再決定要不要調整。

25. 綜合練習二:標準差與 Z-score 找異常值

講義 10-3 的 ZSTDDEV_DEMO 用一組固定數值示範 sum / count / mean / stddev,接著加上 Z-score 的六行程式。進階挑戰是:把這個概念套到 ACDOCA 的會計科目 740300,找出借方金額的異常值

*&---------------------------------------------------------------------*
*& Report ZDEMO_STDDEV  標準差 / Z-score 找異常值(原 ZSTDDEV_DEMO 進階版)
*&---------------------------------------------------------------------*
REPORT zdemo_stddev.

PARAMETERS: p_bukrs TYPE acdoca-rbukrs OBLIGATORY,
            p_gjahr TYPE acdoca-gjahr  OBLIGATORY,
            p_racct TYPE acdoca-racct  DEFAULT '0000740300',
            p_z     TYPE p LENGTH 3 DECIMALS 1 DEFAULT '2.0'.  " |z| 超過就算異常

TYPES: BEGIN OF ty_line,
         belnr  TYPE acdoca-belnr,
         docln  TYPE acdoca-docln,
         budat  TYPE acdoca-budat,
         hsl    TYPE acdoca-hsl,
         zscore TYPE p LENGTH 8 DECIMALS 4,
         flag   TYPE char1,
       END OF ty_line.

" 1) 讓 DB 先做過濾與加總(code pushdown)
SELECT belnr, docln, budat, hsl
  FROM acdoca
  WHERE rldnr  = '0L'
    AND rbukrs = @p_bukrs
    AND gjahr  = @p_gjahr
    AND racct  = @p_racct
    AND drcrk  = 'S'                  " 借方
  ORDER BY belnr, docln
  INTO CORRESPONDING FIELDS OF TABLE @DATA(lt_data).

IF lt_data IS INITIAL.
  MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'E'.
  RETURN.
ENDIF.

" 2) 平均與母體標準差
DATA(lv_n)    = lines( lt_data ).
DATA(lv_sum)  = REDUCE f( INIT s = 0 FOR ls IN lt_data NEXT s = s + ls-hsl ).
DATA(lv_mean) = lv_sum / lv_n.
DATA(lv_std)  = sqrt( REDUCE f( INIT s = 0 FOR ls IN lt_data
                                NEXT s = s + ( ls-hsl - lv_mean ) ** 2 ) / lv_n ).

" 3) 算 Z-score 並標記異常
LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<ls>).
  <ls>-zscore = COND #( WHEN lv_std > 0 THEN ( <ls>-hsl - lv_mean ) / lv_std ELSE 0 ).
  <ls>-flag   = COND #( WHEN abs( <ls>-zscore ) >= p_z THEN 'X' ELSE space ).
ENDLOOP.

WRITE: / |筆數 { lv_n },加總 { lv_sum },平均 { lv_mean },標準差 { lv_std }|.
ULINE.
WRITE: / '傳票號', 25 '行號', 35 '過帳日', 50 '金額', 70 'Z-score', 85 '異常'.
LOOP AT lt_data INTO DATA(ls_out).
  WRITE: /  ls_out-belnr,
         25 ls_out-docln,
         35 ls_out-budat,
         50 ls_out-hsl,
         70 ls_out-zscore,
         85 ls_out-flag.
ENDLOOP.
若系統是 HANA,同樣的統計可以用 AMDP 的 STDDEV( )/MEDIAN( ) 一次算完(見第 21 節),資料量大時差距非常明顯。這就是「先用 ABAP 想清楚邏輯,再 pushdown 到 DB」的標準流程。

26. 長文本(Long Text)與內表

SAP 的長文本(訂單抬頭備註、料件長描述等)不存在一般欄位,而是存在 STXH(表頭)/STXL(內容,壓縮),並且用四個鍵定位:TDOBJECT(物件)、TDID(文字 ID)、TDSPRAS(語言)、TDNAME(物件鍵)。內容以 TLINE 結構的內表傳遞,所以它其實是內表章的延伸應用。

*&---------------------------------------------------------------------*
*& Report ZDEMO_LONGTEXT  READ_TEXT / SAVE_TEXT
*&---------------------------------------------------------------------*
REPORT zdemo_longtext.

DATA: lt_line TYPE STANDARD TABLE OF tline WITH EMPTY KEY,
      ls_head TYPE thead.

" ---- 讀長文 ----
CALL FUNCTION 'READ_TEXT'
  EXPORTING
    id                      = '0002'          " 文字 ID
    language                = sy-langu
    name                    = '1110599005'    " 物件鍵(例如銷售訂單號)
    object                  = 'VBBK'          " 物件(VBBK = 銷售文件抬頭)
  TABLES
    lines                   = lt_line
  EXCEPTIONS
    id                      = 1
    language                = 2
    name                    = 3
    not_found               = 4
    object                  = 5
    reference_check         = 6
    wrong_access_to_archive = 7
    OTHERS                  = 8.

IF sy-subrc = 0.
  LOOP AT lt_line INTO DATA(ls_line).
    WRITE: / ls_line-tdline.
  ENDLOOP.
ENDIF.

" ---- 寫長文 ----
CLEAR lt_line.
APPEND VALUE #( tdformat = '*' tdline = 'TEST WRITE A TEXT' ) TO lt_line.

ls_head = VALUE #( tdobject = 'VBBK'
                   tdid     = '0002'
                   tdspras  = sy-langu
                   tdname   = '1110599005' ).

CALL FUNCTION 'SAVE_TEXT'
  EXPORTING
    header          = ls_head
    savemode_direct = abap_true
  TABLES
    lines           = lt_line
  EXCEPTIONS
    id              = 1
    language        = 2
    name            = 3
    object          = 4
    OTHERS          = 5.

IF sy-subrc = 0.
  COMMIT WORK AND WAIT.        " SAVE_TEXT 之後一定要 COMMIT
  MESSAGE '長文已儲存' TYPE 'S'.
ENDIF.
STXL 的內容是壓縮存放的,不可以直接 SELECT STXL 來讀文字,一定要透過 READ_TEXT(或 CDS 的 text association)。想知道某段文字的 OBJECT/ID/NAME 是什麼,在該欄位上按「移至 → 標頭」或用 SE37 的 READ_TEXT 加除錯即可查出。

27. 內表操作總表(隨時複習用)

講義 01-09「內表操作的總結」列了 28 條,這裡整理成表並補上現代寫法:

#動作講義寫法現代寫法/備註
01清空CLEAR itab.無 header line 時即清整表
02清空REFRESH itab.不釋放記憶體;FREE 才釋放
03單筆新增INSERT wa INTO itab INDEX idx.INSERT VALUE #( ... ) INTO itab INDEX idx.
04單筆新增INSERT wa INTO TABLE itab.SORTED/HASHED 用這個
05附加APPEND wa TO itab.APPEND VALUE #( ... ) TO itab.(僅 STANDARD)
06累加COLLECT wa INTO itab.大量資料改用 SQL GROUP BY 或 LOOP GROUP BY
07多筆新增INSERT LINES OF it1 FROM 1 TO 9 INTO it2 INDEX idx.
08多筆附加APPEND LINES OF it1 FROM 1 TO 9 TO it2.比 LOOP+APPEND 快很多
09覆蓋MOVE it1 TO it2.已過時 → it2 = it1.
10覆蓋it2 = it1.推薦
11SQL 灌入SELECT ... INTO TABLE itab.SELECT ... INTO TABLE @DATA(itab).
12SQL 追加SELECT ... APPENDING TABLE itab.APPENDING TABLE @itab.
13單筆修改MODIFY itab FROM wa INDEX idx.或 LOOP ... ASSIGNING 就地改
14單筆部分修改MODIFY itab FROM wa INDEX idx TRANSPORTING f1.
15多筆修改MODIFY itab FROM wa TRANSPORTING f2 WHERE f1 = '0000'.批次改的首選
16刪一筆DELETE TABLE itab FROM wa.依主鍵
17刪一筆DELETE itab INDEX idx.
18刪多筆DELETE itab WHERE ...最常用
19刪區間DELETE itab FROM idx1 TO idx2.
20去重DELETE ADJACENT DUPLICATES FROM itab.務必先 SORT
21讀取READ TABLE itab INTO wa INDEX i.itab[ i ]
22讀取READ TABLE itab INTO wa WITH KEY k1 = v1.itab[ k1 = v1 ]
23讀取READ ... WITH KEY ... BINARY SEARCH.需先 SORT;或直接用 SORTED TABLE
24排序SORT itab BY f1 ASCENDING / DESCENDING.一律明寫 BY
25計數DESCRIBE TABLE itab LINES lv_cnt.lv_cnt = lines( itab ).
26判斷(內表)IF sy-subrc = 0.或 IF itab IS NOT INITIAL / line_exists( )
27判斷(DB)IF sy-dbcnt = 0.SQL 實際處理筆數
28判斷CHECK sy-subrc = 0.條件不成立就跳過本次迴圈剩下的敘述

SAP vs MSSQL 對照(講義 03-04)

動作MSSQL T-SQLABAP SQLABAP 內表
新增INSERTINSERTINSERT / COLLECT / APPEND / it_a = it_b
修改UPDATEUPDATEMODIFY
刪除DELETEDELETEDELETE
查詢SELECTSELECTREAD TABLE / LOOP AT / itab[ ]
增改(Upsert)MERGEMODIFYCOLLECT(數字累加)
清空TRUNCATE TABLEDELETE FROM dbtab.CLEAR / CLEAR it[] / REFRESH / FREE
筆數SELECT COUNT(*)SELECT COUNT( * )lines( itab ) / DESCRIBE
判斷IF ... ELSEIF / CHECKIF / CHECK / line_exists( )
指令回傳@@ERRORSY-SUBRCSY-SUBRC
當前筆數 / 列號@@ROWCOUNTSY-DBCNTSY-TABIX

28. 講義練習索引

方便回頭對照原始講義:

編號原程式名主題本章對應節次
第 12 個練習Z202202_12內表的定義(OCCURS 0、BREAK-POINT)第 3 節
第 13 個練習Z202202_13內表的新增(INSERT / COLLECT / APPEND)第 5 節
第 14 個練習Z202202_14內表的更改(MODIFY ... TRANSPORTING ... WHERE)第 6 節
第 15 個練習Z202202_15內表的刪除與去重(DELETE ADJACENT DUPLICATES、USING KEY loop_key)第 7 節
第 16 個練習Z202202_16內表的讀取 READ第 8 節
第 17 個練習Z202202_17內表精簡操作、WITH HEADER LINE 與 CLEAR 的陷阱第 4 節
第 18~23 個練習Z202202_18 ~ _23SELECT SINGLE/彙總函數/JOIN/UPDATE/INSERT+DELETE/MODIFY第 13~19 節
第 64 個練習Z202202_64Native SQL EXEC SQL PERFORMING第 21 節
第 65 個練習Z202202_65DATA(@it):新版 740 內表精簡寫法、內表當資料來源第 17 節
第 66 個練習Z202202_66ACDOCA GROUP BY 加總第 16 節
期中題Z00_MIDTERM歐幾里德距離+Z-score+最適伙伴第 24 節
效能測試Z00_03HASHED vs SORTED 讀取效能(用 SE30/SAT 測)第 2 節
OPEN SQL 20 題ZOPENSQL01 ~ ZOPENSQL20UP TO ROWS、DISTINCT、PACKAGE SIZE、動態表名、INNER/LEFT JOIN、動態 WHERE、FOR ALL ENTRIES、GROUP BY、ORDER BY、IN、EXISTS、INSERT、UPDATE、DELETE、MODIFY、CL_SQL_STATEMENT第 14~21 節
標準差ZSTDDEV_DEMOsum / mean / stddev / z-score,進階套用到 ACDOCA 740300第 25 節
動態排序ZSORTSORT BY (欄位變數)、VALUE 建表、p_sort+5 偏移第 10 節
雙迴圈ZLOOP巢狀 LOOP、AT 事件、call by value/reference、header line第 9~12 節

SQL 終極大考驗:採購全鏈欄位(講義 09-1)

老師列了 26 個欄位當作綜合考題,要用一段 SQL 把採購從抬頭到收貨串起來:

#欄位說明#欄位說明
01EKKO-EBELN採購單號14MARA-MTART物料類型
02EKKO-BUKRS公司代碼15MAKT-MAKTX物料名稱
03EKKO-BSTYP採購類別16EKPO-INFNR價格條件(採購資訊記錄)
04EKKO-BSART採購單別17EINA-ERDAT條件日期
05EKKO-LIFNR採購廠商18EKPO-WERKS工廠代碼
06LFA1-NAME1廠商名稱19EKPO-LGORT儲存位置
07EKKO-AEDAT建檔日期20EKPO-MATKL物料群組
08EKKO-ERNAM建檔人員21EKPO-MENGE採購數量
09EKKO-EKORG採購組織22EKPO-NETPR採購單價
10EKKO-EKGRP採購群組23EKPO-NETWR採購金額
11EKKO-WAERS採購幣別24EKBE-GJAHR物料年度
12EKPO-EBELP採購項次25EKBE-BELNR物料憑證號
13EKPO-MATNR物料編號26EKBE-BWART移動類型
*&---------------------------------------------------------------------*
*& Report ZDEMO_PO_ALL  SQL 終極大考驗:採購全鏈 26 欄
*&---------------------------------------------------------------------*
REPORT zdemo_po_all.

SELECT-OPTIONS: s_ebeln FOR (ekko-ebeln),
                s_aedat FOR (ekko-aedat).

SELECT k~ebeln, k~bukrs, k~bstyp, k~bsart, k~lifnr,
       v~name1,
       k~aedat, k~ernam, k~ekorg, k~ekgrp, k~waers,
       p~ebelp, p~matnr,
       m~mtart,
       t~maktx,
       p~infnr,
       a~erdat,
       p~werks, p~lgort, p~matkl, p~menge, p~netpr, p~netwr,
       h~gjahr, h~belnr, h~bwart
  FROM ekko AS k
  INNER JOIN ekpo AS p ON p~ebeln = k~ebeln
  LEFT OUTER JOIN lfa1 AS v ON v~lifnr = k~lifnr
  LEFT OUTER JOIN mara AS m ON m~matnr = p~matnr
  LEFT OUTER JOIN makt AS t ON t~matnr = p~matnr
                           AND t~spras = @sy-langu
  LEFT OUTER JOIN eina AS a ON a~infnr = p~infnr
  LEFT OUTER JOIN ekbe AS h ON h~ebeln = p~ebeln
                           AND h~ebelp = p~ebelp
                           AND h~vgabe = '1'          " 1 = 收貨
  WHERE k~ebeln IN @s_ebeln
    AND k~aedat IN @s_aedat
    AND p~loekz = @space
  ORDER BY k~ebeln, p~ebelp, h~gjahr, h~belnr
  INTO TABLE @DATA(lt_po)
  UP TO 5000 ROWS.

TRY.
    cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_salv)
                            CHANGING  t_table      = lt_po ).
    lo_salv->get_functions( )->set_all( abap_true ).
    lo_salv->get_columns( )->set_optimize( abap_true ).
    lo_salv->display( ).
  CATCH cx_salv_msg INTO DATA(lx).
    MESSAGE lx->get_text( ) TYPE 'E'.
ENDTRY.

傳票雙檔常用欄位(講義 09-2)

講義的範例是 BSEG(檔身)INNER JOIN BKPF(檔頭),常用欄位:BUKRS、BELNR、GJAHR、VBELN、VORGN、BUDAT、VALUT、KOSTL、HKONT、WERKS、PAOBJNR、LIFNR、MATNR、MENGE、ERFME、PSWSL、DMBTR、EBELN、EBELP、PRCTR。

講義在 a~budatb~valut 旁註記「檔身的生效日期,是錯的」——過帳日期 BUDAT 在檔頭 BKPF,不在檔身;BSEG-VALUT 是「起息日」,不等於生效日。
更重要的是:在 S/4HANA 上,這類報表請改讀 ACDOCA(一張表就有檔頭+檔身欄位,不必 JOIN,而且欄位齊全、效能好得多)。BSEG/BKPF 仍在,但 JOIN 大量資料會很慢。

練習與自我檢核

  1. (作業 1・內表)針對 20190101~20191231 的接單資料做 Group by 分析:輸出「客戶編號、下單金額」。提示:用 COLLECT 練一次,再用 SQL GROUP BY kunnr 寫一次,比較兩者效能與程式長度。
  2. (作業 2・內表)同期間做 RFM 分析:輸出「客戶編號、累計下單金額、累計下單筆數、最近下單日、首次下單日」。提示:一句 SQL 即可 —— SELECT kunnr, SUM( netwr ), COUNT( * ), MAX( audat ), MIN( audat ) ... GROUP BY kunnr
  3. (作業 3・內表)建立 Euclidean distance 的計算結果:輸出「客戶編號、累計下單金額、累計下單筆數、金額距離、筆數距離」。提示:LOOP + MODIFY,或用 field-symbol 就地寫回;先做 Z-score 標準化,否則金額會壓過筆數。
  4. (作業 4・內表)請證明「READ TABLE 只讀符合條件的第一筆」。提示:造一張有三筆相同 key 的內表,READ 後印出 sy-tabix;再用 LOOP ... WHERE 對照筆數。當做要教別人,把程式改得讓人一看就信。
  5. (作業 5・ABAP SQL)GROUP BY、ORDER BY 及 STRING_AGG,把 VBAP 的 MATNR 依訂單彙整並列出前十筆。提示:SELECT vbeln, STRING_AGG( matnr, ',' ) AS mat_list, COUNT( * ) AS cnt FROM vbap GROUP BY vbeln ORDER BY cnt DESCENDING INTO TABLE @DATA(lt) UP TO 10 ROWS.
  6. 三種內表型別各自的 Big O 是多少?為什麼 HASHED TABLE 不能用 READ TABLE ... INDEX答:O(n)/O(log n)/O(1);HASHED 沒有主表索引。
  7. DELETE ADJACENT DUPLICATES FROM itab COMPARING f1 f2. 執行前一定要先做什麼?不做會怎樣?答:先 SORT BY f1 f2;不做只會刪掉「剛好相鄰」的重複列,資料看起來少了一些又沒去乾淨。
  8. FOR ALL ENTRIES 的驅動內表如果是空的,會發生什麼事?正確的防呆寫法是什麼?答:整段 WHERE(含其他條件)被忽略,把整張表撈回來;一律先 IF lt_x IS NOT INITIAL.
  9. SELECT SINGLE 與 SELECT ... UP TO 1 ROWS 差在哪?要「取最新一筆」該用哪一個、為什麼?答:SINGLE 要主鍵給齊、不能 ORDER BY;取最新一筆必須用 UP TO 1 ROWS + ORDER BY,因為 HANA 沒有排序保證。
  10. AT NEW / AT END OF 有哪三個前提或副作用?答:必須先排序;比對該欄位「及其左邊所有欄位」,所以結構欄位順序有影響;區塊內右側欄位會被填成 * 或 0。
  11. 把下面這段舊程式改成 7.40+ 寫法:READ TABLE it WITH KEY field1 = 'X' INTO wa. IF sy-subrc = 0. WRITE wa-fielda. ENDIF.答:IF line_exists( it[ field1 = 'X' ] ). WRITE it[ field1 = 'X' ]-fielda. ENDIF.
  12. 什麼情況該建 CDS View、什麼情況直接在程式裡 JOIN?答:邏輯會被多支程式重複使用、要給 Fiori/OData 用 → CDS;一次性、條件隨選擇畫面變動 → 程式內 JOIN。
  13. S/4HANA 上查 ACDOCA 必帶哪些條件?為什麼不該再用 BSEG+BKPF 做大報表?答:RLDNR、RBUKRS、GJAHR(再加期間或傳票號);ACDOCA 是 Universal Journal,一張表就有全部明細,不必 JOIN。
  14. EXEC SQL 為什麼不該再用?替代方案是什麼?答:已 obsolete,且不做 client 處理、不走表緩衝、不檢查授權;改用 ADBC(CL_SQL_STATEMENT)、AMDP 或 CDS。
  15. 把「LOOP 內對每一列下 SELECT」的程式改寫成三種更快的版本,並說明各自適用時機。答:JOIN(關聯明確)/子查詢 EXISTS(存在性判斷)/先撈進內表再用 SORTED KEY 對照(資料要重複使用)。
  16. ZKEPH 成本抓錯的根因是什麼?從中該學到的通則是什麼?答:成本評價後有 item category H 與 cost component E 兩種角度、欄位數量不同,程式沒排序就 READ,忽 H 忽 E 導致結果不穩定;通則是「只取一筆」之前必須先確定排序或明確過濾條件

第 2-3 章 ALV 報表:欄位目錄 / 事件 / 紅綠燈 / 小計 / 下載 / 新式 ALV

ABAP 顧問一輩子寫最多的東西就是報表,而 SAP 的報表標準就叫 ALV(SAP List Viewer)。這一章從 1996 年的 REUSE_ALV_GRID_DISPLAY 一路走到 2004 年的 CL_SALV_TABLE,再走到 S/4HANA 的 CDS + Fiori List Report:告訴你欄位目錄(Field Catalog)怎麼建、17 個事件怎麼掛、紅綠燈與顏色怎麼點亮、小計與變式怎麼做、雙擊怎麼跳到 ME23N、以及怎麼把畫面丟成 Excel/PDF。學完之後,你能在 30 秒內生一支能看的報表,也能在半天內生一支能編輯、能存檔、能鑽取、能匯出的正式報表。

核心 TCODESE38SE80SE37SE41SE11OAERSE78ABAPDOCUABAPHELPME23NVA03VL03NMM03FB03MIGOSE93
關鍵物件CL_SALV_TABLE、CL_GUI_ALV_GRID、CL_GUI_CUSTOM_CONTAINER、CL_SALV_COLUMNS_TABLE、CL_SALV_SORTS、CL_SALV_EVENTS_TABLE、SLIS 類型組、LVC_T_FCAT/LVC_S_LAYO、DISVARIANT、REUSE_ALV_GRID_DISPLAY、REUSE_ALV_HIERSEQ_LIST_DISPLAY、VBAK/VBAP、EKKO/EKPO/EKET、SFLIGHT/SCARR/SPFLI

1. ALV 家族演進:先知道自己站在哪一代

老師在講義裡把 ABAP 的工作排序講得很直白:報表開發=ALV、單據列印=SmartForms/Form、財務樞紐=Report Painter。而 ALV 本身歷經三代,第四代則已經跳到 Fiori。

R/3 4.6C 之前 ALV 全稱是 ABAP List Viewer,之後正式更名為 SAP List Viewer。ALV 從最早的 List Viewer 發展到 Grid Control:LIST 型 ALV 跟傳統 WRITE 輸出樣式相近,只是多了一排功能按鈕;GRID 型 ALV 則長得像 Excel。

世代Function ALVGRID ALV(OOALV)SALV(OOALV)Fiori(S/4HANA)
年代1996200020042015 以後
版本SAP R/3 3.11SAP R/3 4.5SAP NetWeaverS/4HANA
代表物件REUSE_ALV_LIST_DISPLAY
REUSE_ALV_GRID_DISPLAY
CL_GUI_ALV_GRID
REUSE_ALV_GRID_DISPLAY_LVC
CL_SALV_TABLE
CL_SALV_FUNCTIONS_LIST
CDS View + Annotation
List Report / OData
要不要 Screen不需螢幕控制就能顯示需要螢幕控制(Dialog + Custom Control)不需螢幕控制就能顯示(也可嵌在容器)不需 SAP GUI,瀏覽器執行
物件導向是(宣告式)
功能普通豐富(可編輯、細部事件)豐富且簡潔最豐富(含搜尋、變式、匯出)
本書案例本章第 1~3 節本章第 10 節、ABAP3_2 第 1–13 節本章第 2~3 節、ABAP3_2 第 16 節本章第 15 節
選型口訣:「看就好 → SALV;要編輯/要細部事件 → CL_GUI_ALV_GRID;要給業務在手機上看 → CDS + Fiori」。新專案不要再從 REUSE_ALV_GRID_DISPLAY 開始寫。
講義的第一個範例是 REUSE_ALV_LIST_DISPLAY / REUSE_ALV_GRID_DISPLAY 直接餵 I_STRUCTURE_NAME = 'MATDOC'。在 S/4HANA 上要特別注意兩件事:
(1)MATDOC 是 S/4 的物料憑證單一來源(取代 MKPF/MSEG),欄位極多(數百欄),整表丟 ALV 會非常慢,務必只選需要的欄位;
(2)這兩支 FM 至今仍可用、但已被 SAP 標記為「不再增強」,新程式請改用 CL_SALV_TABLE

下面是講義最原始的兩支範例(ZREUSE_ALV01 / ZREUSE_ALV02)改寫成現代寫法後的等價版本,用來體會「三行就有一張報表」的感覺:

REPORT zreuse_alv01.
" 講義原文用 REUSE_ALV_LIST_DISPLAY / REUSE_ALV_GRID_DISPLAY 直接吃 MATDOC
" S/4HANA 建議寫法:明列欄位 + CL_SALV_TABLE
SELECT mblnr, mjahr, zeile, bwart, matnr, werks, lgort, menge, meins
  FROM matdoc
  UP TO 5 ROWS
  INTO TABLE @DATA(lt_matdoc).

TRY.
    cl_salv_table=>factory(
      IMPORTING r_salv_table = DATA(lo_alv)
      CHANGING  t_table      = lt_matdoc ).
    lo_alv->display( ).
  CATCH cx_salv_msg INTO DATA(lx_msg).
    MESSAGE lx_msg->get_text( ) TYPE 'E'.
ENDTRY.

2. 最短的 ALV:CL_SALV_TABLE 三步驟

講義第 04 節標題就叫「NEW ALV(SALV) 2004 發佈,最短的 ALV」,原始程式是 ZSALV。SALV 的三步驟永遠一樣:

  1. 備資料:SELECT 進內表。
  2. factorycl_salv_table=>factory( ) 把內表交給 ALV,拿回一個 lo_alv 參考。
  3. displaylo_alv->display( )
REPORT zsalv.
" 講義原始 ZSALV:SELECT * FROM sflight ... + cl_salv_table=>factory + display
" 現代寫法:明列欄位、內嵌宣告、TRY/CATCH

SELECT carrid, connid, fldate, price, currency, seatsmax, seatsocc
  FROM sflight
  UP TO 25 ROWS
  INTO TABLE @DATA(lt_flight).

TRY.
    " list_display = IF_SALV_C_BOOL_SAP=>false → GRID 樣式(Excel 風格)
    " list_display = IF_SALV_C_BOOL_SAP=>true  → LIST 樣式(傳統清單)
    cl_salv_table=>factory(
      EXPORTING list_display = if_salv_c_bool_sap=>false
      IMPORTING r_salv_table = DATA(lo_alv)
      CHANGING  t_table      = lt_flight ).

    lo_alv->display( ).

  CATCH cx_salv_msg INTO DATA(lx_msg).
    MESSAGE lx_msg->get_text( ) TYPE 'E'.
ENDTRY.
factory 的三種輸出容器
• 不帶 r_container → 全螢幕 ALV(最常用)。
• 帶 r_container = lo_containerCL_GUI_CUSTOM_CONTAINER)+ container_name → 嵌在自畫的 Screen 裡。
• 帶 CL_GUI_DOCKING_CONTAINER → 停靠在選擇畫面旁邊,做「不用另開畫面」的報表。
SALV 的資料表不能是有 HEADER LINE 的舊式內表,也不能是深層巢狀(含內表的內表)結構,否則 factory 會丟 CX_SALV_MSG。另外 t_table 是 CHANGING 參數,代表 ALV 直接引用你的內表——顯示期間不要把它 REFRESH 掉。

3. CL_SALV_TABLE 完整用法(一支可直接跑的範例)

SALV 的設計是「一個 ALV 分成好幾個管理員物件,各管一件事」。把下表背起來,SALV 就等於學會了:

取得方法回傳類別管什麼
get_functions( )CL_SALV_FUNCTIONS_LIST工具列按鈕:set_all、set_export_xxl、set_default
get_columns( )CL_SALV_COLUMNS_TABLE欄位:抬頭、寬度、隱藏、順序、紅綠燈、熱點
get_column( 'X' )CL_SALV_COLUMN_TABLE單一欄位的細部設定
get_sorts( )CL_SALV_SORTS排序、群組、小計(subtotal)
get_aggregations( )CL_SALV_AGGREGATIONS合計/平均/最大最小
get_display_settings( )CL_SALV_DISPLAY_SETTINGS抬頭文字、斑馬紋、水平/垂直格線
get_layout( )CL_SALV_LAYOUTLayout 變式(儲存/預設/限制)
get_selections( )CL_SALV_SELECTIONS選取模式、取得選取行/選取格
get_event( )CL_SALV_EVENTS_TABLE事件:ADDED_FUNCTION、DOUBLE_CLICK、LINK_CLICK
get_functional_settings( )CL_SALV_FUNCTIONAL_SETTINGSTooltip、F4 說明
top_of_page / end_of_pageCL_SALV_FORM_LAYOUT_GRID報表抬頭/頁尾(取代 TOP_OF_PAGE 事件)

以下是把講義精神(VBAK ⋈ VBAP、紅綠燈、排序小計、雙擊跳 VA03、自訂按鈕)全部用 SALV 重寫的完整可執行範例。這支可以當你日後所有 SALV 報表的骨架:

*&--------------------------------------------------------------------*
*& Report ZSALV_DEMO_FULL   ── CL_SALV_TABLE 完整範本(S/4HANA 1809+)
*& 對應講義 Z202202_24 / _25 / _26 的所有功能,但改用 SALV 實作
*&--------------------------------------------------------------------*
REPORT zsalv_demo_full.

TYPES: BEGIN OF ty_out,
         light TYPE char4,       " 紅綠燈欄位(exception column,值 1/2/3)
         vbeln TYPE vbak-vbeln,  " 訂單編號
         erdat TYPE vbak-erdat,  " 訂單日期
         ernam TYPE vbak-ernam,  " 輸入人員
         kunnr TYPE vbak-kunnr,  " 客戶編號
         posnr TYPE vbap-posnr,  " 訂單項次
         matnr TYPE vbap-matnr,  " 物料編號
         matkl TYPE vbap-matkl,  " 物料組
         zmeng TYPE vbap-zmeng,  " 銷售數量
         zieme TYPE vbap-zieme,  " 銷售單位
         werks TYPE vbap-werks,  " 工廠
         lgort TYPE vbap-lgort,  " 儲存位置
       END OF ty_out.

DATA gt_out TYPE STANDARD TABLE OF ty_out WITH EMPTY KEY.
DATA go_alv TYPE REF TO cl_salv_table.

*--------------------------------------------------------------------*
* 事件處理類別:SALV 的事件一定要有一個 handler 類別
*--------------------------------------------------------------------*
CLASS lcl_handler DEFINITION.
  PUBLIC SECTION.
    METHODS on_user_command FOR EVENT added_function OF cl_salv_events
      IMPORTING e_salv_function.
    METHODS on_double_click FOR EVENT double_click  OF cl_salv_events_table
      IMPORTING row column.
    METHODS on_link_click   FOR EVENT link_click    OF cl_salv_events_table
      IMPORTING row column.
ENDCLASS.

CLASS lcl_handler IMPLEMENTATION.

  " 自訂按鈕(GUI Status 上的功能碼)
  METHOD on_user_command.
    CASE e_salv_function.
      WHEN 'ZCAS'.
        MESSAGE '你按下了 CASPER 按鈕' TYPE 'I'.
      WHEN 'ZADD'.
        MESSAGE '你按下了 新增2 按鈕' TYPE 'I'.
      WHEN 'ZEXP'.
        PERFORM download_xlsx.       " 見第 14 節
      WHEN 'EXIT'.
        LEAVE PROGRAM.
    ENDCASE.
  ENDMETHOD.

  " 雙擊:Drilldown 到 VA03
  METHOD on_double_click.
    READ TABLE gt_out INTO DATA(ls_row) INDEX row.
    CHECK sy-subrc = 0.
    CASE column.
      WHEN 'VBELN'.
        SET PARAMETER ID 'AUN' FIELD ls_row-vbeln.
        CALL TRANSACTION 'VA03' AND SKIP FIRST SCREEN.
      WHEN 'MATNR'.
        SET PARAMETER ID 'MAT' FIELD ls_row-matnr.
        CALL TRANSACTION 'MM03' AND SKIP FIRST SCREEN.
    ENDCASE.
  ENDMETHOD.

  " 熱點單擊(滑鼠變成手指、按一下就觸發)
  METHOD on_link_click.
    READ TABLE gt_out INTO DATA(ls_row) INDEX row.
    CHECK sy-subrc = 0.
    MESSAGE |熱點:{ column } = { ls_row-vbeln }| TYPE 'S'.
  ENDMETHOD.

ENDCLASS.

*--------------------------------------------------------------------*
START-OF-SELECTION.

  " ── 1. 取資料:明列欄位、用 @ 逸出主機變數 ──────────────────
  SELECT FROM vbak AS a
         INNER JOIN vbap AS b ON a~vbeln = b~vbeln
         FIELDS a~vbeln, a~erdat, a~ernam, a~kunnr,
                b~posnr, b~matnr, b~matkl, b~zmeng, b~zieme,
                b~werks, b~lgort
         ORDER BY a~vbeln, b~posnr
         INTO CORRESPONDING FIELDS OF TABLE @gt_out
         UP TO 200 ROWS.

  IF gt_out IS INITIAL.
    MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.

  " ── 2. 算紅綠燈:1=紅 2=黃 3=綠 ─────────────────────────────
  LOOP AT gt_out ASSIGNING FIELD-SYMBOL(<ls_out>).
    <ls_out>-light = COND #( WHEN <ls_out>-zmeng >= 100 THEN '1'
                             WHEN <ls_out>-zmeng >=  10 THEN '2'
                             ELSE                            '3' ).
  ENDLOOP.

  PERFORM show_alv.

*&--------------------------------------------------------------------*
FORM show_alv.

  TRY.
      cl_salv_table=>factory(
        IMPORTING r_salv_table = go_alv
        CHANGING  t_table      = gt_out ).

      " ── 3. 工具列:把標準功能全開 ──────────────────────────
      DATA(lo_func) = go_alv->get_functions( ).
      lo_func->set_all( abap_true ).        " 排序/篩選/合計/匯出/變式全開

      " ── 4. 自訂 GUI Status(SE41 複製 SAPLSALV 的 STANDARD)──
      go_alv->set_screen_status(
        report        = sy-repid
        pfstatus      = 'ZSALV_STATUS'
        set_functions = go_alv->c_functions_all ).

      " ── 5. 事件註冊 ────────────────────────────────────────
      DATA(lo_events) = go_alv->get_event( ).
      DATA(lo_handler) = NEW lcl_handler( ).
      SET HANDLER lo_handler->on_user_command FOR lo_events.
      SET HANDLER lo_handler->on_double_click FOR lo_events.
      SET HANDLER lo_handler->on_link_click   FOR lo_events.

      " ── 6. 欄位設定 ────────────────────────────────────────
      DATA(lo_cols) = go_alv->get_columns( ).
      lo_cols->set_optimize( abap_true ).           " 自動最佳欄寬
      lo_cols->set_exception_column( 'LIGHT' ).     " 指定紅綠燈欄位
      lo_cols->set_key_fixation( abap_true ).       " Key 欄位凍結不捲動

      " 單一欄位:抬頭文字、隱藏、熱點、加總
      DATA(lo_col) = CAST cl_salv_column_table( lo_cols->get_column( 'VBELN' ) ).
      lo_col->set_short_text( '訂單' ).
      lo_col->set_medium_text( '訂單編號' ).
      lo_col->set_long_text( '客戶訂單編號' ).
      lo_col->set_key( abap_true ).                                  " 藍色鍵值欄
      lo_col->set_cell_type( if_salv_c_cell_type=>hotspot ).         " 熱點

      CAST cl_salv_column_table( lo_cols->get_column( 'LGORT' )
        )->set_visible( abap_false ).                                " 隱藏欄位

      CAST cl_salv_column_table( lo_cols->get_column( 'ZMENG' )
        )->set_quantity_column( 'ZIEME' ).   " 數量欄要綁單位欄才會正確顯示

      " ── 7. 排序 + 小計 ─────────────────────────────────────
      DATA(lo_sorts) = go_alv->get_sorts( ).
      lo_sorts->add_sort( columnname = 'KUNNR'
                          sequence   = if_salv_c_sort=>sort_up
                          subtotal   = abap_true ).   " 依客戶小計
      lo_sorts->add_sort( columnname = 'ERDAT'
                          sequence   = if_salv_c_sort=>sort_down ).

      " ── 8. 合計 ────────────────────────────────────────────
      DATA(lo_aggr) = go_alv->get_aggregations( ).
      lo_aggr->add_aggregation( columnname  = 'ZMENG'
                                aggregation = if_salv_c_aggregation=>total ).

      " ── 9. 顯示設定 ────────────────────────────────────────
      DATA(lo_disp) = go_alv->get_display_settings( ).
      lo_disp->set_striped_pattern( abap_true ).       " 斑馬紋(= zebra)
      lo_disp->set_list_header( |VA01 客戶訂單 共 { lines( gt_out ) } 筆| ).

      " ── 10. Layout 變式(等同 disvariant / I_SAVE = 'A')────
      DATA(lo_layout) = go_alv->get_layout( ).
      lo_layout->set_key( VALUE #( report = sy-repid handle = '0001' ) ).
      lo_layout->set_save_restriction( cl_salv_layout=>restrict_none ).
      lo_layout->set_default( abap_true ).

      " ── 11. 選取模式:讓使用者可以勾選整列 ──────────────────
      go_alv->get_selections( )->set_selection_mode(
        if_salv_c_selection_mode=>row_column ).

      " ── 12. 報表抬頭(取代舊的 TOP_OF_PAGE 事件)────────────
      DATA(lo_grid) = NEW cl_salv_form_layout_grid( ).
      lo_grid->create_label( row = 1 column = 1 text = 'VA01 客戶訂單' ).
      lo_grid->create_label( row = 2 column = 1 text = '僅顯示部份欄位' ).
      lo_grid->create_label( row = 2 column = 2
                             text = |執行者:{ sy-uname } { sy-datum DATE = USER }| ).
      go_alv->set_top_of_list( lo_grid ).

      go_alv->display( ).

    CATCH cx_salv_error INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.

ENDFORM.
老師的口訣:「SALV 就是拿管理員、下指令、再 display」。每一段 get_xxx( ) 都可以獨立抄,忘記哪個方法就在 SE24 打開 CL_SALV_COLUMN_TABLE 看方法清單,比查文件快。
取得使用者勾選了哪幾行
DATA(lt_rows) = go_alv->get_selections( )->get_selected_rows( ).
回傳的是 SALV_T_ROW(列號清單),再用 LOOP AT lt_rows INTO DATA(lv_row). READ TABLE gt_out INDEX lv_row ... 取值。
取得游標所在儲存格:get_current_cell( )

4. Field Catalog(欄位目錄):ALV 的靈魂

欄位目錄用來控制 ALV 每一欄的屬性:順序、對齊、可編輯、顏色、加總、要不要顯示⋯⋯。傳統 ALV 有三種建法,各有適用場合:

作法怎麼做適合
① 全自動(DDIC)FM 參數 I_STRUCTURE_NAME = 'SFLIGHT';SALV 則是 factory 自動依內表行型別產生內表結構=某個 DDIC 結構/資料表,欄位全部要顯示
② 半自動(MERGE)REUSE_ALV_FIELDCATALOG_MERGE 讀程式內表的宣告或 DDIC 結構自動產生,再手工覆寫少數欄位欄位很多、只想改其中幾個屬性
③ 全手動逐欄 APPEND 一筆 SLIS_FIELDCAT_ALV / LVC_S_FCAT內表是自訂 TYPES、抬頭要中文、要精準控制

講義 Z202202_24 是全手動版本,逐欄 CLEAR / col_pos + 1 / APPEND,寫了一百多行。現代 ABAP 可以用 VALUE #( ) 一次寫完,可讀性天差地遠:

" ── 手動建 Field Catalog(LVC 版,給 CL_GUI_ALV_GRID 用)──────────
" 講義 Z202202_24 逐欄 APPEND 的一百多行,用 VALUE #( ) 濃縮成一段
DATA(lt_fcat) = VALUE lvc_t_fcat(
  " col_pos 若照順序寫可以省略,ALV 會依 table 順序排
  ( fieldname = 'VBELN' col_pos = 1  key = abap_true
    outputlen = 10 scrtext_m = '訂單編號' hotspot = abap_true )
  ( fieldname = 'ERDAT' col_pos = 2  outputlen = 10 scrtext_m = '訂單日期' )
  ( fieldname = 'ERNAM' col_pos = 3  outputlen = 12 scrtext_m = '輸入人員' )
  ( fieldname = 'KUNNR' col_pos = 4  outputlen = 10 scrtext_m = '客戶編號' )
  ( fieldname = 'POSNR' col_pos = 5  outputlen = 6  scrtext_m = '項次' )
  ( fieldname = 'MATNR' col_pos = 6  outputlen = 40 scrtext_m = '物料編號'
    ref_table = 'VBAP' ref_field = 'MATNR' )   " 給 F1/F4 說明用
  ( fieldname = 'MATKL' col_pos = 7  outputlen = 9  scrtext_m = '物料組' )
  ( fieldname = 'ZMENG' col_pos = 8  outputlen = 15 scrtext_m = '銷售數量'
    do_sum = abap_true            " 自動加總
    edit   = abap_true            " 可編輯
    decimals_o = 3                " 可編輯且有小數,一定要指定小數位
    qfieldname = 'ZIEME' )        " 綁單位欄位
  ( fieldname = 'ZIEME' col_pos = 9  outputlen = 3  scrtext_m = '銷售單位' )
  ( fieldname = 'WERKS' col_pos = 10 outputlen = 4  scrtext_m = '工廠' )
  ( fieldname = 'LGORT' col_pos = 11 outputlen = 4  scrtext_m = '儲存位置' ) ).

如果內表結構直接來自 DDIC,最省事的是讓系統自己生,再微調:

" ── 半自動:先讓系統依 DDIC 結構生成,再覆寫個別欄位 ──────────────
" 舊法:REUSE_ALV_FIELDCATALOG_MERGE(講義 BALVSD11 / BALVHD01 用它)
" 新法(推薦):CL_SALV_DATA_DESCR 或直接用 SALV,不必自己生 fieldcat
DATA lt_fcat TYPE lvc_t_fcat.

CALL FUNCTION 'LVC_FIELDCATALOG_MERGE'
  EXPORTING
    i_structure_name       = 'SFLIGHT'
    i_client_never_display = abap_true   " 自動隱藏 MANDT
  CHANGING
    ct_fieldcat            = lt_fcat
  EXCEPTIONS
    inconsistent_interface = 1
    program_error          = 2
    OTHERS                 = 3.
IF sy-subrc <> 0.
  MESSAGE '欄位目錄建立失敗' TYPE 'E'.
ENDIF.

" 覆寫個別欄位:用 LOOP ... ASSIGNING 修改,不要重建整張表
LOOP AT lt_fcat ASSIGNING FIELD-SYMBOL(<ls_fcat>).
  CASE <ls_fcat>-fieldname.
    WHEN 'CARRID'.   <ls_fcat>-key       = abap_true.
                     <ls_fcat>-outputlen = 7.
    WHEN 'PRICE'.    <ls_fcat>-do_sum    = abap_true.
    WHEN 'SEATSMAX'. <ls_fcat>-no_out    = abap_true.   " 隱藏但可從 layout 拉出來
    WHEN 'SEATSOCC'. <ls_fcat>-no_sum    = abap_true.   " 禁止加總
  ENDCASE.
ENDLOOP.
三個一定會踩的坑:
1. fieldname 必須大寫,而且要跟內表欄位名一字不差,寫錯不會報錯、只是那欄空白或不見。
2. 所有要顯示的欄位,內表一定要有對應欄位;ALV 不會幫你補。
3. 單引號內的字串(結構名、欄位名、功能碼)在呼叫系統物件時一律大寫

4-1. SLIS_FIELDCAT_ALV 常用屬性總表

這張表要自己找得到(SE11 查 SLIS_FIELDCAT_ALV;查不到的 SLIS 結構改用 SE80 開 type-pool SLIS)。以下屬性對 REUSE_ALV_LIST_DISPLAYREUSE_ALV_GRID_DISPLAY 皆生效;LVC 版(LVC_S_FCAT)欄位名略有不同(例如 SELTEXT_M → SCRTEXT_M、REF_TABNAME → REF_TABLE)。

屬性用途類別
FIELDNAME內表欄位名(必填、大寫)對應輸出表
TABNAME內表名稱(多表/階層式必填)對應輸出表
COL_ID / COL_POS欄位識別碼(唯讀)/輸出位置輸出選項
ROW_POS第幾列顯示,預設 0,可 1/2/3,最多分 3 級輸出選項
KEY鍵值欄(藍底、固定在左)輸出選項
KEY_SEL與 KEY 併用,允許互動式隱藏 KEY 欄(LIST 有效)輸出選項
NO_OUT預設不顯示(使用者仍可由 Layout 拉出)輸出選項
TECH技術欄位,完全不顯示也不可拉出(像 MANDT)輸出選項
OUTPUTLEN / DD_OUTLEN / INTLEN輸出寬度/DDIC 輸出長度/內部長度寬度
EDIT該欄可編輯輸出選項
EMPHASIZE整欄顏色,格式 C(1-7)(0-1)(0-1);與 EDIT 同時設定會互相衝突顏色
HOTSPOT熱點:滑鼠變手指,單擊即觸發互動
CHECKBOX以核取方塊呈現輸出選項
ICON以圖示輸出,欄位內容須為有效 ICON 名稱(見 DDIC 的 ICON 表)格式化
SYMBOL以 Symbol 輸出,值可用常數如 SYM_FILLED_SQUARE(不加引號),程式要 INCLUDE <LIST>;參考 RSTXSYMB格式化
DO_SUM / NO_SUM自動加總/禁止加總小計
CFIELDNAME / CTABNAME貨幣單位欄位名/其所屬內表金額顯示
QFIELDNAME / QTABNAME計量單位欄位名/其所屬內表數量顯示
CURRENCY / QUANTITY固定的幣別/單位金額數量
REF_FIELD(NAME) / REF_TAB(NAME)參考 DDIC 欄位/資料表,用來產生 F1、F4DDIC 參照
ROLLNAME資料元素(F1 說明)無 DDIC 參照時使用
DATATYPE / INTTYPEDDIC 資料型別(CHAR/DATS/QUAN/CURR/NUMC/UNIT)/ABAP 型別無 DDIC 參照時使用
DECIMALS_O(UT) / DECMLFIELD輸出小數位數/小數位取自哪個欄位格式化
ROUND / ROUNDFIELD / EXPONENT四捨五入位數/來源欄位/浮點指數格式化
EDIT_MASK同 WRITE 的 edit mask,常用來格式化日期時間格式化
JUST對齊:R / L / C格式化
LZERO / NO_ZERO / NO_SIGN顯示前置零(NUMC 有效)/零值顯示空白/不顯示正負號格式化
LOWERCASE允許小寫輸出選項
FIX_COLUMN固定欄不隨捲軸捲動(必須排在左邊才生效)輸出選項
NO_MERGING不合併相同儲存格輸出選項
SELTEXT_S/M/L(LVC:SCRTEXT_S/M/L)短/中/長欄位抬頭文字
REPTEXT(_DDIC)抬頭,取自資料元素主標題文字
DDICTXT指定用 S / M / L 哪一種抬頭,設定後抬頭固定文字
COLTEXT / TOOLTIP對話功能用的欄名/欄抬頭提示文字
SP_GROUP群組鍵(配合 IT_SPECIAL_GROUPS 做欄位分群)其他
STYLE以按鈕呈現輸出選項
HREF_HNDLE / WEB_FIELD超連結代號/超連結欄位其他
DRAGDROPID拖放物件的 handle其他
REPREP作為 report/report 介面的選擇條件其他

4-2. Layout(版面)常用屬性

講義所有範例都會設這五行,可以當固定套路(SLIS_LAYOUT_ALV 版):

" ── 傳統 Layout(SLIS 版)常用設定,講義 Z202202_24~26 的固定五行 ──
DATA(ls_layout) = VALUE slis_layout_alv(
  zebra             = abap_true      " 黃白(斑馬)相間
  detail_popup      = abap_true      " 彈出明細視窗
  detail_titlebar   = '詳細訊息'      " 明細視窗抬頭
  f2code            = '&ETA'         " 設定觸發明細視窗的功能碼(雙擊)
  colwidth_optimize = abap_true      " 最佳欄寬
  no_vline          = abap_false     " 欄間隔線
  no_colhead        = abap_false     " 不顯示欄抬頭
  no_input          = abap_false     " 全部欄位唯讀
  box_fieldname     = 'BOX'          " 勾選欄綁到內表的 BOX 欄位
  lights_fieldname  = 'LIGHTS'       " 紅綠燈綁到內表的 LIGHTS 欄位
  lights_condense   = abap_true      " 小計列的燈號取最差的一顆
  key_hotspot       = abap_true      " Key 欄位自動變熱點
  totals_text       = '總計'
  subtotals_text    = '小計'
  totals_only       = abap_false
  cell_merge        = abap_false
  group_change_edit = abap_true      " 允許群組換行控制
  detail_initial_lines = abap_true ).

" ── LVC 版(CL_GUI_ALV_GRID / REUSE_ALV_GRID_DISPLAY_LVC 用)──────
DATA(ls_layo) = VALUE lvc_s_layo(
  zebra      = abap_true
  cwidth_opt = abap_true        " 最佳欄寬(LVC 叫 cwidth_opt)
  sel_mode   = 'A'              " A=多列多格 B=單列 C=多列 D=儲存格
  grid_title = 'VA01 客戶訂單'
  smalltitle = abap_true
  box_fname  = 'BOX'            " 勾選欄
  excp_fname = 'LIGHT'          " 紅綠燈欄(LVC 叫 excp_fname)
  info_fname = 'ROWCOLOR'       " 整列顏色欄
  ctab_fname = 'CELLTAB'        " 單格樣式表(可編輯/顏色)
  stylefname = 'STYLETAB' ).
Grid 設定 I_GRID_SETTINGS(型別 LVC_S_GLAY)中最重要的一欄是 EDT_CLL_CB = 'X'編輯儲存格後把值回傳給程式。講義第 26 支程式特別強調——沒設這個,你在畫面上改的數字根本不會進到內表,存檔就會存到舊值。

5. 紅綠燈(Exception / Lights)與圖示欄位

紅綠燈有兩種做法,講義兩種都示範了:

  1. Exception 欄位:內表加一個 CHAR1(或 CHAR4)欄位,值填 1 = 紅、2 = 黃、3 = 綠,再告訴 ALV 這欄是燈號欄。
    傳統:layout-lights_fieldname = 'LIGHTS'(LVC:excp_fname)。
    SALV:lo_cols->set_exception_column( 'LIGHT' )
  2. ICON 欄位:內表欄位型別 icon-id(或 CHAR4),值填 icon_led_green / icon_led_red / icon_led_yellow 等常數,fieldcat 該欄設 icon = 'X'。這種做法可以一列放好幾顆燈(講義 ZEKET_43 就放了 LIGHT1/2/3 三顆)。
*&--------------------------------------------------------------------*
*& Report ZSALV_LIGHT  ── 掛上警示燈的 NEW ALV(講義原名 ZSALV_LIGHT)
*&--------------------------------------------------------------------*
REPORT zsalv_light.

TYPES: BEGIN OF ty_row.
         INCLUDE TYPE sflight.
TYPES:   light TYPE char1,     " 講義原本欄位名叫 EXCEPTION,屬保留字風險,改名
       END OF ty_row.

DATA gt_out TYPE STANDARD TABLE OF ty_row WITH EMPTY KEY.

SELECT carrid, connid, fldate, price, currency, seatsmax, seatsocc
  FROM sflight
  UP TO 25 ROWS
  INTO CORRESPONDING FIELDS OF TABLE @gt_out.

" 1=紅 2=黃 3=綠:座位佔用率越高越危險
LOOP AT gt_out ASSIGNING FIELD-SYMBOL(<ls>).
  <ls>-light = COND #( WHEN <ls>-seatsocc >= 90 THEN '1'
                       WHEN <ls>-seatsocc >= 50 THEN '2'
                       ELSE                          '3' ).
ENDLOOP.

TRY.
    cl_salv_table=>factory(
      IMPORTING r_salv_table = DATA(lo_alv)
      CHANGING  t_table      = gt_out ).

    DATA(lo_cols) = lo_alv->get_columns( ).
    lo_cols->set_exception_column( 'LIGHT' ).   " 指定燈號欄
    CAST cl_salv_column_table( lo_cols->get_column( 'LIGHT' )
      )->set_short_text( '提示' ).

    " 滑到燈上時顯示的說明文字(Tooltip)
    DATA(lo_tips) = lo_alv->get_functional_settings( )->get_tooltips( ).
    lo_tips->add_tooltip( type  = cl_salv_tooltip=>c_type_exception
                          value = '1' tooltip = '爆量:需立即處理' ).
    lo_tips->add_tooltip( type  = cl_salv_tooltip=>c_type_exception
                          value = '2' tooltip = '偏高:請留意' ).
    lo_tips->add_tooltip( type  = cl_salv_tooltip=>c_type_exception
                          value = '3' tooltip = '正常' ).

    lo_alv->get_functions( )->set_all( abap_true ).
    lo_alv->display( ).

  CATCH cx_salv_error INTO DATA(lx).
    MESSAGE lx->get_text( ) TYPE 'E'.
ENDTRY.

ICON 做法(講義 ZEKET_43 的 SET_LIGHT 副程式)現代化寫法:

" ── ICON 欄位版紅綠燈:一列可以放好幾顆燈 ──────────────────────
" 程式開頭要 TYPE-POOLS: icon.(S/4 亦可直接用 ICON 常數,不必宣告)
" 內表欄位:light1 TYPE icon-id, light2 TYPE icon-id, light3 TYPE icon-id

LOOP AT it_zeket ASSIGNING FIELD-SYMBOL(<ls_zeket>).
  " 交期已過 → 紅燈;三天內到期 → 黃燈;其餘 → 綠燈
  <ls_zeket>-light1 = SWITCH #( abap_true
      WHEN boolc( <ls_zeket>-eindt <  sy-datum )     THEN icon_led_red
      WHEN boolc( <ls_zeket>-eindt <= sy-datum + 3 ) THEN icon_led_yellow
      ELSE                                                icon_led_green ).

  " 也可以直接用 icon 常數表達語意:
  <ls_zeket>-light2 = COND #( WHEN <ls_zeket>-menge = 0
                              THEN icon_incomplete ELSE icon_complete ).
  <ls_zeket>-light3 = icon_display_more.
ENDLOOP.

" fieldcat 對應設定(LVC):
DATA(ls_fcat_light) = VALUE lvc_s_fcat(
  fieldname = 'LIGHT1' col_pos = 1 outputlen = 4
  scrtext_m = '燈' icon = abap_true ).
常用 ICON 常數:icon_led_red / icon_led_yellow / icon_led_green / icon_led_inactiveicon_green_light / icon_yellow_light / icon_red_lighticon_complete / icon_incompleteicon_display / icon_change / icon_delete。在 SE11 打開資料表 ICON 可以查全部,或執行 SHOWICON 目視挑選。
燈號欄位不要用名稱 EXCEPTION(講義原文用了 EXCEPTION type char1)。EXCEPTION 是 ABAP 關鍵字家族的一員,在某些語法情境會造成編譯困擾,建議改名 LIGHTSTATUS_LIGHT
另外,SALV 的 exception 欄位寬度不要自己設太寬,設 set_optimize( abap_true ) 讓它自己算最好看。

6. 顏色:整列、整欄、單一儲存格

ALV 的顏色碼是 4 個字元:C + 顏色碼(1–7) + 反白(0/1) + 反轉(0/1)

顏色碼顏色慣用語意
C100灰藍抬頭/標題列
C200淺灰藍Key 欄位
C300小計列
C400藍綠一般強調
C500正常/已完成
C600異常/需處理
C700警示
" ── 三種上色方式(CL_GUI_ALV_GRID / REUSE_..._LVC 都適用)─────────
TYPES: BEGIN OF ty_out,
         vbeln    TYPE vbak-vbeln,
         zmeng    TYPE vbap-zmeng,
         rowcolor TYPE char4,             " ① 整列顏色
         cellcolor TYPE lvc_t_scol,       " ② 單格顏色(內表)
         celltab   TYPE lvc_t_styl,       " ③ 單格可否編輯
       END OF ty_out.

DATA gt_out TYPE STANDARD TABLE OF ty_out WITH EMPTY KEY.

LOOP AT gt_out ASSIGNING FIELD-SYMBOL(<ls>).

  " ① 整列上色:layout-info_fname = 'ROWCOLOR'
  <ls>-rowcolor = COND #( WHEN <ls>-zmeng > 1000 THEN 'C600'    " 紅
                          WHEN <ls>-zmeng >  100 THEN 'C300'    " 黃
                          ELSE                        space ).

  " ② 單格上色:layout-ctab_fname = 'CELLCOLOR'
  <ls>-cellcolor = VALUE #(
    ( fname = 'ZMENG'
      color-col = COND #( WHEN <ls>-zmeng > 1000 THEN col_negative
                          ELSE                        col_positive )
      color-int = 0        " 0 = 淡色底,1 = 強烈底色
      color-inv = 0 ) ).   " 0 = 底色,1 = 只有文字上色

  " ③ 單格可否編輯:layout-stylefname = 'CELLTAB'
  "    整欄 edit=X 之後,用 celltab 針對個別儲存格「開特例/關特例」
  <ls>-celltab = VALUE #(
    ( fieldname = 'ZMENG'
      style = COND #( WHEN <ls>-vbeln IS INITIAL
                      THEN cl_gui_alv_grid=>mc_style_disabled
                      ELSE cl_gui_alv_grid=>mc_style_enabled ) ) ).
ENDLOOP.

" 顏色常數:col_heading / col_normal / col_total / col_key
"           col_positive / col_negative / col_group / col_background
SALV 也能上色,但介面不同:
整欄顏色 → CAST cl_salv_column_table( ... )->set_color( VALUE #( col = 6 int = 0 inv = 0 ) )
單格顏色 → 內表加一個 lvc_t_scol 欄位,再 lo_cols->set_color_column( 'CELLCOLOR' )
SALV 不支援編輯——要能改資料就得改用 CL_GUI_ALV_GRID

7. 排序、小計與合計

ALV 的小計是「排序」帶出來的:先用 IT_SORT 指定排序欄位,再在該排序項上打開 SUBTOT,同時要有欄位設 DO_SUM,小計才會出現。

" ── 傳統 ALV 的排序+小計(講義 Z202202_25 / BALVHD01_GROUP)───────
DATA(lt_sort) = VALUE slis_t_sortinfo_alv(
  ( spos = 1 fieldname = 'KUNNR' up = abap_true
    subtot = abap_true            " 依客戶小計
    group  = '*' )                " '*' = 換頁,'UL' = 加底線
  ( spos = 2 fieldname = 'ERDAT' down = abap_true ) ).

" LVC 版(CL_GUI_ALV_GRID)
DATA(lt_sort_lvc) = VALUE lvc_t_sort(
  ( spos = 1 fieldname = 'KUNNR' up = abap_true subtot = abap_true )
  ( spos = 2 fieldname = 'ERDAT' down = abap_true ) ).

" ── SALV 的排序+小計+合計 ──────────────────────────────────
DATA(lo_sorts) = lo_alv->get_sorts( ).
lo_sorts->add_sort( columnname = 'KUNNR'
                    sequence   = if_salv_c_sort=>sort_up
                    subtotal   = abap_true
                    group      = if_salv_c_sort=>group_none ).

DATA(lo_aggr) = lo_alv->get_aggregations( ).
lo_aggr->add_aggregation( columnname  = 'ZMENG'
                          aggregation = if_salv_c_aggregation=>total ).
" 也可以 =>average / =>maximum / =>minimum

" 小計文字
lo_alv->get_display_settings( )->set_list_header( '客戶訂單彙總' ).

另一種常被搞混的是自己在 LOOP 裡算小計AT END OF)。兩者差別如下:

ALV 自動小計(IT_SORT + DO_SUM)AT END OF 自己算
誰算ALV 控制項在前端算你在程式裡算,多插一列進內表
使用者拖欄位重排小計自動跟著重算不會重算,數字會錯
匯出 Excel會帶小計列會帶(因為它就是資料列)
建議優先使用只有要特殊格式(例如小計列要不同文字)才用
" ── AT END OF 的正確用法:使用前必須先 SORT,而且只看「該欄與其左邊所有欄」──
SORT lt_out BY kunnr matnr.

DATA lv_sum TYPE vbap-zmeng.

LOOP AT lt_out INTO DATA(ls_out).
  lv_sum = lv_sum + ls_out-zmeng.

  AT END OF kunnr.          " 注意:判斷的是 kunnr 及其「左側全部欄位」
    WRITE: / |客戶 { ls_out-kunnr } 小計:{ lv_sum NUMBER = USER }|.
    CLEAR lv_sum.
  ENDAT.
ENDLOOP.
AT END OF 的三個陷阱:
1. 一定要先 SORT,否則群組會斷斷續續。
2. AT ... ENDAT 區塊內,除了控制欄位以外的其他欄位都會被填成 *(星號),所以要在進入 AT 之前先把要用的值存起來。
3. 講義 ZEKET_43 註記「DO_SUM 做不出來,可能跟 JOIN 有關」——真正原因通常是:數量欄沒有綁單位欄(qfieldname/cfieldname 沒設),或該欄的 datatype 被手動指定成 CHAR。金額欄要綁 CFIELDNAME,數量欄要綁 QFIELDNAME,否則 ALV 不知道能不能加總。

8. Layout 變式(DISVARIANT):讓使用者自己排版面

使用者最常抱怨的是「欄位太多、順序不對」。正解不是改程式,而是把版面存成變式。傳統做法要自己接 4 支 FM(講義 BALVSD01 / BALVSD11 就是完整示範):

FM時機用途
REUSE_ALV_VARIANT_DEFAULT_GETINITIALIZATION取得預設變式,填進選擇畫面的 P_VARI
REUSE_ALV_VARIANT_F4AT SELECTION-SCREEN ON VALUE-REQUEST提供 F4 選變式
REUSE_ALV_VARIANT_EXISTENCEAT SELECTION-SCREEN檢查使用者輸入的變式存不存在
REUSE_ALV_LIST/GRID_DISPLAY 的 IS_VARIANT / I_SAVE顯示時I_SAVE:' '不可存、'U' 只能存個人、'X' 只能存共用、'A' 兩種都可
*&--------------------------------------------------------------------*
*& Layout 變式完整套路(講義 BALVSD01 / BALVSD11 的現代化版本)
*&--------------------------------------------------------------------*
DATA: gs_variant  TYPE disvariant,
      gs_variant_x TYPE disvariant,
      gv_exit     TYPE char1.
CONSTANTS gc_save TYPE char1 VALUE 'A'.   " A = 個人 + 共用皆可儲存

SELECTION-SCREEN BEGIN OF BLOCK b_var WITH FRAME TITLE TEXT-064.
PARAMETERS p_vari TYPE disvariant-variant.
SELECTION-SCREEN END OF BLOCK b_var.

INITIALIZATION.
  gs_variant-report = sy-repid.
  gs_variant_x      = gs_variant.
  CALL FUNCTION 'REUSE_ALV_VARIANT_DEFAULT_GET'
    EXPORTING  i_save     = gc_save
    CHANGING   cs_variant = gs_variant_x
    EXCEPTIONS not_found  = 2.
  IF sy-subrc = 0.
    p_vari = gs_variant_x-variant.
  ENDIF.

AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_vari.
  CALL FUNCTION 'REUSE_ALV_VARIANT_F4'
    EXPORTING  is_variant = gs_variant
               i_save     = gc_save
    IMPORTING  e_exit     = gv_exit
               es_variant = gs_variant_x
    EXCEPTIONS not_found  = 2.
  IF sy-subrc = 2.
    MESSAGE ID sy-msgid TYPE 'S' NUMBER sy-msgno
       WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
  ELSEIF gv_exit IS INITIAL.
    p_vari = gs_variant_x-variant.
  ENDIF.

AT SELECTION-SCREEN.
  IF p_vari IS NOT INITIAL.
    gs_variant_x         = gs_variant.
    gs_variant_x-variant = p_vari.
    CALL FUNCTION 'REUSE_ALV_VARIANT_EXISTENCE'
      EXPORTING i_save     = gc_save
      CHANGING  cs_variant = gs_variant_x.
    gs_variant = gs_variant_x.
  ELSE.
    CLEAR gs_variant.
    gs_variant-report = sy-repid.
  ENDIF.
SALV 只要三行就搞定同一件事(而且 F4、儲存、預設全部自動):
DATA(lo_layout) = lo_alv->get_layout( ).
lo_layout->set_key( VALUE #( report = sy-repid handle = '0001' ) ).
lo_layout->set_save_restriction( cl_salv_layout=>restrict_none ).
其中 handle 是同一支程式有多個 ALV 時用來區分的代號;restrict_none / restrict_user_dependant / restrict_user_independant 對應舊的 A / U / X。

9. ALV 事件全覽:傳統 17 個事件與 OO 事件

傳統 ALV 的事件是「填一張 SLIS_T_EVENT 表,指定事件名稱對應到哪個 FORM」。講義列出全部 17 個事件名稱,其中標黃色的四個一定要會

事件名稱觸發時機重要度
TOP_OF_PAGE每頁開始時執行(畫抬頭、放 LOGO)★必會
USER_COMMAND使用者按下功能鍵/按鈕/雙擊★必會
PF_STATUS_SET設定 GUI Status(自訂工具列)★必會
END_OF_PAGE每頁結束時執行(頁尾)★必會
CALLER_EXITALV 顯示前最後一個掛勾進階
TOP_OF_LIST / END_OF_LIST整份清單的開頭/結尾(只出現一次)常用
TOP_OF_COVERPAGE / END_OF_COVERPAGE封面頁開頭/結尾少用
FOREIGN_TOP_OF_PAGE / FOREIGN_END_OF_PAGE外部呼叫時的頁首頁尾少用
LIST_MODIFY清單輸出後可再修改內容少用
AFTER_LINE_OUTPUT / BEFORE_LINE_OUTPUT每一行輸出前後少用
SUBTOTAL_TEXT自訂小計列的文字常用
GROUPLEVEL_CHANGE群組層級改變時少用
REPREP_SEL_MODIFYreport/report 介面的選擇條件修改少用

這些事件常搭配兩個古典的清單事件:AT LINE-SELECTION(在 LIST 中雙擊)與 AT USER-COMMAND(按下功能表的某鍵)。

" ── 傳統事件表建法(講義 Z202202_25 的 step5)─────────────────────
DATA(lt_event) = VALUE slis_t_event(
  ( name = 'USER_COMMAND'  form = 'FORM_USER_COMMAND' )   " 用戶回應
  ( name = 'TOP_OF_PAGE'   form = 'FORM_TOP_OF_PAGE' )    " 顯示標題欄
  ( name = 'PF_STATUS_SET' form = 'FORM_PF_STATUS_SET' )  " GRID 狀態欄
  ( name = 'END_OF_PAGE'   form = 'FORM_END_OF_PAGE' ) ).

" 也可以先向系統要「標準事件表」再改(講義 BALVSD01 的 E03_EVENTTAB_BUILD)
DATA lt_std_event TYPE slis_t_event.
CALL FUNCTION 'REUSE_ALV_EVENTS_GET'
  EXPORTING i_list_type = 0        " 0=簡單清單 1=階層 2=區塊 3=順序
  IMPORTING et_events   = lt_std_event.

READ TABLE lt_std_event INTO DATA(ls_ev)
     WITH KEY name = slis_ev_top_of_page.
IF sy-subrc = 0.
  ls_ev-form = 'TOP_OF_PAGE'.
  MODIFY lt_std_event FROM ls_ev INDEX sy-tabix.
ENDIF.

" ── 三個 callback FORM(名稱要跟事件表一致,或直接用 I_CALLBACK_xxx 傳)──
FORM form_pf_status_set USING rt_extab TYPE slis_t_extab.
  " RT_EXTAB 是「目前不能啟用」的 ALV 功能碼清單,要原封不動傳給 SET PF-STATUS
  SET PF-STATUS 'STANDARD' EXCLUDING rt_extab.
ENDFORM.

FORM form_top_of_page.
  DATA(lt_comment) = VALUE slis_t_listheader(
    ( typ = 'H' info = 'VA01 客戶訂單' )              " H = Header 大標題
    ( typ = 'S' key = '選擇條件' info = '僅顯示部份欄位' ) " S = Selection
    ( typ = 'A' info = |列印者 { sy-uname }| ) ).      " A = Action

  CALL FUNCTION 'REUSE_ALV_COMMENTARY_WRITE'
    EXPORTING
      it_list_commentary = lt_comment
      i_logo             = 'ZNCU'.     " OAER 上傳的 LOGO 物件名稱
ENDFORM.

FORM form_user_command USING r_ucomm     LIKE sy-ucomm
                             rs_selfield TYPE slis_selfield.
  CASE r_ucomm.
    WHEN '&IC1'.                       " 雙擊(Interactive Command 1)
      CASE rs_selfield-fieldname.
        WHEN 'VBELN'.
          SET PARAMETER ID 'AUN' FIELD rs_selfield-value.
          CALL TRANSACTION 'VA03' AND SKIP FIRST SCREEN.
        WHEN 'MATNR'.
          SET PARAMETER ID 'MAT' FIELD rs_selfield-value.
          CALL TRANSACTION 'MM03' AND SKIP FIRST SCREEN.
      ENDCASE.
    WHEN 'ZCAS'.  MESSAGE '你按下了 CASPER 按鈕' TYPE 'I'.
    WHEN 'ZADD'.  MESSAGE '你按下了 新增2 按鈕' TYPE 'I'.
    WHEN 'EXIT'.  LEAVE PROGRAM.
    WHEN OTHERS.
  ENDCASE.
  rs_selfield-refresh = abap_true.     " 要求 ALV 重新整理畫面
ENDFORM.
功能碼 '&IC1' 就是雙擊;'&ETA' 是彈出明細視窗(layout-f2code 預設用它)。rs_selfield 裡最好用的三個欄位:tabindex(第幾列)、fieldname(哪一欄)、value(那一格的值)。設 rs_selfield-refresh = 'X' 才會刷新畫面。

9-1. OO ALV 的事件對照

要做的事CL_SALV_TABLECL_GUI_ALV_GRID
自訂按鈕被按ADDED_FUNCTIONUSER_COMMAND(+ TOOLBAR 加按鈕)
雙擊某一格DOUBLE_CLICKDOUBLE_CLICK
單擊熱點LINK_CLICKHOTSPOT_CLICK
編輯後回寫內表不支援(SALV 唯讀)DATA_CHANGED / DATA_CHANGED_FINISHED
自畫工具列set_screen_status( )TOOLBAR + MENU_BUTTON
報表抬頭set_top_of_list( )TOP_OF_PAGE(+ CL_DD_DOCUMENT)
右鍵選單不支援ONF4 / CONTEXT_MENU_REQUEST

10. CL_GUI_ALV_GRID 完整範例(可編輯 + 事件 + 回寫資料庫)

當你需要「使用者在 ALV 上直接改資料 → 按存檔 → 寫回資料表」時,SALV 就不夠用了,必須用 CL_GUI_ALV_GRID。這需要一個 Dialog Screen(SE80 建 Screen 0100 + 一個 Custom Control,名稱例如 CC_ALV)與一個 GUI Status。以下是完整可跑的骨架,功能對應講義第 26 支程式與 ZEKET_43(EKKO ⋈ EKPO ⋈ EKET 三表 JOIN、改交期、存檔回寫):

*&--------------------------------------------------------------------*
*& Report ZGRID_EKET_EDIT
*& CL_GUI_ALV_GRID 完整範例:三表 JOIN、紅綠燈、可編輯、存檔回寫 EKET
*& 前置:SE80 建 Screen 0100(含 Custom Control 名稱 CC_ALV)
*&       SE41 建 GUI Status ZSTATUS(含 ZSAV 存檔、BACK/EXIT/CANC)
*&--------------------------------------------------------------------*
REPORT zgrid_eket_edit.

TYPES: BEGIN OF ty_out,
         light   TYPE char4,        " 紅綠燈(ICON)
         ebeln   TYPE eket-ebeln,   " 採購編號
         ebelp   TYPE eket-ebelp,   " 採購項次
         etenr   TYPE eket-etenr,   " 交貨排程行
         lifnr   TYPE ekko-lifnr,   " 廠商編號
         name1   TYPE lfa1-name1,   " 廠商名稱
         bedat   TYPE ekko-bedat,   " 採購日期
         matnr   TYPE ekpo-matnr,   " 物料編號
         maktx   TYPE makt-maktx,   " 物料名稱
         menge   TYPE ekpo-menge,   " 採購數量
         meins   TYPE ekpo-meins,   " 採購單位
         eindt   TYPE eket-eindt,   " 廠商交期(可編輯)
         slfdt   TYPE eket-slfdt,   " 統計交期
         celltab TYPE lvc_t_styl,   " 單格可編輯控制
       END OF ty_out.

DATA: gt_out    TYPE STANDARD TABLE OF ty_out WITH EMPTY KEY,
      go_cont   TYPE REF TO cl_gui_custom_container,
      go_grid   TYPE REF TO cl_gui_alv_grid,
      gv_ok     TYPE sy-ucomm.

SELECT-OPTIONS: s_matnr FOR  gt_out-matnr,     " 用內表欄位當參照即可
                s_bedat FOR  gt_out-bedat.

*--------------------------------------------------------------------*
* 事件處理類別
*--------------------------------------------------------------------*
CLASS lcl_evt DEFINITION.
  PUBLIC SECTION.
    METHODS on_toolbar      FOR EVENT toolbar       OF cl_gui_alv_grid
      IMPORTING e_object e_interactive.
    METHODS on_user_command FOR EVENT user_command  OF cl_gui_alv_grid
      IMPORTING e_ucomm.
    METHODS on_hotspot      FOR EVENT hotspot_click OF cl_gui_alv_grid
      IMPORTING e_row_id e_column_id.
    METHODS on_double_click FOR EVENT double_click  OF cl_gui_alv_grid
      IMPORTING e_row e_column.
    METHODS on_data_changed FOR EVENT data_changed  OF cl_gui_alv_grid
      IMPORTING er_data_changed e_onf4.
ENDCLASS.

CLASS lcl_evt IMPLEMENTATION.

  " 在 ALV 工具列自己加按鈕
  METHOD on_toolbar.
    APPEND VALUE #( butn_type = 3 ) TO e_object->mt_toolbar.   " 分隔線
    APPEND VALUE #( function  = 'ZSAV'
                    icon      = icon_system_save
                    quickinfo = '存檔回寫 EKET'
                    text      = '存檔' ) TO e_object->mt_toolbar.
    APPEND VALUE #( function  = 'ZXLS'
                    icon      = icon_xls
                    quickinfo = '匯出 Excel'
                    text      = '匯出' ) TO e_object->mt_toolbar.
  ENDMETHOD.

  " 自訂按鈕被按
  METHOD on_user_command.
    CASE e_ucomm.
      WHEN 'ZSAV'.
        " 先強迫 ALV 把畫面上編輯中的值送回內表,否則會存到舊值
        go_grid->check_changed_data( ).
        PERFORM save_to_db.
      WHEN 'ZXLS'.
        PERFORM download_xlsx.
    ENDCASE.
  ENDMETHOD.

  " 熱點單擊 → 跳 ME23N
  METHOD on_hotspot.
    READ TABLE gt_out INTO DATA(ls) INDEX e_row_id-index.
    CHECK sy-subrc = 0.
    IF e_column_id-fieldname = 'EBELN'.
      SET PARAMETER ID 'BES' FIELD ls-ebeln.
      CALL TRANSACTION 'ME23N' AND SKIP FIRST SCREEN.
    ENDIF.
  ENDMETHOD.

  " 雙擊 → 顯示物料
  METHOD on_double_click.
    READ TABLE gt_out INTO DATA(ls) INDEX e_row-index.
    CHECK sy-subrc = 0.
    IF e_column-fieldname = 'MATNR'.
      CALL FUNCTION 'MMPUR_MATERIAL_DISPLAY'
        EXPORTING im_matnr = ls-matnr.
    ENDIF.
  ENDMETHOD.

  " 編輯即時檢核:交期不可早於今天
  METHOD on_data_changed.
    LOOP AT er_data_changed->mt_good_cells INTO DATA(ls_cell).
      IF ls_cell-fieldname = 'EINDT' AND ls_cell-value < sy-datum.
        er_data_changed->add_protocol_entry(
          i_msgid = '00' i_msgty = 'E' i_msgno = '001'
          i_msgv1 = '交期不可早於今天'
          i_fieldname = ls_cell-fieldname
          i_row_id    = ls_cell-row_id ).
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

ENDCLASS.

DATA go_evt TYPE REF TO lcl_evt.

*--------------------------------------------------------------------*
START-OF-SELECTION.
  PERFORM get_data.
  IF gt_out IS INITIAL.
    MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.
  CALL SCREEN 0100.

*&--------------------------------------------------------------------*
FORM get_data.

  " 三表 JOIN:EKET ⋈ EKPO ⋈ EKKO,再左外聯 LFA1 / MAKT 取名稱
  SELECT FROM eket AS a
         INNER JOIN ekpo AS b ON  a~ebeln = b~ebeln
                              AND a~ebelp = b~ebelp
         INNER JOIN ekko AS c ON  a~ebeln = c~ebeln
         LEFT OUTER JOIN lfa1 AS d ON c~lifnr = d~lifnr
         LEFT OUTER JOIN makt AS e ON  b~matnr = e~matnr
                                   AND e~spras = @sy-langu
         FIELDS a~ebeln, a~ebelp, a~etenr, a~eindt, a~slfdt,
                b~matnr, b~menge, b~meins,
                c~bedat, c~lifnr, d~name1, e~maktx
         WHERE b~matnr IN @s_matnr
           AND c~bedat IN @s_bedat
         ORDER BY a~ebeln, a~ebelp, a~etenr
         INTO CORRESPONDING FIELDS OF TABLE @gt_out
         UP TO 500 ROWS.

  " 紅綠燈 + 單格可編輯控制
  LOOP AT gt_out ASSIGNING FIELD-SYMBOL(<ls>).
    <ls>-light = COND #( WHEN <ls>-eindt <  sy-datum     THEN icon_led_red
                         WHEN <ls>-eindt <= sy-datum + 7 THEN icon_led_yellow
                         ELSE                                 icon_led_green ).
    " 只有未逾期的行才允許改交期
    <ls>-celltab = VALUE #(
      ( fieldname = 'EINDT'
        style = COND #( WHEN <ls>-eindt < sy-datum
                        THEN cl_gui_alv_grid=>mc_style_disabled
                        ELSE cl_gui_alv_grid=>mc_style_enabled ) ) ).
  ENDLOOP.

ENDFORM.

*&--------------------------------------------------------------------*
MODULE status_0100 OUTPUT.
  SET PF-STATUS 'ZSTATUS'.
  SET TITLEBAR  'T100'.

  IF go_grid IS INITIAL.

    go_cont = NEW cl_gui_custom_container( container_name = 'CC_ALV' ).
    go_grid = NEW cl_gui_alv_grid( i_parent = go_cont ).

    " 註冊事件:TOOLBAR 必須在 set_table_for_first_display 之前註冊
    go_evt = NEW lcl_evt( ).
    SET HANDLER go_evt->on_toolbar      FOR go_grid.
    SET HANDLER go_evt->on_user_command FOR go_grid.
    SET HANDLER go_evt->on_hotspot      FOR go_grid.
    SET HANDLER go_evt->on_double_click FOR go_grid.
    SET HANDLER go_evt->on_data_changed FOR go_grid.

    " 編輯後每一格都立刻回報(等同舊 I_GRID_SETTINGS-EDT_CLL_CB = 'X')
    go_grid->register_edit_event(
      i_event_id = cl_gui_alv_grid=>mc_evt_modified ).

    DATA(ls_layo) = VALUE lvc_s_layo(
      zebra      = abap_true
      cwidth_opt = abap_true
      sel_mode   = 'A'
      grid_title = '採購交貨排程(可修改交期)'
      excp_fname = space           " 這裡用 ICON 欄,不用 exception
      stylefname = 'CELLTAB' ).

    DATA(lt_fcat) = VALUE lvc_t_fcat(
      ( fieldname = 'LIGHT' col_pos = 1  outputlen = 4  scrtext_m = '燈'
        icon = abap_true )
      ( fieldname = 'EBELN' col_pos = 2  outputlen = 10 scrtext_m = '採購編號'
        key = abap_true hotspot = abap_true )
      ( fieldname = 'EBELP' col_pos = 3  outputlen = 5  scrtext_m = '項次'
        key = abap_true )
      ( fieldname = 'ETENR' col_pos = 4  outputlen = 4  scrtext_m = '排程行'
        key = abap_true )
      ( fieldname = 'LIFNR' col_pos = 5  outputlen = 10 scrtext_m = '廠商編號' )
      ( fieldname = 'NAME1' col_pos = 6  outputlen = 35 scrtext_m = '廠商名稱' )
      ( fieldname = 'BEDAT' col_pos = 7  outputlen = 10 scrtext_m = '採購日期' )
      ( fieldname = 'MATNR' col_pos = 8  outputlen = 18 scrtext_m = '物料編號' )
      ( fieldname = 'MAKTX' col_pos = 9  outputlen = 40 scrtext_m = '物料名稱' )
      ( fieldname = 'MENGE' col_pos = 10 outputlen = 15 scrtext_m = '採購數量'
        qfieldname = 'MEINS' do_sum = abap_true )
      ( fieldname = 'MEINS' col_pos = 11 outputlen = 3  scrtext_m = '單位' )
      ( fieldname = 'EINDT' col_pos = 12 outputlen = 10 scrtext_m = '廠商交期'
        edit = abap_true )
      ( fieldname = 'SLFDT' col_pos = 13 outputlen = 10 scrtext_m = '統計交期' ) ).

    DATA(lt_sort) = VALUE lvc_t_sort(
      ( spos = 1 fieldname = 'LIFNR' up = abap_true subtot = abap_true )
      ( spos = 2 fieldname = 'EBELN' up = abap_true ) ).

    DATA(ls_variant) = VALUE disvariant( report = sy-repid handle = '0001' ).

    go_grid->set_table_for_first_display(
      EXPORTING is_layout        = ls_layo
                is_variant       = ls_variant
                i_save           = 'A'
      CHANGING  it_outtab        = gt_out
                it_fieldcatalog  = lt_fcat
                it_sort          = lt_sort ).

  ELSE.
    " 資料變更後刷新,並保留捲軸位置
    go_grid->refresh_table_display(
      EXPORTING is_stable = VALUE lvc_s_stbl( row = abap_true col = abap_true ) ).
  ENDIF.

ENDMODULE.

MODULE user_command_0100 INPUT.
  CASE gv_ok.
    WHEN 'BACK' OR 'EXIT' OR 'CANC'.
      go_grid->free( ).
      go_cont->free( ).
      CLEAR: go_grid, go_cont.
      LEAVE TO SCREEN 0.
  ENDCASE.
  CLEAR gv_ok.
ENDMODULE.

*&--------------------------------------------------------------------*
FORM save_to_db.

  " 依內表的 key 撈出實體表資料(FOR ALL ENTRIES 前務必檢查非空)
  IF gt_out IS INITIAL.
    MESSAGE '無資料可存' TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.

  SELECT ebeln, ebelp, etenr, eindt, slfdt, menge
    FROM eket
    FOR ALL ENTRIES IN @gt_out
    WHERE ebeln = @gt_out-ebeln
      AND ebelp = @gt_out-ebelp
      AND etenr = @gt_out-etenr
    INTO TABLE @DATA(lt_eket).

  LOOP AT lt_eket ASSIGNING FIELD-SYMBOL(<ls_eket>).
    " line_exists / 讀表:用 key 精準對應
    ASSIGN gt_out[ ebeln = <ls_eket>-ebeln
                   ebelp = <ls_eket>-ebelp
                   etenr = <ls_eket>-etenr ] TO FIELD-SYMBOL(<ls_out>).
    IF sy-subrc = 0.
      <ls_eket>-eindt = <ls_out>-eindt.
      <ls_eket>-slfdt = <ls_out>-eindt.
    ENDIF.
  ENDLOOP.

  " 標準 SAP 表建議走 BAPI(BAPI_PO_CHANGE),示範用 UPDATE 說明交易一致性
  UPDATE eket FROM TABLE @lt_eket.
  IF sy-subrc = 0.
    COMMIT WORK AND WAIT.
    MESSAGE '存檔成功!' TYPE 'S'.
  ELSE.
    ROLLBACK WORK.
    MESSAGE '存檔失敗!' TYPE 'S' DISPLAY LIKE 'E'.
  ENDIF.

ENDFORM.
直接 UPDATE 標準表是大忌。講義為了教「內表 → 實體表回寫」與 COMMIT WORK / ROLLBACK WORK 的觀念才這樣寫;正式專案改採購單一定要用 BAPI_PO_CHANGE + BAPI_TRANSACTION_COMMIT,改銷售訂單用 BAPI_SALESORDER_CHANGE。直接 UPDATE 會略過所有檢核、不寫變更文件(CDHDR/CDPOS)、不觸發後續流程,之後帳一定對不起來。
FOR ALL ENTRIES 三大陷阱:(1)驅動內表為空 → 條件被忽略,等於全表掃描;使用前一定要 IF lt_x IS NOT INITIAL。(2)結果會自動去重複(等同 DISTINCT),所以 SELECT 欄位一定要包含完整主鍵。(3)驅動內表太大時會被切成多次 DB 呼叫,效能不見得比 JOIN 好——能 JOIN 就 JOIN。
可編輯 ALV 的三個必要條件,缺一不可:
① fieldcat 該欄 edit = 'X'(或 layout edit = 'X' 整張開放)。
② 呼叫 register_edit_event( mc_evt_modified )(或舊式 I_GRID_SETTINGS-EDT_CLL_CB = 'X')。
③ 存檔前呼叫 go_grid->check_changed_data( ),把游標還停在編輯格的最後一筆值 flush 回內表。
另外,可編輯且有小數的數量/金額欄一定要指定 decimals_o(講義第 26 支程式特別註記),否則使用者輸入 1.5 會被吃成 15。

11. GUI Status 與自訂按鈕:SE41 的正確作法

講義第 25 支程式的「特色一」就是 PF_STATUS_SET 前置準備。SAP 官方說明講得很清楚:要設自訂 Status,請以函數群組 SALV 的 STANDARD 為樣板複製。步驟如下(依講義步驟整理)

  1. SE41 Menu Painter → 程式名輸入 SAPLSALV、Status 輸入 STANDARD(或 STANDARD_FULLSCREEN)。
  2. 功能表 Status → 複製,目標程式填你自己的 Z 程式、Status 名稱自取(例如 ZSTATUS)。
  3. Application Toolbar(應用工具列)新增按鈕,功能碼取 ZADDZSAVZCAS 之類的 Z 開頭代碼,填上圖示與 Quickinfo。建議放 Application Bar,不要塞進 Menu Bar——使用者根本不會去點選單。
  4. 存檔後一定要啟動(Activate)。物件變多時,可用 SE80 的物件清單一次全選啟動,或用 SE38 執行 RS_WORKING_AREA_ACTIVATE/在 SE80 對 Package 按「Activate All」。
  5. 在程式的 PF_STATUS_SET 事件裡 SET PF-STATUS 'ZSTATUS' EXCLUDING rt_extab.
RT_EXTAB 是什麼?SAP 說明原文:Parameter RT_EXTAB 內含「目前必須停用」的 ALV 專屬功能碼,你必須把這張表原封不動傳給 SET PF-STATUS ... EXCLUDING。不傳的話,畫面上會出現一些當下根本不能用的按鈕,按下去就 dump。因為這個 callback 太常用,SAP 讓你可以直接用 I_CALLBACK_PF_STATUS_SET 參數傳 FORM 名稱,不必建事件表。

SALV 的自訂按鈕作法不同——它用 set_screen_status( ),而且要先在自己的程式裡建一個 Status(同樣從 SAPLSALV 的 STANDARD 複製):

" ── SALV 掛自訂 GUI Status ─────────────────────────────────────
" set_functions 三個選項:
"   c_functions_none / c_functions_default / c_functions_all
lo_alv->set_screen_status(
  report        = sy-repid          " Status 存在哪一支程式
  pfstatus      = 'ZSALV_STATUS'    " SE41 從 SAPLSALV-STANDARD 複製而來
  set_functions = lo_alv->c_functions_all ).

" 或者不自訂 Status,只把標準功能全開(最省事)
lo_alv->get_functions( )->set_all( abap_true ).

" 也可以逐項開關
DATA(lo_func) = lo_alv->get_functions( ).
lo_func->set_default( abap_true ).
lo_func->set_export_localfile( abap_true ).   " 本機檔案
lo_func->set_export_spreadsheet( abap_true ). " 試算表
lo_func->set_print( abap_true ).
lo_func->set_graphics( abap_false ).

11-1. 報表 LOGO:OAER 上傳圖片

講義第 25 支程式「特色四」是在 TOP_OF_PAGE 放一張 LOGO。做法(依講義步驟整理)

  1. 執行 OAER,Class Name 填 PICTURES、Class Type 填 OT、Object Key 填你自取的名稱(講義用 ZNCUZCERPS43)。
  2. 進入主控畫面後選「新增」,在視窗下方的 Standard Doc. Type 一定要選 SCREEN(若要列印用則選 PRINT)。
  3. 選擇本機圖檔上傳。建立成功後,重新進入該物件點 Attachment 就能看到 LOGO。
  4. 程式中 REUSE_ALV_COMMENTARY_WRITEI_LOGO 填該 Object Key。
若是 SmartForms/SAPscript 要用的圖檔,改走 SE78(Form Graphics)上傳,不是 OAER。兩者的圖庫是分開的。

12. Drilldown:從報表跳到標準交易

ALV 報表的價值一半來自「雙擊能跳過去看單據」。SAP 的作法是先 SET PARAMETER ID 把值塞進使用者的 SPA/GPA 記憶區,再 CALL TRANSACTION ... AND SKIP FIRST SCREEN;或直接呼叫該模組提供的顯示 FM。

要跳到Parameter ID作法
採購單 ME23NBESSET PARAMETER + CALL TRANSACTION,或 FM ME_DISPLAY_PURCHASE_DOCUMENT
銷售訂單 VA03AUNSET PARAMETER + CALL TRANSACTION
交貨單 VL03NVLSET PARAMETER + CALL TRANSACTION
物料 MM03MAT(+WRK 工廠、LIF 廠商、MXX 視圖SET PARAMETER + CALL TRANSACTION,或 FM MMPUR_MATERIAL_DISPLAY
會計傳票 FB03BLN + BUK + GJRSET PARAMETER + CALL TRANSACTION
物料憑證 MIGOMBN + MJA + BUKFM MIGO_DIALOG
供應商FM MMPUR_VENDOR_DISPLAY(im_lifnr、im_ekorg)
採購資訊記錄FM MMPUR_INFO_RECORD_DISPLAY(im_infnr、im_ekorg、im_werks)
維護視圖(如工廠 V_T001W)FM VIEW_MAINTENANCE_SINGLE_ENTRY(action='SHOW')
程式碼FM EDITOR_PROGRAM(display='X')
函數群組FM RS_FUNCTION_POOL_CHANGE(aktion='SHOW')
*&--------------------------------------------------------------------*
*& Report ZDRILLDOWN  ── 雙擊跳 ME23N(講義原例,改為 LVC + 現代語法)
*&--------------------------------------------------------------------*
REPORT zdrilldown.

DATA gt_ekko TYPE STANDARD TABLE OF ekko WITH EMPTY KEY.

START-OF-SELECTION.

  SELECT ebeln, bukrs, bsart, lifnr, bedat, waers
    FROM ekko
    UP TO 10 ROWS
    INTO CORRESPONDING FIELDS OF TABLE @gt_ekko.

  DATA(lt_fcat) = VALUE lvc_t_fcat(
    ( fieldname = 'EBELN' col_pos = 1 outputlen = 10 scrtext_m = '採購單號'
      key = abap_true hotspot = abap_true )
    ( fieldname = 'LIFNR' col_pos = 2 outputlen = 10 scrtext_m = '廠商' )
    ( fieldname = 'BEDAT' col_pos = 3 outputlen = 10 scrtext_m = '採購日期' )
    ( fieldname = 'WAERS' col_pos = 4 outputlen = 5  scrtext_m = '幣別' ) ).

  DATA(ls_layo) = VALUE lvc_s_layo( zebra      = abap_true
                                    sel_mode   = 'A'
                                    cwidth_opt = abap_true ).

  CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY_LVC'
    EXPORTING
      i_callback_program      = sy-repid
      i_callback_user_command = 'FRM_USER_COMMAND'
      is_layout_lvc           = ls_layo
      it_fieldcat_lvc         = lt_fcat
    TABLES
      t_outtab                = gt_ekko
    EXCEPTIONS
      program_error           = 1
      OTHERS                  = 2.

*&--------------------------------------------------------------------*
FORM frm_user_command USING i_ucomm    TYPE sy-ucomm
                            is_selfield TYPE slis_selfield.
  CASE i_ucomm.
    WHEN '&IC1'.                     " 雙擊 / 熱點
      CASE is_selfield-fieldname.
        WHEN 'EBELN'.
          " 作法一:Parameter ID + CALL TRANSACTION
          SET PARAMETER ID 'BES' FIELD is_selfield-value.
          CALL TRANSACTION 'ME23N' AND SKIP FIRST SCREEN.
          " 作法二(更安全,會做權限檢查):
          " CALL FUNCTION 'ME_DISPLAY_PURCHASE_DOCUMENT'
          "   EXPORTING i_ebeln = CONV ekko-ebeln( is_selfield-value )
          "             i_enjoy = abap_true
          "   EXCEPTIONS not_found = 1 no_authority = 2
          "              invalid_call = 3 OTHERS = 4.
      ENDCASE.
  ENDCASE.
ENDFORM.
MM03 要跳到哪一個視圖?由 Parameter ID MXX 決定,對應表 T132(同時參考 MOFF):
A 工作計畫|B 會計|C 分類|D 物料需求規劃|E 採購|F 生產資源/工具|G 成本核算|K 基本資料|L 儲存|P 預測|Q 品質管制|S 倉庫管理|V 銷售|X 工廠庫存|Z 儲存位置庫存。
例:SET PARAMETER ID 'MXX' FIELD 'E'. 就會直接開採購視圖。
各模組的顯示 FM 多半集中在幾個函數群組:CK50EDI5MEEXT 等。
CALL TRANSACTION 之後使用者按上一頁會回到你的報表,但不會自動刷新資料。若跳過去有改資料,回來要重新 SELECT 並 refresh_table_display( )。另外 CALL TRANSACTION 不做權限檢查(WITH AUTHORITY-CHECK 才會做),有敏感資料的報表請加上 AUTHORITY-CHECK OBJECT ...

13. 階層式 ALV 與 SAP 標準示範程式庫

兩層(Header–Item)的清單用 REUSE_ALV_HIERSEQ_LIST_DISPLAY:兩張內表、一張 SLIS_KEYINFO_ALV 說明兩表怎麼對應。講義以 SCARR(航空公司)+ SPFLI(航班)示範。

" ── 階層式 ALV(BALVHD01 / BALVHD01_GROUP 的核心)─────────────────
" KEYINFO:header01..05 對 item01..05,說明兩表的關聯欄位
DATA(ls_keyinfo) = VALUE slis_keyinfo_alv(
  header01 = 'CARRID'  item01 = 'CARRID'     " 表頭鍵 = 項目鍵
  header02 = space     item02 = 'CONNID' ).  " 項目自己的第二層鍵

DATA(ls_layout) = VALUE slis_layout_alv(
  header_text       = '航空公司'
  item_text         = '航班'
  default_item      = abap_true        " 預設展開項目
  group_change_edit = abap_true
  lights_fieldname  = 'LIGHTS'
  lights_tabname    = 'GT_SPFLI' ).    " 燈號長在哪張表

CALL FUNCTION 'REUSE_ALV_HIERSEQ_LIST_DISPLAY'
  EXPORTING
    i_callback_program      = sy-repid
    i_structure_name_header = 'SCARR'
    i_structure_name_item   = 'SFLIGHT'
    i_tabname_header        = '1'
    i_tabname_item          = '2'
    is_keyinfo              = ls_keyinfo
    is_layout               = ls_layout
    it_fieldcat             = lt_fieldcat
    it_sort                 = lt_sort
    it_events               = lt_events
    i_save                  = 'A'
  TABLES
    t_outtab_header         = gt_scarr
    t_outtab_item           = gt_sflight.
階層式 ALV 的 fieldcat 一定要填 TABNAME(哪張表的欄位),否則欄位會亂跑或不顯示。排序 SLIS_SORTINFO_ALV 也要填 TABNAME
S/4HANA 上要做樹狀報表,建議改用 CL_GUI_ALV_TREE,或乾脆用 CDS 的 parent-child association 交給 Fiori 呈現。

SAP 內建一整套 ALV 示範程式,都在 SE38 直接執行、SE80 直接讀原始碼,是最好的自學教材:

程式示範內容用到的關鍵 FM
BALVSD01飛機模型 Simple list:變式、事件、抬頭、群組、紅綠燈、勾選欄,全部參數化成 CheckBoxREUSE_ALV_VARIANT_DEFAULT_GET / _F4 / _EXISTENCE、REUSE_ALV_EVENTS_GET、REUSE_ALV_COMMENTARY_WRITE、REUSE_ALV_LIST_DISPLAY
BALVSD02最簡版本:SELECT + I_STRUCTURE_NAME 就出報表REUSE_ALV_LIST_DISPLAY
BALVSD02_GRID同上,但全螢幕 GRID 模式REUSE_ALV_GRID_DISPLAY
BALVSD02_SAVE加上 I_SAVE = 'A' 的變式儲存REUSE_ALV_LIST_DISPLAY
BALVSD03自建 fieldcat(欄位名寫成 SCARR-CARRID 這種巢狀形式)REUSE_ALV_LIST_DISPLAY
BALVSD04Merged list:多表 JOIN 後輸出,示範自訂欄位 SEATSFREE(DDIC 沒有的欄位)REUSE_ALV_LIST_DISPLAY
BALVSD06simple version + save,示範 SELECT-OPTIONS 過濾REUSE_ALV_LIST_DISPLAY
BALVSD11互動+Layout:自訂 GUI Status STANDA02、USER_COMMAND 用 SUBMIT 跳程式REUSE_ALV_FIELDCATALOG_MERGE、REUSE_ALV_LIST_DISPLAY
BALVHD01階層式(Header–Item)完整版REUSE_ALV_HIERSEQ_LIST_DISPLAY 等 7 支
BALVHD01_GROUP階層式 + 群組小計(GT_SORT 的 SUBTOT / GROUP)REUSE_ALV_EVENTS_GET、REUSE_ALV_HIERSEQ_LIST_DISPLAY
BALV1F01超級複雜版——老師的原話是「等著您進入專業程式師的領域」
SLIS_DUMMYBALVSD11 用 SUBMIT 呼叫的示範被叫程式
善用輔助說明:ABAPDOCU(ABAP 說明文件與可執行範例)、ABAPHELP(關鍵字說明)。飛行資料(SFLIGHT/SCARR/SPFLI/SBOOK)若是空的,執行程式 SAPBC_DATA_GENERATORBC_DATA_GEN 可以重新產生。

14. 匯出:Excel、XLSX 與 PDF

ALV 標準工具列本身就有「匯出 → 試算表 / 本機檔案」。真正要自己寫程式的情境有三種:

14-1. SALV 內建匯出(最推薦)

" ── SALV 直接把目前畫面轉成 XLSX(S/4HANA 建議作法)─────────────
" cl_salv_export_xml_xlsx 需要 SALV 已 display 過或已有 model
DATA(lv_xlsx) = lo_alv->to_xml( xml_type = if_salv_bs_xml=>c_type_xlsx ).
" lv_xlsx 是 XSTRING,可直接下載或當附件寄出

" 下載到本機
DATA lt_bin TYPE STANDARD TABLE OF x255.
DATA lv_len TYPE i.

CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
  EXPORTING buffer        = lv_xlsx
  IMPORTING output_length = lv_len
  TABLES    binary_tab    = lt_bin.

cl_gui_frontend_services=>gui_download(
  EXPORTING
    bin_filesize = lv_len
    filename     = 'C:\temp\alv_export.xlsx'
    filetype     = 'BIN'                  " XLSX 一定要用 BIN
  CHANGING
    data_tab     = lt_bin
  EXCEPTIONS
    OTHERS       = 1 ).
IF sy-subrc <> 0.
  MESSAGE '下載失敗' TYPE 'E'.
ENDIF.

14-2. 純文字 / CSV 下載(GUI_DOWNLOAD 的正確用法)

" ── GUI_DOWNLOAD 下載 CSV:中文一定要注意編碼 ──────────────────
DATA(lt_csv) = VALUE string_table(
  ( |訂單編號,訂單日期,客戶,數量| ) ).

LOOP AT gt_out INTO DATA(ls_out).
  APPEND |{ ls_out-vbeln },{ ls_out-erdat DATE = ISO },| &&
         |{ ls_out-kunnr },{ ls_out-zmeng NUMBER = RAW }| TO lt_csv.
ENDLOOP.

" 讓使用者選存檔位置
DATA lv_path TYPE string.
DATA lv_file TYPE string.
DATA lv_full TYPE string.
DATA lv_act  TYPE i.

cl_gui_frontend_services=>file_save_dialog(
  EXPORTING
    default_extension = 'csv'
    default_file_name = 'sales_order'
    file_filter       = |CSV (*.csv)\|*.csv\|| " 注意管線符號要逸出
  CHANGING
    filename          = lv_file
    path              = lv_path
    fullpath          = lv_full
    user_action       = lv_act ).

CHECK lv_act = cl_gui_frontend_services=>action_ok.

cl_gui_frontend_services=>gui_download(
  EXPORTING
    filename                = lv_full
    filetype                = 'ASC'
    write_field_separator   = abap_false
    codepage                = '4110'        " UTF-8
    write_bom               = abap_true     " 沒有 BOM,Excel 開中文會變亂碼
  CHANGING
    data_tab                = lt_csv
  EXCEPTIONS
    OTHERS                  = 1 ).
中文亂碼的標準解法codepage = '4110'(UTF-8)+ write_bom = abap_true。少了 BOM,Excel 在中文 Windows 上會用 Big5 解讀 UTF-8,全部變亂碼。若要相容舊系統,改 codepage = '8300'(Big5)。
另外 GUI_DOWNLOAD 屬於前端服務,背景作業(SM36/SM37)不能用,會 dump。背景排程要輸出檔案請改 OPEN DATASET ... FOR OUTPUT IN TEXT MODE ENCODING UTF-8 寫到 AL11 應用伺服器目錄,再由 FTP/介面取走。

14-3. 產生真正的 Excel 檔(XLSX)

  • SALV to_xml:如上,最省事,格式與畫面一致,S/4HANA 首選。
  • abap2xlsx(開源):需要多工作表、公式、格式化、圖表時使用。核心是 zcl_excelzcl_excel_worksheetzcl_excel_writer_2007,寫法概念是「建 workbook → 加 worksheet → set_cell( ip_row / ip_column / ip_value ) → writer 產出 XSTRING → 下載」。屬非 SAP 標準物件,導入前要確認公司政策。
  • OLE 自動化CREATE OBJECT ole 'EXCEL.APPLICATION'):老專案常見,速度極慢且需要用戶端裝 Excel,新程式不要用
  • XML Spreadsheet 2003 / MHTML:舊式「假 Excel」,副檔名 .xls 但內容是 XML,開啟會跳警告,也不建議。

14-4. 匯出 PDF

" ── ALV 轉 PDF:先送到 Spool,再把 Spool 轉 PDF ──────────────────
" 步驟一:用 NEW-PAGE PRINT ON 把 ALV 輸出導到 spool
NEW-PAGE PRINT ON
  DESTINATION 'LOCL'
  IMMEDIATELY ' '
  KEEP IN SPOOL 'X'
  NEW LIST IDENTIFICATION 'X'
  NO DIALOG.

lo_alv->display( ).

NEW-PAGE PRINT OFF.

DATA(lv_spool) = sy-spono.

" 步驟二:Spool → PDF
CALL FUNCTION 'CONVERT_ABAPSPOOLJOB_2_PDF'
  EXPORTING
    src_spoolid   = lv_spool
    no_dialog     = abap_true
    dst_device    = 'LOCL'
  IMPORTING
    pdf_bytecount = DATA(lv_bytes)
  TABLES
    pdf           = DATA(lt_pdf)
  EXCEPTIONS
    err_no_abap_spooljob = 1
    OTHERS               = 12.
IF sy-subrc <> 0.
  MESSAGE '轉 PDF 失敗' TYPE 'E'.
ENDIF.

" lt_pdf 可再 gui_download(filetype = 'BIN')或當 Email 附件送出

15. S/4HANA:什麼時候該離開 ALV,改用 CDS + Fiori

ALV 依然是 SAP GUI 報表的標準,但在 S/4HANA 上有些情境明顯應該換路線:

情境建議作法
只給 IT/Key User 在 GUI 上跑,欄位固定CL_SALV_TABLE,最快
需要在 ALV 上編輯並回寫CL_GUI_ALV_GRID(或改做 Fiori RAP 應用)
要給業務單位/主管在瀏覽器或手機看CDS View + Annotation + Fiori List Report
資料量大、需要 HANA 下推運算CDS View(含 aggregation、association),不要在 ABAP 端 LOOP 加總
要對外提供資料(Excel 抓、Power BI 抓)CDS View 加 @OData.publish: true 曝露成 OData
需要跨模組彙總(銷售+庫存+成本)建 Composite CDS View 疊在標準 I_ CDS 之上
" ── S/4HANA 觀念示範:同樣一張報表,改用 CDS View 當資料來源 ────────
" ① 先在 ADT(Eclipse)建 CDS View(此段為 CDS DDL,不是 ABAP)
"
"   @AbapCatalog.sqlViewName: 'ZVSOITEM'
"   @OData.publish: true
"   @UI.headerInfo.typeName: 'Sales Order Item'
"   define view Z_C_SalesOrderItem
"     as select from vbak as h
"     inner join      vbap as i on h.vbeln = i.vbeln
"   {
"     key h.vbeln  as SalesOrder,
"     key i.posnr  as Item,
"         h.kunnr  as Customer,
"         h.erdat  as CreatedOn,
"         i.matnr  as Material,
"         @Semantics.quantity.unitOfMeasure: 'SalesUnit'
"         i.zmeng  as Quantity,
"         i.zieme  as SalesUnit
"   }
"
" ② ABAP 端只要把 CDS 當成一張表 SELECT,欄位語意、單位、文字都自動帶
SELECT SalesOrder, Item, Customer, CreatedOn, Material, Quantity, SalesUnit
  FROM z_c_salesorderitem
  WHERE CreatedOn IN @s_erdat
  INTO TABLE @DATA(lt_out)
  UP TO 500 ROWS.

TRY.
    cl_salv_table=>factory(
      IMPORTING r_salv_table = DATA(lo_alv)
      CHANGING  t_table      = lt_out ).
    lo_alv->get_functions( )->set_all( abap_true ).
    lo_alv->display( ).
  CATCH cx_salv_msg INTO DATA(lx).
    MESSAGE lx->get_text( ) TYPE 'E'.
ENDTRY.
S/4HANA 常見的資料來源變動,寫 ALV 前一定要確認:
• 物料憑證:MKPF/MSEG → MATDOC(MSEG 仍以相容視圖存在,但效能差)。
• 會計傳票:BKPF/BSEG/FAGLFLEXA/COEP → ACDOCA 單一總帳行項目表。
• 庫存:MARD/MARC/MCHB 的數量欄位在部分版本已改由 MATDOC_EXTRACT 聚合;MB5B 類報表要改讀 MATDOC。
• 客戶/供應商:KNA1/LFA1 → 商業夥伴 BUT000(KNA1/LFA1 仍在,但主檔維護走 BP)。
• 自建 DB View → 一律改用 CDS View。
SELECT SINGLE 沒有排序保證:若 WHERE 條件不是完整主鍵,HANA 回哪一筆是不確定的(不同時間、不同節點可能不同)。講義 ZEKET_43 的 SELECT SINGLE maktx FROM makt WHERE matnr = ... 就少了 spras,一定要補:AND spras = @sy-langu
另外,講義用 LOOP 內逐筆 SELECT SINGLE 補廠商名稱/物料名稱,這是效能殺手(N+1 查詢)。正解:一次 JOIN 進來,或先把 key 收集起來一次 SELECT 進暫存內表,再用 line_exists( ) 對應。

16. 老師強調的注意事項與坑

  1. 在 ALV 中,所有要顯示的欄位都必須在對應內表中有欄位,欄位名不能寫錯。
  2. ABAP 字串大小寫很重要。單引號包起來的字串,呼叫系統方法時一律用大寫
  3. 傳給 ALV 的欄位名稱必須大寫,否則資料不會顯示。
  4. 巨集(DEFINE)或副程式(FORM)可以大幅縮短欄位目錄的程式碼並重複使用;現代寫法更推薦 VALUE #( ) 或抽成方法。
  5. FORM 內定義的變數在離開 FORM 後就被系統釋放、記憶體回收——不要期待它跨 FORM 存活。
  6. 程式一直出錯時,建議把關鍵字都改成大寫再試(講義 Z202202_25 的註記)。
  7. 有些 SLIS 結構無法用 SE11 直接查,要用 SE80 打開 type-pool SLIS 來看。
  8. ZEKET_43 的功能演進清單(可當作自己練習的檢核表):排序、抓供應商編號、加物料編號、TOP_OF_PAGE 抬頭與圖片、日期格式化、日期可修改、PF_STATUS_SET 功能表、USER_COMMAND 存檔、回寫 EKET、單位欄位、紅綠燈、LEAVE PROGRAM 退出、DO_SUM、用副程式改寫、兩次 INNER JOIN。

練習與自我檢核

  1. (講義題)把 VBAK、VBAP、EKKO、EKPO、EKET、LFA1、MAKT 這幾張表的關聯圖畫出來,標出主鍵與外鍵。提示:VBAK-VBELN ↔ VBAP-VBELN;EKKO-EBELN ↔ EKPO-EBELN ↔ EKET-EBELN+EBELP;MAKT 要加 SPRAS。
  2. (講義題,第 88 題)把 Z202202_24 改寫成採購訂單版報表,欄位為:採購編號、採購日期、廠商編號、物料編號、採購數量、採購單位、工廠代碼、儲存位置。若能再加上 Z202202_25 的特色(TOP_OF_PAGE 抬頭、LOGO、PF_STATUS 自訂按鈕、USER_COMMAND、排序),每個特色加 2 分提示:資料來自 EKKO ⋈ EKPO;工廠 WERKS、儲位 LGORT 都在 EKPO。
  3. 把上題改用 CL_SALV_TABLE 重寫,要求:欄寬自動最佳化、依廠商小計、數量合計、雙擊採購單號跳 ME23N、可儲存 Layout 變式。
  4. 做一支「交期預警報表」:交期已過=紅燈、7 天內=黃燈、其餘=綠燈,並在燈號上加 Tooltip。提示:exception column 用 1/2/3;Tooltip 用 cl_salv_tooltip=>c_type_exception
  5. ALV 上的數量欄位設了 do_sum = 'X' 卻加總不出來,列出至少三個可能原因。答案要點:沒綁 qfieldname/cfieldname;datatype 被指定成 CHAR;欄位設了 no_sum;資料型別本身不是 QUAN/CURR/數值。
  6. 使用者反映「我在 ALV 上改了數量,按存檔卻存到舊值」,請說出三個檢查點。答案要點:fieldcat 沒設 edit;沒有 register_edit_eventEDT_CLL_CB;存檔前沒呼叫 check_changed_data( )
  7. 寫出 AT END OF 的三個使用前提,並說明為什麼 ALV 的自動小計通常比它更好。
  8. 解釋 I_SAVE 的四個值(' '、'U'、'X'、'A')分別代表什麼,以及 SALV 對應的 restrict_* 常數。
  9. 下載 CSV 給業務,對方回報中文全是亂碼。請寫出 gui_download 應該怎麼設參數,並說明為什麼背景作業不能用這個方法。
  10. 列出 ALV 17 個事件中「一定要會」的四個,並各寫一句話說明用途。
  11. 說明 RT_EXTAB 的作用,以及不把它傳給 SET PF-STATUS ... EXCLUDING 會發生什麼事。
  12. 在 S/4HANA 上,若使用者要求「這張報表要能在手機看、要能匯出 Excel、要能存我自己的篩選條件」,你會怎麼設計?答案要點:CDS View + Annotation + Fiori List Report,Excel 匯出與變式是 Fiori 內建能力。

第 2-4 章 模組化編程:FORM / FUNCTION MODULE / MACRO / INCLUDE / 類別方法

一支程式寫到幾千行、同一段邏輯複製五次,是所有 ABAPer 都會撞到的牆。這一章把 SAP 提供的五種模組化手法一次講齊:FORM(副程式)、FUNCTION MODULE(函數)、MACRO(巨集)、INCLUDE(包含程式)、CLASS/METHOD(類別方法),說明各自的參數傳遞規則、可見範圍、除錯難易度,以及在 S/4HANA 1809 之後「哪一種還能用、哪一種只能維護不能新寫」。學完之後,你能把一支上千行的報表拆成 TOP/FORM/類別三層,能自己建 Function Group 與 RFC 函數,也知道為什麼老師說「新程式一律寫方法」。

核心 TCODESE37SE80SE24SE38SE91SM59SM13SM58SMQ1SMQ2SE93SE41VA03/H
關鍵物件VBAK、VBAP、EKKO、EKPO、EKES、MARA、ACDOCA、SAPMV45A、CL_SALV_TABLE、CL_GUI_FRONTEND_SERVICES、CX_STATIC_CHECK、CX_DYNAMIC_CHECK、CX_NO_CHECK、IF_T100_DYN_MSG、ZFM_CASPER_VBAP、ZFM_GET_EBELP

1. 為什麼要模組化:五種手法與選用原則

模組化就是「把程式中一段原始碼獨立出來、封裝成一個特定功能」。講義列的四大好處:提高程式透明度、提高程式碼重用、簡化程式維護、方便程式呼叫。最典型的例子就是「取得每月最後一天」「把檔案上傳成內表」這種到處都要用的小功能。

手法可見範圍參數可否 DebugS/4HANA 建議
METHOD(本地/全域類別)本地類別=程式內;全域類別(SE24)=全系統IMPORTING/EXPORTING/CHANGING/RETURNING/RAISING,強型別可,且可單元測試首選。新程式一律寫方法
FUNCTION MODULE全系統(必須放在 Function Group 內)IMPORT/EXPORT/CHANGING/TABLES/EXCEPTIONS可(SE37 可直接測)仍在用。RFC/Update/BAPI 目前只能用 FM
FORM(副程式 Subroutine)同程式內(可用 PERFORM ... IN PROGRAM 外部呼叫)USING/CHANGING/TABLES,型別可有可無已宣告過時,只維護不新寫;ABAP Cloud 禁用
MACRO(DEFINE)同程式內(或 TRMAC 表的全域巨集)&1~&9,無型別不可 debug不建議。只在極短的重複賦值場合殘留
INCLUDE編譯期文字插入,不是真正的模組無參數(共用全域變數)可(就是主程式的一部分)可用,但只用來「切檔案」,不要當封裝手段

參數(Parameter)的用途是「在呼叫程式與模組單元之間交換資料」,定義模組時就決定了有哪些參數。講義的三分法:

  • 輸入參數(Importing/USING):把資料傳給模組。
  • 匯出參數(Exporting/CHANGING 的回傳側):把模組內的結果送回呼叫程式。
  • 變更參數(Changing):傳進去、改完再傳回來。
老師的口訣(第 10-8 節):「模組化共有五種手法:FUNCTION、FORM、DEFINE、CLASS、INCLUDE。」面試常問,記起來。真正要選的時候,順序是:CLASS > FUNCTION > FORM > DEFINE,INCLUDE 只是切檔案用的輔助。

講義開頭那段 FORM SET_AMOUNT 是很典型的「幣別換算後改寫內表」場景。以下是同一段邏輯改寫成 S/4HANA 現代語法的樣子(把 FORM 改成本地類別方法、內表用 FIELD-SYMBOL 就地修改):

CLASS lcl_amount DEFINITION.
  PUBLIC SECTION.
    " 依幣別的小數位換算係數,就地調整 ACDOCA 的三個金額欄位
    METHODS set_amount
      IMPORTING iv_waers  TYPE waers
      CHANGING  ct_acdoca TYPE ty_t_acdoca.
ENDCLASS.

CLASS lcl_amount IMPLEMENTATION.
  METHOD set_amount.
    DATA(lv_factor) = CONV i( 1 ).           " 預設係數 1

    CALL FUNCTION 'CURRENCY_CONVERTING_FACTOR'
      EXPORTING  currency          = iv_waers
      IMPORTING  factor            = lv_factor
      EXCEPTIONS too_many_decimals = 1
                 OTHERS            = 2.
    IF sy-subrc <> 0.
      lv_factor = 1.                          " 取不到就當 1,不要讓金額變 0
    ENDIF.

    " 舊寫法是 LOOP ... MODIFY it FROM wa;現代用 FIELD-SYMBOL 就地改,快且不必 MODIFY
    LOOP AT ct_acdoca ASSIGNING FIELD-SYMBOL(<ls>).
      <ls>-wsl = <ls>-wsl * lv_factor.        " 交易貨幣金額
      <ls>-tsl = <ls>-tsl * lv_factor.        " 公司代碼貨幣金額
      <ls>-ksl = <ls>-ksl * lv_factor.        " 集團貨幣金額
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.
S/4HANA:ACDOCA 是「通用日記帳(Universal Journal)」,FI(BSEG/BKPF)、CO(COEP/COBK)、AA、ML 的行項目全部落在這一張表。舊程式若還在讀 BSEG 做金額彙總,在 S/4 會抓不到 CO 的資料,也享受不到 HANA 的欄位式壓縮。

2. FORM/PERFORM:語法、USING、CHANGING、TABLES

副程式(Subroutine)是「原始碼裡具有一定獨立功能的模組單元」,用 FORM 開頭、ENDFORM 結尾。分兩類:

  • 內部副程式:原始碼與呼叫程式在同一支 ABAP 程式中。
  • 外部副程式:原始碼在別支程式裡,用 PERFORM form IN PROGRAM prog 呼叫。

基本規則(講義重點):

  • 語法:FORM <subroutine> [<pass>]. ... ENDFORM.,參數段可以完全省略。
  • 副程式可以直接存取主程式宣告的所有全域變數 —— 這正是它最大的坑。
  • 同一程式的所有內部副程式,習慣集中放在程式最末端(或獨立成 F01 include)。
  • 副程式的定義不能互相巢狀(FORM 裡不能再寫 FORM),但可以互相呼叫、也可以遞迴。
  • 形式參數(formal parameters)= FORM 上宣告的;實際參數(actual parameters)= PERFORM 傳進去的。
REPORT zdemo_form.

PERFORM welcome.                 " 呼叫副程式

DATA(lv_str1) = |Hello|.
DATA(lv_str2) = |SAP|.
PERFORM con_str USING lv_str1 lv_str2.

*&--- 副程式定義 -------------------------------------------------------*
FORM welcome.
  WRITE / 'Subroutine'.
ENDFORM.

FORM con_str USING iv_s1 TYPE string
                   iv_s2 TYPE string.
  " 舊講義用 CONCATENATE,現代一律用字串範本
  DATA(lv_all) = |{ iv_s1 } { iv_s2 }|.
  WRITE / lv_all.
ENDFORM.

三種參數段的差別:

寫法傳遞方式語意
USING pCall by reference(傳址)語意上是「只讀」,但 ABAP 不會擋你改,改了外面真的會變
USING VALUE(p)Call by value(傳值)副程式內改動不影響外部變數
CHANGING pCall by reference明確表示「要改」
CHANGING VALUE(p)Call by value and result執行中不影響外部;ENDFORM 正常結束時才把值寫回
TABLES pCall by reference,內表已過時:參數會被當成帶表頭行(header line)的內表
CHANGING VALUE( ) 的隱藏陷阱:值是在 ENDFORM 正常結束時才回寫。如果副程式中途遇到 MESSAGE ... TYPE 'E'EXIT 離開程式或發生例外,回寫不會發生,呼叫端拿到的還是舊值。這種 bug 非常難查。
TABLES 參數已過時FORM f TABLES it STRUCTURE mara. 語法檢查會出現「過時」警告。改用型別化的 USING/CHANGING。注意 FORM f USING it TYPE TABLE OF mara 是錯的 —— FORM 的參數只能用「已存在的型別名稱」或泛型型別(TYPE STANDARD TABLETYPE INDEX TABLETYPE ANY TABLE),不能在參數列上就地 TABLE OF。正確寫法見下例。
*&--- 新方法:用 TYPES 先定義表型別,再給 FORM 參數強型別 -------------*
TYPES ty_t_mara TYPE STANDARD TABLE OF mara WITH EMPTY KEY.

DATA: lt_input  TYPE ty_t_mara,
      lt_output TYPE ty_t_mara.

PERFORM process_data USING lt_input CHANGING lt_output.

FORM process_data USING    it_input  TYPE ty_t_mara
                  CHANGING ct_output TYPE ty_t_mara.
  " 只挑出成品(FERT)
  ct_output = VALUE #( FOR ls IN it_input
                       WHERE ( mtart = 'FERT' )
                       ( ls ) ).
ENDFORM.

*&--- 舊方法(講義原文,S/4 請勿再寫)---------------------------------*
* FORM process_data TABLES it_input  STRUCTURE mara
*                          it_output STRUCTURE mara.
*   LOOP AT it_input INTO DATA(ls_input).
*     IF ls_input-mtart = 'FERT'.
*       APPEND ls_input TO it_output.
*     ENDIF.
*   ENDLOOP.
* ENDFORM.
* PERFORM process_data TABLES lt_input lt_output.
S/4HANA 與 ABAP Cloud 的定調FORMPERFORM 自 ABAP Objects 推出後就被 SAP 標示為 obsolete;在 ABAP Cloud(ABAP for Cloud Development、BTP ABAP Environment、S/4HANA Cloud 的開發模型)中被完全禁止,語法檢查直接報錯。原因:無法完整型別檢查、沒有 RETURNING、無法單元測試、全域變數污染。舊程式維護可以留,新程式一律改寫成類別方法。

3. 三種傳參方式的實證:傳址、傳值、傳值並回傳

講義用三支小程式(ZPU300_06、Z202202_29/30/31)示範同一件事,把三種方式的差異壓在同一個算式上。這裡合併成一支現代寫法的示範程式,一次跑完三種。

REPORT zdemo_pass_by.

DATA: lv_a TYPE i VALUE 1,
      lv_b TYPE i VALUE 2,
      lv_c TYPE i VALUE 0.

*--- 方式一:call by reference(傳址)------------------------------*
PERFORM frm_ref USING lv_a lv_b lv_c.
WRITE: / |方式一 main a={ lv_a } b={ lv_b } c={ lv_c }|.

*--- 方式二:call by value(傳值)----------------------------------*
lv_a = 1. lv_b = 2. lv_c = 0.
PERFORM frm_val USING lv_a lv_b lv_c.
WRITE: / |方式二 main a={ lv_a } b={ lv_b } c={ lv_c }|.

*--- 方式三:call by value and return result -----------------------*
lv_a = 1. lv_b = 2. lv_c = 0.
PERFORM frm_val_ret USING lv_a lv_b CHANGING lv_c.
WRITE: / |方式三 main a={ lv_a } b={ lv_b } c={ lv_c }|.

*------------------------------------------------------------------*
FORM frm_ref USING p_a TYPE i p_b TYPE i p_c TYPE i.
  p_c = p_a + p_b.          " 傳址:外面的 lv_c 真的變成 3
ENDFORM.

FORM frm_val USING VALUE(p_a) TYPE i VALUE(p_b) TYPE i VALUE(p_c) TYPE i.
  p_c = p_a + p_b.          " 傳值:副程式內 p_c=3,但外面的 lv_c 仍是 0
ENDFORM.

FORM frm_val_ret USING    VALUE(p_a) TYPE i
                          VALUE(p_b) TYPE i
                 CHANGING VALUE(p_c) TYPE i.
  " 這不是 call by address(reference),
  " 而是讓 FORM 能 return 運算結果,稱為 call by value and return result
  p_c = p_a + p_b.
  p_a = 0.                  " 改 p_a、p_b 都不會影響外部
  p_b = 0.
ENDFORM.                    " <-- 執行到這一行,p_c 的 3 才寫回 lv_c

執行結果重點:方式一與方式三的 c 都是 3,但方式三的 ab 保持原值;方式二的 c 仍是 0。

記法:USING = 我給你看;CHANGING = 我給你改;VALUE( ) = 我給你影本。影本在 CHANGING VALUE() 時,結束才貼回正本。

對照到現代方法(Method),這三件事變得一目瞭然,而且編譯器會幫你擋錯:

CLASS lcl_calc DEFINITION.
  PUBLIC SECTION.
    " IMPORTING 在方法內是「唯讀」,編譯器直接擋,比 FORM USING 安全
    CLASS-METHODS add
      IMPORTING iv_a          TYPE i
                iv_b          TYPE i
      RETURNING VALUE(rv_sum) TYPE i.     " RETURNING 必須是 pass by value
ENDCLASS.

CLASS lcl_calc IMPLEMENTATION.
  METHOD add.
    rv_sum = iv_a + iv_b.
    " iv_a = 0.   <-- 這一行會直接語法錯誤:IMPORTING 參數不可修改
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  " RETURNING 讓方法可以像函數一樣直接寫在運算式裡
  DATA(lv_total) = lcl_calc=>add( iv_a = 1 iv_b = 2 ) * 10.
  WRITE / |Total: { lv_total NUMBER = USER }|.

4. 區域變數、全域變數,與外部呼叫(IN PROGRAM/SUBMIT/CALL TRANSACTION)

  • 全域變數:在主程式宣告,主程式與它呼叫的每個副程式都看得到、改得動。
  • 區域變數:在副程式(或方法)內宣告,只活在該副程式中,每次呼叫重新建立在堆疊上 —— 所以遞迴是安全的
  • 同名時區域變數優先,全域變數不受影響。
  • STATICS 宣告的變數例外:它跨呼叫保留值(可用來做計數器/快取),但也是 bug 溫床。
老師特別強調:「副程式中應避免使用主程式的變數,應使用參數。」這條規矩在寫方法時更嚴格 —— 方法只能透過參數與類別屬性溝通,這正是 OO 比 FORM 好維護的根本原因。

遞迴示範(副程式與方法都適用,這裡示範方法版):

CLASS lcl_rec DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS factorial
      IMPORTING iv_n          TYPE i
      RETURNING VALUE(rv_res) TYPE p LENGTH 16 DECIMALS 0.
ENDCLASS.

CLASS lcl_rec IMPLEMENTATION.
  METHOD factorial.
    " 每一層呼叫都有自己的區域變數,所以遞迴安全
    rv_res = COND #( WHEN iv_n <= 1
                     THEN 1
                     ELSE iv_n * factorial( iv_n - 1 ) ).
  ENDMETHOD.
ENDCLASS.

外部呼叫的三種方式(講義 04-9 與第 4 節):

REPORT z027_t.

DATA: lv_num1 TYPE i VALUE 10,
      lv_num2 TYPE i VALUE 20,
      lv_num3 TYPE i.

*--- (1) 呼叫另一支程式裡的副程式:外部 PERFORM ---------------------*
"    好處是共用邏輯,壞處是耦合、無法型別檢查、對方改壞你就 dump
PERFORM calculator IN PROGRAM z300_common
        USING lv_num1 lv_num2 CHANGING lv_num3.
WRITE: / |num1={ lv_num1 } num2={ lv_num2 } num3={ lv_num3 }|.

*--- (2) 呼叫另一支報表:SUBMIT(常用於 drill-down)-----------------*
SUBMIT z300_report
       WITH p_bukrs = '1000'
       WITH s_matnr IN s_matnr        " 把 select-option 整段帶過去
       VIA SELECTION-SCREEN           " 顯示對方的選擇畫面
       AND RETURN.                    " 執行完回到本程式

*--- (3) 呼叫交易碼 ------------------------------------------------*
CALL TRANSACTION 'VA03' AND SKIP FIRST SCREEN.
PERFORM ... IN PROGRAM 講義自己就標了「不建議」。它有幾個致命問題:對方程式若沒載入會 dump(要加 IF FOUND)、參數完全沒有型別檢查、跨程式相依性在 Where-Used 上看不清楚。要共用邏輯就建 Function Module 或全域類別。
另外 CALL TRANSACTION 在 S/4HANA 仍可用,但在 ABAP Cloud 禁用;Fiori 環境要改用 Intent Navigation。
DATA BEGIN OF COMMON PART(講義第 7 節提到)是早期讓主程式與外部副程式共用資料區的機制:DATA: BEGIN OF COMMON PART numbers, num1 TYPE i, ... END OF COMMON PART numbers.已完全過時,S/4 請勿使用,改用 Function Group 全域資料或類別靜態屬性。

5. Function Group 與 Function Module:SE37/SE80

Function Module(函數模組)是具有全域可見性的特殊程式,全系統任何程式都能呼叫。它只能在 Function Group(函數組)中定義 —— Function Group 是 FM 的「主程式」,一個 Group 可以包含多個 FM,通常放同一類物件的操作。

動作入口說明
建立/修改 Function GroupSE37 → 功能表 Goto → Function Groups → Create/Change/Display/Delete Group也可以在 SE80 直接建(建議)
顯示 Function Group 結構SE80 物件瀏覽器可以看到底下所有 Include、FM、畫面
建立/修改 Function ModuleSE37輸入 FM 名稱 → 選 Function Group → 填描述
測試 FMSE37 → 測試/執行(F8)不用寫呼叫程式就能測

Function Group 產生的 Include 結構(假設 Group 名為 ZFG_SD,主程式為 SAPLZFG_SD):

Include內容
LZFG_SDTOPFUNCTION-POOL 敘述與全域資料宣告(本 Group 內所有 FM 共用)
LZFG_SDUXX系統自動產生,內含 INCLUDE LZFG_SDU01. INCLUDE LZFG_SDU02. ...
LZFG_SDU01U02每個 FM 的實作原始碼(一個 FM 一個 Uxx)
LZFG_SDF01自行建立,放 Group 內共用的 FORM/本地類別

函數處理類型(Processing Type),在屬性頁籤選:

  1. Normal Function Module(一般函數模組):只能在本系統使用。
  2. Remote-Enabled Module(遠端模組,RFC):可被其他 SAP 或非 SAP 系統呼叫,呼叫時要指定 DESTINATION,目的地在 SM59 設定。
  3. Update Module(更新函數):用於資料庫資料更新,講義註「常用於系統導入之初的大量數據更新」。

介面元素(SE37 的五個頁籤):

頁籤語意是否可選
Import呼叫時把值傳進 FM可勾 Optional;可設預設值(Default)
ExportFM 把結果送出永遠是 Optional
Changing傳進去、改完傳回可勾 Optional;可設預設值
Tables用內表傳參,行為同 Changing已過時(見下方警告)
Exceptions錯誤情況;呼叫端用 sy-subrc 檢查
參數型別要怎麼填(講義的三個註記):輸出結果若是一列資料,Associated Type 填結構(如 VBAK);若要傳一整張表,用 TABLES 頁籤或在 Export/Changing 填表型別(如 SE11 建的 VBAP_T)。老師的提醒是:用表型別(Table Type)取代 TABLES 頁籤,才不會被系統標過時。
TABLES 參數在 SE37 已被標為過時:講義寫「用 TABLES 頁籤傳回 EKES,系統會提示過時,但強制 ENTER 仍會過」。可以過,但新 FM 請一律改用 Export/Changing + Table Type。唯一例外:RFC FM 的相容性與部分外部系統(如舊版 JCo/pyrfc 呼叫端)仍習慣用 TABLES 參數,此時可保留。
測試前一定要全部 ACTIVE:Function Group 和 Function Module 兩者都要激活,否則 SE37 測試會失敗或呼叫端找不到。編輯程式時善用 Ctrl+F6 快速複製既有 FM 的呼叫樣板(SE37 → 模式 Pattern,或編輯器的 Pattern 按鈕,可自動生成完整 CALL FUNCTION 骨架)。
有些 BASIS 會安裝 BC400 這個 SAP 標準教學包(含 SFLIGHT/SBOOK 練習資料與範例 FM),是 ABAPer 練習 FUNCTION 的最佳教材。講義註明 GBIKE 環境未安裝。

6. 實作案例:ZFM_CASPER_VBAP 與 ZFM_GET_EBELP

講義的第一個 FM 練習:輸入銷售訂單號 I_VBELN,回傳表頭結構 E_VBAK 與表身內表 T_VBAP,找不到就拋 VBAK_NOT_EXIST 例外。

FUNCTION zfm_casper_vbak_vbap.
*"----------------------------------------------------------------------
*" IMPORTING  I_VBELN TYPE VBELN_VA
*" EXPORTING  E_VBAK  TYPE VBAK
*"            ET_VBAP TYPE ZTT_VBAP        " SE11 建的 Table Type
*" EXCEPTIONS VBAK_NOT_EXIST
*"----------------------------------------------------------------------

  CLEAR: e_vbak, et_vbap.

  " 表頭:明列欄位,主機變數加 @
  SELECT SINGLE vbeln, erdat, auart, vkorg, vtweg, spart, kunnr, netwr, waerk
    FROM vbak
    WHERE vbeln = @i_vbeln
    INTO CORRESPONDING FIELDS OF @e_vbak.

  IF sy-subrc <> 0.
    RAISE vbak_not_exist.        " 傳統例外:呼叫端用 sy-subrc 接
  ENDIF.

  " 表身
  SELECT vbeln, posnr, matnr, arktx, kwmeng, vrkme, netwr, werks
    FROM vbap
    WHERE vbeln = @i_vbeln
    ORDER BY posnr                " 一定要排序,否則順序不保證
    INTO CORRESPONDING FIELDS OF TABLE @et_vbap.

ENDFUNCTION.

呼叫端(講義 Z202202_28)現代化版本 —— 注意 ALPHA 轉換:使用者輸入 40,資料庫存的是 0000000040

REPORT z202202_28.

PARAMETERS p_vbeln TYPE vbeln_va OBLIGATORY.

START-OF-SELECTION.

  " 舊寫法要 CALL FUNCTION 'CONVERSION_EXIT_ALPHA_INPUT';
  " S/4 可以直接用字串範本的 ALPHA 修飾詞,一行搞定
  DATA(lv_vbeln) = CONV vbeln_va( |{ p_vbeln ALPHA = IN }| ).   " 前補 0
  " 反向顯示用:|{ lv_vbeln ALPHA = OUT }|  → 去掉前導 0

  DATA ls_vbak  TYPE vbak.
  DATA lt_vbap  TYPE ztt_vbap.

  CALL FUNCTION 'ZFM_CASPER_VBAK_VBAP'
    EXPORTING  i_vbeln        = lv_vbeln
    IMPORTING  e_vbak         = ls_vbak
               et_vbap        = lt_vbap
    EXCEPTIONS vbak_not_exist = 1
               OTHERS         = 2.

  CASE sy-subrc.
    WHEN 0.
      WRITE: / |訂單編號存在:{ lv_vbeln ALPHA = OUT },共 { lines( lt_vbap ) } 個項目|.
    WHEN 1.
      WRITE: / |訂單編號不存在:{ lv_vbeln ALPHA = OUT }|.
    WHEN OTHERS.
      WRITE: / |呼叫失敗,sy-subrc = { sy-subrc }|.
  ENDCASE.
EXCEPTIONS 一定要寫 OTHERS。如果 FM 拋了一個你沒列在 EXCEPTIONS 區塊的例外,程式會直接以執行階段錯誤(short dump)終止,不是回傳 sy-subrc。講義原文:「如果未列出該異常,則程式會因執行階段錯誤而終止。」

第二個案例 ZFM_GET_EBELP:依採購單號+項次,回傳最近一筆 EKKO/EKPO 與收貨確認 EKES。講義原文用 SELECT * ... UP TO 1 ROWS ... ORDER BY ... LOOP 取值,這在 S/4 有明確的寫法問題:

講義原程式的三個坑
1. SELECT ... UP TO 1 ROWS ... ORDER BY x DESCENDING 在傳統 DB 是先排序再取一筆,但語意上易誤解;在 HANA 上要確保 ORDER BY 真的先於 UP TO 生效,建議寫 SELECT ... ORDER BY ... INTO ... UP TO 1 ROWS 或用 SELECT SINGLE 搭配明確 key。
2. SELECT SINGLE 在沒有指定完整主鍵時,回哪一筆是不保證的(HANA 上的平行處理讓結果更不穩定)。要「最近一筆」就必須 ORDER BY。
3. 原程式在 LOOP ... ENDLOOP 之後才檢查 sy-subrc,此時 sy-subrc 已被 LOOP 覆寫,檢查的其實是 LOOP 的結果不是 SELECT 的結果。這是老師課堂上要學生自己抓的錯。
FUNCTION zfm_get_ebelp.
*"----------------------------------------------------------------------
*" IMPORTING  I_EBELN TYPE EBELN
*"            I_EBELP TYPE EBELP
*"            I_WERKS TYPE WERKS_D DEFAULT 'TW10'
*" EXPORTING  E_EKKO  TYPE EKKO
*"            E_EKPO  TYPE EKPO
*"            E_EKES  TYPE EKES        " 最近一筆收貨確認
*" EXCEPTIONS EKPO_NOT_EXIST
*"----------------------------------------------------------------------

  CLEAR: e_ekko, e_ekpo, e_ekes.

  " 採購單項目(主鍵齊全,用 SELECT SINGLE 最安全)
  SELECT SINGLE ebeln, ebelp, matnr, werks, menge, meins, netpr, aedat
    FROM ekpo
    WHERE ebeln = @i_ebeln
      AND ebelp = @i_ebelp
      AND werks = @i_werks
    INTO CORRESPONDING FIELDS OF @e_ekpo.

  IF sy-subrc <> 0.
    RAISE ekpo_not_exist.
  ENDIF.

  " 採購單表頭
  SELECT SINGLE ebeln, bukrs, bsart, lifnr, bedat, waers
    FROM ekko
    WHERE ebeln = @i_ebeln
    INTO CORRESPONDING FIELDS OF @e_ekko.

  " 最近一筆收貨確認:必須 ORDER BY,UP TO 1 ROWS 才有意義
  SELECT ebeln, ebelp, etens, eindt, menge, dabmg
    FROM ekes
    WHERE ebeln = @i_ebeln
      AND ebelp = @i_ebelp
      AND dabmg > 0
    ORDER BY eindt DESCENDING, etens DESCENDING
    INTO CORRESPONDING FIELDS OF @e_ekes
    UP TO 1 ROWS.
  ENDSELECT.        " 取一筆的 SELECT...UP TO 1 ROWS 必須配 ENDSELECT

ENDFUNCTION.
上面是少數必須保留 SELECT ... ENDSELECT 的場合(UP TO 1 ROWS 搭配 ORDER BY 寫進工作區)。若不想寫 ENDSELECT,可改成 ... INTO TABLE @DATA(lt_ekes) UP TO 1 ROWS. 再取 lt_ekes[ 1 ]

7. Function Group 的全域資料、SET/GET 樣式與 SE37 測試序列

同一個 Function Group 內的所有 FM,共用 LxxxTOP 裡宣告的全域資料,而且這份資料在整個使用者交談期間(roll area)持續存在,只要 Function Group 被載入就不會消失。這就是 SAP 標準大量使用的 SET/GET 樣式(例如 SET_PARAMETERGET_PARAMETER、各種 *_INIT*_SAVE 配對)。

*&--- Include LZFG_BUFFERTOP:Function Group 全域資料 -----------------*
FUNCTION-POOL zfg_buffer.

TYPES: BEGIN OF ty_ctx,
         bukrs TYPE bukrs,
         gjahr TYPE gjahr,
         monat TYPE monat,
       END OF ty_ctx.

DATA gs_ctx TYPE ty_ctx.          " 整個 Group 共用,跨 FM 呼叫仍存在

*&--- FM 1:設定 --------------------------------------------------*
FUNCTION z_ctx_set.
*" IMPORTING IV_BUKRS TYPE BUKRS
*"           IV_GJAHR TYPE GJAHR
*"           IV_MONAT TYPE MONAT
  gs_ctx = VALUE #( bukrs = iv_bukrs gjahr = iv_gjahr monat = iv_monat ).
ENDFUNCTION.

*&--- FM 2:取用 --------------------------------------------------*
FUNCTION z_ctx_get.
*" EXPORTING ES_CTX TYPE TY_CTX
  es_ctx = gs_ctx.                " 先前 Z_CTX_SET 設的值還在
ENDFUNCTION.
SE37 測試序列(Test Sequence):要驗證上面這種「先 SET 再 GET」的行為,不必寫呼叫程式。在 SE37 進入 FM 後,功能表 Function Module → Test → Test sequence(測試 → 測試序列),輸入要依序執行的 FM 名稱清單(例如 Z_CTX_SET、Z_CTX_GET),按執行,系統會在同一個 roll area 內依序跑,你就能看到全域資料真的被保留下來。這是測試 SET/GET 型 FM、以及測試 Update FM 的標準手法。
全域資料是雙面刃:好處是省下重複查詢(可以當 buffer),壞處是狀態殘留。如果某個 FM 忘了 CLEAR,下一次呼叫可能拿到上一張單據的資料 —— 批次處理時特別容易中招。習慣在每個 FM 開頭 CLEAR 掉輸出參數。

8. RFC 函數與 Update 函數(V1/V2)

RFC-Enabled Function Module:在 SE37 屬性頁籤勾 Remote-Enabled Module。呼叫時指定 DESTINATION,目的地在 SM59 維護(ABAP Connection、TCP/IP Connection、HTTP Connection…)。

*--- (1) 同步 RFC(sRFC):最常見,呼叫端會等 -----------------------*
CALL FUNCTION 'ZFM_GET_STOCK'
  DESTINATION 'PRD100'                 " SM59 定義的目的地
  EXPORTING  iv_werks              = '1000'
  IMPORTING  et_stock              = lt_stock
  EXCEPTIONS system_failure        = 1  MESSAGE lv_msg
             communication_failure = 2  MESSAGE lv_msg
             resource_failure      = 3
             OTHERS                = 4.
IF sy-subrc <> 0.
  MESSAGE |RFC 失敗({ sy-subrc }):{ lv_msg }| TYPE 'E'.
ENDIF.

*--- (2) 非同步 aRFC:不等回應,可平行處理 --------------------------*
CALL FUNCTION 'ZFM_HEAVY_JOB'
  STARTING NEW TASK 'JOB01'
  DESTINATION 'NONE'
  EXPORTING iv_werks = '1000'.

*--- (3) 交易式 tRFC / 佇列式 qRFC:保證只執行一次、可排隊 ----------*
CALL FUNCTION 'ZFM_SEND_IDOC'
  IN BACKGROUND TASK
  DESTINATION 'MW100'
  EXPORTING iv_docnum = lv_docnum.
COMMIT WORK.                            " tRFC 要 COMMIT 才會真的送出
種類語法監控 TCODE
sRFC 同步DESTINATIONSM59(連線測試)
aRFC 非同步STARTING NEW TASKSM50/SM66
tRFC 交易式IN BACKGROUND TASKSM58
qRFC 佇列式IN BACKGROUND UNITSMQ1(出)/SMQ2(入)
RFC FM 的介面限制:所有參數必須是傳值(pass by value);型別必須是 DDIC 型別(不能用程式內定義的本地型別);不能有深層巢狀的非 DDIC 結構;必須宣告 SYSTEM_FAILURECOMMUNICATION_FAILURE 兩個標準例外。另外 RFC FM 內不能做螢幕輸出(WRITE/POPUP)。

Update Function Module(更新函數,V1/V2):用 CALL FUNCTION ... IN UPDATE TASK 呼叫,實際的資料庫更新不會立刻執行,而是登錄在更新佇列裡,等到 COMMIT WORK 才由更新工作程序(update work process)執行。

類型SE37 屬性設定執行時機失敗影響
V1(Update Module)Update Module,勾 Start immediately 之外的選項COMMIT WORK 後立即、且在同一個 DB LUW 內依序執行失敗則整個 V1 全部 rollback,單據不會產生
V2(Update Module,延遲)Update Module (delayed) / Start delayed所有 V1 成功後才執行,獨立 LUWV2 失敗不影響 V1;適合統計、次要資料
Collective runCollective run由 RSM13005 排程批次執行用於可延後的大量更新
*--- 呼叫端:整個交易的邏輯工作單元(LUW)--------------------------*
CALL FUNCTION 'Z_POST_HEADER'  IN UPDATE TASK    " V1:關鍵資料
  EXPORTING is_head = ls_head.

CALL FUNCTION 'Z_POST_STATIS'  IN UPDATE TASK    " V2:統計資料
  EXPORTING is_stat = ls_stat.

IF lv_error IS INITIAL.
  COMMIT WORK AND WAIT.        " AND WAIT:等更新真的做完才往下走
ELSE.
  ROLLBACK WORK.               " 取消整批登錄的更新
ENDIF.
SM13 看更新請求(Update Requests)的狀態與錯誤,SM14 管理更新機制的啟停。程式明明沒報錯但資料沒進去,第一個要看的就是 SM13。
COMMIT WORK AND WAITCOMMIT WORK 的差別是後續程式能不能立刻讀到剛寫的資料 —— 呼叫 BAPI 後馬上要 SELECT 驗證,一定要加 AND WAIT
在 Update FM 裡不可以使用 COMMIT WORK、不可以呼叫對話式畫面、不可以用 MESSAGE ... TYPE 'E'(會造成更新終止且使用者看不到)。要回報錯誤請用 MESSAGE ... TYPE 'A' 或寫進 log。

9. MACRO:DEFINE … END-OF-DEFINITION

巨集(Macro)是「一段獨立的程式碼,能實現資料的運算與輸出,功能與副程式類似,主要用於同一程式中重複的運算,以簡化程式碼」。語法:

REPORT z202202_32.

DATA lv_a TYPE i VALUE 1.
DATA lv_b TYPE i VALUE 10.

DEFINE increment.          " increment 為巨集名稱
  &1 = &1 + 1.             " &1 = 第 1 個參數,最多到 &9
  WRITE / &1.
  WRITE / &2.
END-OF-DEFINITION.

increment lv_a lv_b.       " 呼叫:不加逗號、不加括號,參數用空白隔開

規則(講義重點):

  • &1~&9 接收參數,最多九個,不需要也不能宣告型別
  • 巨集只能被本程式中「定義於巨集之後」的敘述呼叫,所以巨集一律寫在程式最前面。
  • 展開發生在編譯期(純文字替換),所以巨集無法 debug —— 這是它最大的缺點,講義特別註明「marco 是無法被 debug 的,因此程式不宜太長」。
  • 另有存在 TRMAC 表的全域巨集,用 SE38 → 功能表 → 巨集維護,可跨程式使用(更不建議)。

老師示範的兩個實務用法都是「建 ALV Field Catalog」—— 這正是巨集最經典的使用場景,因為要重複填十幾個欄位:

*--- 講義 05-2/05-3:用 DEFINE 快速堆 Field Catalog ----------------*
DEFINE add_col.
  ls_fieldcat-fieldname = &1.
  ls_fieldcat-seltext_m = &2.
  ls_fieldcat-outputlen = &3.
  ls_fieldcat-col_pos   = &4.
  APPEND ls_fieldcat TO lt_fieldcat.
  CLEAR ls_fieldcat.
END-OF-DEFINITION.

add_col 'MATNR' '物料編號macro' 40 1.
add_col 'MATKL' '物料群組macro'  9 2.
add_col 'MAKTX' '物料描述macro' 40 3.
講義 05-3 的血淚教訓(20250424 實作):「為了求快,沒有將 SORT 的欄位也放入 ALV,造成 ABAP DUMP,而且從錯誤訊息不易察覺。」原因就是巨集展開後沒有行號可對應,Runtime Error 指到的是展開後的位置,Debug 也停不進去。這就是為什麼現在不建議用 MACRO。

S/4HANA 的替代寫法:同樣的需求,用 CL_SALV_TABLE 根本不需要 Field Catalog;就算要細調欄位,也用型別安全的方法或 VALUE #( ) 一次建好:

*--- 替代 1:CL_SALV_TABLE,欄位標題直接改,不用 fieldcat ------------*
TRY.
    cl_salv_table=>factory(
      IMPORTING r_salv_table = DATA(lo_alv)
      CHANGING  t_table      = lt_data ).

    DATA(lo_cols) = lo_alv->get_columns( ).
    lo_cols->set_optimize( abap_true ).

    DATA(lo_col) = CAST cl_salv_column_table( lo_cols->get_column( 'MATNR' ) ).
    lo_col->set_long_text( '物料編號' ).
    lo_col->set_output_length( 40 ).

    lo_alv->display( ).
  CATCH cx_salv_msg cx_salv_not_found INTO DATA(lx).
    MESSAGE lx->get_text( ) TYPE 'E'.
ENDTRY.

*--- 替代 2:真的要 fieldcat(CL_GUI_ALV_GRID),用 VALUE # 一次建好 --*
DATA(lt_fcat) = VALUE lvc_t_fcat(
  ( fieldname = 'MATNR' scrtext_l = '物料編號' outputlen = 40 col_pos = 1 )
  ( fieldname = 'MATKL' scrtext_l = '物料群組' outputlen =  9 col_pos = 2 )
  ( fieldname = 'MAKTX' scrtext_l = '物料描述' outputlen = 40 col_pos = 3 )
  ( fieldname = 'MFRPN' scrtext_l = '標記(改)' outputlen = 40 col_pos = 4 edit = abap_true ) ).
作業(05-1):用 SE80 打開 VA03 的主程式 SAPMV45A,在物件樹裡找 Macros 節點,看 SAP 標準怎麼用巨集(例如各種 *_message、欄位搬移巨集)。結論:「模組顧問又多認識一種模組化編程的技巧了。」

10. INCLUDE:把程式切成 TOP/F01/O01/I01/E01

INCLUDE 的定位要先講清楚:它不是模組化封裝,而是「把一個檔案切成多個檔案」。編譯時系統把 include 的內容原地展開,執行期完全等同於寫在一起。用途是:共用資料宣告、讓大程式好維護、讓多人可以同時編輯不同 include。

  • 語法:INCLUDE <include 程式名>.
  • 被包含的程式不能有 PROGRAM 或 REPORT 敘述(其程式類型要設為 Include program,即 I 型)。
  • Include 不能包含自己(不可遞迴)。
  • Include 內必須是完整的敘述,不能把一句 ABAP 切成兩半放在兩個 include。

SAP 標準命名慣例(Module Pool/大型報表都照這個切,非常重要):

後綴內容對應
TOP全域宣告:TYPES、DATA、CONSTANTS、TABLES、SELECTION-SCREEN所有程式都會有
F01(F02…)FORM 副程式/本地類別實作報表主邏輯
O01MODULE ... OUTPUT(PBO,Process Before Output)Dialog 程式
I01MODULE ... INPUT(PAI,Process After Input)Dialog 程式
E01AT SELECTION-SCREEN/事件區塊選擇畫面事件
C01 / CL1本地類別定義(CLASS DEFINITION)OO 化之後常用

講義 7-2 的 include 案例,改寫成 S/4HANA 版本(原文用 OCCURS 0 WITH HEADER LINEREUSE_ALV_GRID_DISPLAY,這裡全部現代化):

*&--- 主程式 ZINCLUDE ------------------------------------------------*
REPORT zinclude.

INCLUDE zinclude_top.        " 宣告
INCLUDE zinclude_c01.        " 本地類別定義
INCLUDE zinclude_f01.        " 實作

START-OF-SELECTION.
  lcl_app=>get_data( ).
  lcl_app=>display_alv( ).

*&--- Include ZINCLUDE_TOP -------------------------------------------*
TYPES: BEGIN OF ty_acdoca,
         rldnr TYPE acdoca-rldnr,
         rbukrs TYPE acdoca-rbukrs,
         gjahr  TYPE acdoca-gjahr,
         belnr  TYPE acdoca-belnr,
         docln  TYPE acdoca-docln,
         racct  TYPE acdoca-racct,
         hsl    TYPE acdoca-hsl,
         rhcur  TYPE acdoca-rhcur,
       END OF ty_acdoca.
TYPES ty_t_acdoca TYPE STANDARD TABLE OF ty_acdoca WITH EMPTY KEY.

*&--- Include ZINCLUDE_C01 -------------------------------------------*
CLASS lcl_app DEFINITION.
  PUBLIC SECTION.
    CLASS-DATA gt_out TYPE ty_t_acdoca.
    CLASS-METHODS get_data.
    CLASS-METHODS display_alv.
ENDCLASS.

*&--- Include ZINCLUDE_F01 -------------------------------------------*
CLASS lcl_app IMPLEMENTATION.

  METHOD get_data.
    " 明列欄位、加 @、限筆數;不要 SELECT * INTO CORRESPONDING FIELDS
    SELECT rldnr, rbukrs, gjahr, belnr, docln, racct, hsl, rhcur
      FROM acdoca
      INTO TABLE @gt_out
      UP TO 100 ROWS.
  ENDMETHOD.

  METHOD display_alv.
    TRY.
        cl_salv_table=>factory(
          IMPORTING r_salv_table = DATA(lo_alv)
          CHANGING  t_table      = gt_out ).
        lo_alv->get_columns( )->set_optimize( abap_true ).   " 取代 colwidth_optimize
        lo_alv->get_functions( )->set_all( abap_true ).
        lo_alv->display( ).
      CATCH cx_salv_msg INTO DATA(lx).
        MESSAGE lx->get_text( ) TYPE 'E'.
    ENDTRY.
  ENDMETHOD.

ENDCLASS.
講義原文的 DATA: LT_acdoca LIKE acdoca OCCURS 0 WITH HEADER LINE.REUSE_ALV_FIELDCATALOG_MERGEREUSE_ALV_GRID_DISPLAY 這一套,在 S/4HANA 雖然還能跑,但:WITH HEADER LINE 與 OCCURS 0 已過時且 OO 環境禁用REUSE_ALV_* 系列是 Function ALV,未列入 ABAP Cloud 的釋出 API。新程式一律用 CL_SALV_TABLE。
作業 7-3:把 Z202202_26 改寫成 include 模式,結構如下 ——
INCLUDE Z00_26_TOP(宣告變數)/INCLUDE Z00_26_FORM(副程式),主程式只留:
PERFORM GET_DATA(取得資料)→ PERFORM LAYOUT(設定 Layout)→ PERFORM FIELDCAT(設定欄位屬性)→ PERFORM EVENT(設定事件)→ PERFORM DISPLAY_ALV(顯示結果)。
進階版:把五個 PERFORM 改成五個 lcl_app=>method( ),就完成了 OO 重構。

11. 本地類別與全域類別(SE24):模組化的終點

前面四種手法都有各自的限制,OO 的方法(Method)把它們全部解決了:型別安全、參數唯讀保護、可回傳值、可繼承、可測試、可在 ABAP Cloud 使用。

本地類別(Local Class)全域類別(Global Class)
建立方式直接寫在報表/Include 內SE24 或 SE80/ADT
可見範圍只有本程式全系統
取代對象FORMFUNCTION MODULE
可否被 RFC 呼叫否(RFC 目前仍只能是 FM,需包一層 FM)

方法的五種參數

參數方向預設傳遞備註
IMPORTING傳入by reference方法內唯讀,編譯器強制
EXPORTING傳出by reference方法開頭自動被清空
CHANGING雙向by reference
RETURNING傳出必須 by value只能有一個;有了它方法可寫在運算式裡(functional call),且不能與 EXPORTING/CHANGING 併用
RAISING宣告本方法可能拋出的 class-based exception

要改成傳值,在參數名外面包 VALUE( ),例如 IMPORTING VALUE(iv_x) TYPE i大內表請務必用 by reference(預設),用 VALUE 會整份複製,效能災難。

*--- SE24 建的全域類別,取代 ZFM_CASPER_VBAK_VBAP -------------------*
CLASS zcl_sd_order DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES ty_t_vbap TYPE STANDARD TABLE OF vbap WITH EMPTY KEY.

    METHODS constructor
      IMPORTING iv_vbeln TYPE vbeln_va
      RAISING   zcx_sd_order_not_found.        " 建構失敗直接拋例外

    METHODS get_header  RETURNING VALUE(rs_vbak) TYPE vbak.
    METHODS get_items   RETURNING VALUE(rt_vbap) TYPE ty_t_vbap.
    METHODS get_net_amount RETURNING VALUE(rv_amt) TYPE netwr.

  PRIVATE SECTION.
    DATA ms_vbak TYPE vbak.
    DATA mt_vbap TYPE ty_t_vbap.
ENDCLASS.

CLASS zcl_sd_order IMPLEMENTATION.

  METHOD constructor.
    DATA(lv_vbeln) = CONV vbeln_va( |{ iv_vbeln ALPHA = IN }| ).

    SELECT SINGLE vbeln, erdat, auart, vkorg, kunnr, netwr, waerk
      FROM vbak WHERE vbeln = @lv_vbeln
      INTO CORRESPONDING FIELDS OF @ms_vbak.

    IF sy-subrc <> 0.
      " 用訊息類別產生例外,訊息文字仍可在 SE91 維護
      RAISE EXCEPTION TYPE zcx_sd_order_not_found
        MESSAGE e001(zmessage) WITH lv_vbeln.
    ENDIF.

    SELECT vbeln, posnr, matnr, arktx, kwmeng, vrkme, netwr
      FROM vbap WHERE vbeln = @lv_vbeln
      ORDER BY posnr
      INTO CORRESPONDING FIELDS OF TABLE @mt_vbap.
  ENDMETHOD.

  METHOD get_header.  rs_vbak = ms_vbak.  ENDMETHOD.
  METHOD get_items.   rt_vbap = mt_vbap.  ENDMETHOD.

  METHOD get_net_amount.
    " REDUCE 一行完成加總,取代 LOOP ... ADD
    rv_amt = REDUCE netwr( INIT s = CONV netwr( 0 )
                           FOR ls IN mt_vbap
                           NEXT s = s + ls-netwr ).
  ENDMETHOD.

ENDCLASS.
*--- 呼叫端:比 CALL FUNCTION 短、且是型別安全的 ---------------------*
REPORT zdemo_order.

PARAMETERS p_vbeln TYPE vbeln_va OBLIGATORY.

START-OF-SELECTION.
  TRY.
      DATA(lo_order) = NEW zcl_sd_order( p_vbeln ).

      cl_demo_output=>write( lo_order->get_header( ) ).
      WRITE: / |項目數:{ lines( lo_order->get_items( ) ) }|,
             / |淨值合計:{ lo_order->get_net_amount( ) NUMBER = USER }|.

    CATCH zcx_sd_order_not_found INTO DATA(lx_nf).
      MESSAGE lx_nf->get_text( ) TYPE 'E'.
  ENDTRY.
用 SE24 做 refactor 的實務步驟
1. 先在原報表裡把 FORM 一個一個搬成本地類別的方法(先能跑、行為不變)。
2. 把全域變數改成類別的私有屬性,用 constructor 或 setter 帶入。
3. 確認邏輯與 UI 分離(取資料的方法不要 WRITE/不要 CALL SCREEN)。
4. 在 SE24 建全域類別,把本地類別整段貼進去(SE24 → 功能表 Goto → Source Code-Based(原始碼模式),可以像寫 include 一樣整段編輯)。
5. 用 SE24 工具列的 Refactoring Assistant(重構助手) 把方法/屬性往上搬到父類別、或改名。ADT(Eclipse)的重構更強:Rename、Extract Method、Change Signature、Pull Up 都有。
6. 最後加 ABAP Unit 測試類別(FOR TESTING),這是 FORM 時代做不到的事。
在 SE24 建的類別,如果要讓 RFC 呼叫端(pyrfc、JCo、外部系統)使用,仍必須包一層 RFC-enabled Function Module 當門面,因為 RFC 協定只認得 FM。這也是 FM 到今天還沒被完全取代的主因之一(另一個是 Update FM 與 BAdI 的 filter FM)。

12. Class-Based Exception:三種基底類別與 TRY–CATCH–CLEANUP

FM 的傳統例外(RAISE <exception> + sy-subrc)有兩個問題:例外沒有型別、沒有訊息內容、忘了寫 OTHERS 就 dump。方法用的是 class-based exception:例外本身是一個物件,可以攜帶訊息、屬性、甚至前一個例外(previous)。

基底類別是否必須在 RAISING 宣告使用時機
CX_STATIC_CHECK必須,語法檢查會擋呼叫端「應該要處理」的業務錯誤(訂單不存在、金額不合法)。自建例外的預設首選
CX_DYNAMIC_CHECK不必宣告,但沒接到就 runtime error理論上不該發生、呼叫端可自行避免的錯(如 CX_SY_ZERODIVIDECX_SY_CONVERSION_NO_NUMBER
CX_NO_CHECK不必宣告,會自動往上層傳播無法在區域處理的系統性錯誤(如 CX_SY_NO_HANDLER、資源不足)

三者都繼承自 CX_ROOTCX_SY_* 是 SAP 的系統例外。

*--- 完整的 TRY / CATCH / CLEANUP 範例 ------------------------------*
DATA lo_file TYPE REF TO if_x.

TRY.
    " (1) 主邏輯
    DATA(lv_result) = lcl_calc=>divide( iv_a = 10 iv_b = lv_divisor ).
    DATA(lo_order)  = NEW zcl_sd_order( p_vbeln ).

  CATCH cx_sy_zerodivide INTO DATA(lx_zero).
    " (2) 抓特定例外
    MESSAGE |除數為零:{ lx_zero->get_text( ) }| TYPE 'I'.

  CATCH zcx_sd_order_not_found INTO DATA(lx_nf).
    MESSAGE lx_nf->get_text( ) TYPE 'I'.

  CATCH cx_root INTO DATA(lx_root).
    " (3) 最後防線:cx_root 一定要放最後,因為 CATCH 由上往下比對
    MESSAGE |未預期的錯誤:{ lx_root->get_text( ) }| TYPE 'E'.

  CLEANUP.
    " (4) 只有在「例外往外層傳播」時才執行,用來釋放資源
    "     注意:被本 TRY 的 CATCH 接住時,CLEANUP 不會執行
    CLEAR lo_file.

ENDTRY.
*--- 自建例外類別(SE24,繼承 CX_STATIC_CHECK)----------------------*
* 建立時勾選「with message class」,介面加 IF_T100_MESSAGE / IF_T100_DYN_MSG
* 之後就能把 SE91 的訊息當成例外文字使用

METHOD check_amount.
  IF iv_amt < 0.
    " 方式 1:綁 SE91 訊息(最常用,文字可翻譯)
    RAISE EXCEPTION TYPE zcx_amount_invalid
      MESSAGE e010(zmessage) WITH iv_amt.
  ENDIF.

  IF iv_amt > 9999999.
    " 方式 2:7.52+ 的簡寫,直接傳建構子參數
    RAISE EXCEPTION NEW zcx_amount_invalid( iv_amount = iv_amt ).
  ENDIF.

  " 方式 3:包住前一個例外,保留完整呼叫鏈(很重要,別把原因吃掉)
  TRY.
      lo_helper->do_something( ).
    CATCH cx_helper_error INTO DATA(lx_prev).
      RAISE EXCEPTION TYPE zcx_amount_invalid
        EXPORTING previous = lx_prev.
  ENDTRY.
ENDMETHOD.
怎麼把舊的 FM 例外轉成新的:在包裝方法裡把 sy-subrc 轉成例外 ——
IF sy-subrc <> 0. RAISE EXCEPTION TYPE zcx_x MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4. ENDIF.
這樣呼叫端就能統一用 TRY/CATCH,不必再記每支 FM 的 sy-subrc 代碼。
不要 CATCH cx_root 之後什麼都不做(空的 CATCH 區塊)。這是實務上最常見的災難:錯誤被吞掉,資料寫了一半沒人知道。至少要 log 或 MESSAGE。

13. 常用標準函數清單(背下來會省很多時間)

講義列的常用 FM + 實務上一定會遇到的補充。粗體=老師點名的

Function Module用途S/4HANA 現代替代
GUI_UPLOAD把前端檔案上載成內表cl_gui_frontend_services=>gui_upload( )
GUI_DOWNLOAD把內表下載成前端檔案cl_gui_frontend_services=>gui_download( )
WS_FILENAME_GET取得檔案名稱(開檔對話框)cl_gui_frontend_services=>file_open_dialog( ) / file_save_dialog( )
CLOI_PUT_SIGN_IN_FRONT把負號從數字後面搬到前面(SAP 預設負號在後)字串範本 |{ lv_amt SIGN = LEFT }|
CONVERSION_EXIT_ALPHA_INPUT數字串前補 0(顯示值 → 內部值)|{ lv_x ALPHA = IN }|
CONVERSION_EXIT_ALPHA_OUTPUT去掉前導 0(內部值 → 顯示值)|{ lv_x ALPHA = OUT }|
POPUP_TO_CONFIRM是/否確認視窗,回傳 ANSWER = '1'/'2'/'A'仍用它;Fiori 環境改用前端對話框
POPUP_TO_DECIDE / POPUP_TO_INFORM多選項決定視窗/訊息視窗
F4IF_INT_TABLE_VALUE_REQUEST用內表自製 F4 搜尋說明仍是標準做法(配 AT SELECTION-SCREEN ON VALUE-REQUEST
F4IF_FIELD_VALUE_REQUEST用 DDIC 欄位叫出標準 F4
SO_NEW_DOCUMENT_ATT_SEND_API1寄送含附件的郵件CL_BCScl_bcs=>create_persistent( )
NUMBER_GET_NEXT依號碼範圍物件(SNRO/SNUM)取下一號仍是標準做法;必須先 COMMIT 才不會跳號
NUMBER_RANGE_INTERVAL_LIST查號碼範圍區間
DATE_CONVERT_TO_FACTORYDATE依工廠行事曆換算工作日
FIMA_DAYS_AND_MONTHS_AND_YEARS兩日期間的天/月/年數ABAP SQL 的 DATS_DAYS_BETWEEN( )
LAST_DAY_OF_MONTHS某日期所在月的最後一天算術:下月 1 日 - 1
RP_CALC_DATE_IN_INTERVAL日期加減天/月/年ABAP SQL 的 DATS_ADD_DAYS( )DATS_ADD_MONTHS( )
HR_HK_DIFF_BT_2_DATES兩日期差(年月日)
RFC_READ_TABLE遠端讀任一資料表(外部系統常用)建議自建 RFC FM 或用 OData/CDS,RFC_READ_TABLE 有欄位長度 512 限制
BAPI_MATERIAL_GET_ALL讀取物料主檔全部視圖S/4 建議 BAPI_MATERIAL_GET_DETAIL 或 CDS I_Product
CURRENCY_CONVERTING_FACTOR取幣別小數位換算係數
CONVERT_TO_LOCAL_CURRENCY依匯率換算幣別ABAP SQL 的 CURRENCY_CONVERSION( )
REUSE_ALV_FIELDCATALOG_MERGE / REUSE_ALV_GRID_DISPLAY舊式 Function ALV改用 CL_SALV_TABLE
SAPGUI_PROGRESS_INDICATOR顯示執行進度條
ENQUEUE_/DEQUEUE_E<table>鎖/解鎖物件(SE11 Lock Object 產生)
*--- 兩個最常用的實戰片段 -------------------------------------------*

* (1) 確認視窗
CALL FUNCTION 'POPUP_TO_CONFIRM'
  EXPORTING titlebar              = '確認'
            text_question         = |確定要更新 { lines( lt_data ) } 筆資料嗎?|
            text_button_1         = '是'
            text_button_2         = '否'
            default_button        = '2'
            display_cancel_button = abap_true
  IMPORTING answer                = DATA(lv_answer)
  EXCEPTIONS text_not_found = 1
             OTHERS         = 2.
IF lv_answer <> '1'.
  MESSAGE '使用者取消' TYPE 'S' DISPLAY LIKE 'W'.
  RETURN.
ENDIF.

* (2) 自製 F4:把內表當成搜尋說明
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_matnr.
  SELECT matnr, maktx FROM makt
    WHERE spras = @sy-langu
    INTO TABLE @DATA(lt_f4)
    UP TO 500 ROWS.

  CALL FUNCTION 'F4IF_INT_TABLE_VALUE_REQUEST'
    EXPORTING retfield        = 'MATNR'
              dynpprog        = sy-repid
              dynpnr          = sy-dynnr
              dynprofield     = 'P_MATNR'
              value_org       = 'S'
    TABLES    value_tab       = lt_f4
    EXCEPTIONS parameter_error = 1
               no_values_found = 2
               OTHERS          = 3.
ABAP Cloud 的釋出(Released)API 概念:在 S/4HANA Cloud 與 BTP ABAP Environment,只有 SAP 標記為 Released for Cloud Development 的 FM/類別才能呼叫。GUI_UPLOAD、GUI_DOWNLOAD、POPUP_TO_CONFIRM、REUSE_ALV_* 等 SAP GUI 相依的 FM 全部不在名單內。判斷方式:在 ADT 中查看物件的 API State,或用 SE84 → Released Objects。On-premise(S/4HANA 1809~2023)用傳統寫法仍可以,但寫新程式時要有意識。

14. 日期與時間運算(講義第 10 節)

ABAP 的日期型別 d為單位做加減,時間型別 t為單位。搭配字元偏移(offset)就能做出大部分需求,不一定要呼叫 FM。

*--- 純 ABAP 寫法(不必呼叫 FM,效能最好)--------------------------*
DATA(lv_today) = sy-datum.                          " 伺服器當前日期
WRITE / |今天:{ lv_today DATE = USER }|.

" 本月第一天:字元偏移,把日部分設成 01
DATA(lv_first) = lv_today.
lv_first+6(2) = '01'.
WRITE / |本月 1 日:{ lv_first DATE = USER }|.

" 上月最後一天=本月第一天 - 1
DATA(lv_prev_last) = CONV d( lv_first - 1 ).

" 本月最後一天=下月第一天 - 1
DATA(lv_next_first) = CONV d( |{ lv_today+0(6) }01| ).
lv_next_first = lv_next_first + 45.                 " 先跳到下個月的某天
lv_next_first+6(2) = '01'.
DATA(lv_last) = CONV d( lv_next_first - 1 ).
WRITE / |本月最後一天:{ lv_last DATE = USER }|.

*--- 時間 ---------------------------------------------------------*
DATA(lv_time) = sy-uzeit.                           " 伺服器當前時間
DATA lv_t TYPE t.
lv_t = '000000'.
lv_t = lv_t + 60 * 60.                              " 加一小時 → 010000
lv_t+0(2) = '02'.                                   " 直接把小時改成 02

*--- ABAP SQL 內建日期函數(HANA 下推,處理大量資料時最快)---------*
SELECT belnr, budat,
       dats_days_between( budat, @sy-datum )  AS days_open,
       dats_add_months( budat, 1 )            AS next_month,
       dats_add_days( budat, 30 )             AS due_date
  FROM bkpf
  WHERE bukrs = @p_bukrs
    AND gjahr = @p_gjahr
  INTO TABLE @DATA(lt_aging).

對應的傳統 FM 寫法(講義原文):

* 10-1 兩日期間的天/月/年數
CALL FUNCTION 'FIMA_DAYS_AND_MONTHS_AND_YEARS'
  EXPORTING i_date_from = '20080101'
            i_date_to   = '20090508'
  IMPORTING e_days      = DATA(lv_days)
            e_months    = DATA(lv_months)
            e_years     = DATA(lv_years).

* 10-2 某月最後一天
CALL FUNCTION 'LAST_DAY_OF_MONTHS'
  EXPORTING  day_in            = lv_first
  IMPORTING  last_day_of_month = DATA(lv_lastday)
  EXCEPTIONS day_in_no_date    = 1
             OTHERS            = 2.

* 10-3 日期加減
CALL FUNCTION 'RP_CALC_DATE_IN_INTERVAL'
  EXPORTING date      = lv_lastday
            days      = 1
            months    = 0
            years     = 0
            signum    = '+'
  IMPORTING calc_date = DATA(lv_calc).
ABAP Cloud 不能用 sy-datumsy-uzeit(它們是使用者時區的值,語意不明確)。改用:
cl_abap_context_info=>get_system_date( )get_system_time( )get_user_time_zone( )get_user_technical_name( )(取代 sy-uname)。

15. 超級 Debug 模式 /H:追出程式用了哪些資料表

講義第 6 節這一招非常實用:第一次上課時我們想盡辦法要找到「畫面上的欄位來自哪張表」,學會 ABAP 之後只要用 /H 就能直接看。

  1. 進入交易碼(例如 VA03)。
  2. 在指令欄輸入 /H → Enter,畫面出現「Debugging switched on」。
  3. 再按 Enter 執行下一個動作,就會進入 Debugger。
  4. F9(設中斷點)→ 選 Breakpoint at Statement → 輸入 SELECT
  5. F8 執行,每遇到一句 SELECT 就停下來,看它讀哪一張表。
  6. F5(單步)、F6(略過副程式)、F7(執行完目前副程式返回)、F8(繼續)搭配追蹤。
  7. 查完記得取消中斷點(Breakpoints 頁籤 → 刪除),否則之後每支程式都會停。
其他常用的 Debugger 中斷點類型(Breakpoint at …):
Statement(如 SELECT、CALL FUNCTION、MESSAGE、AUTHORITY-CHECK)/Function Module(追某支 FM 被誰呼叫)/MethodSubroutineException(抓 dump 的來源)。
Watchpoint(監視點):對某個變數設條件,「當 sy-subrc <> 0」或「當 lv_matnr = '123'」時才停;若不指定條件,則該變數值一被改變就停下來。這是抓「值被誰改掉了」的唯一有效手段。
Debug 也可以事後補勾:進了 Debugger 才決定要不要進某段程式。
兩種 Debugger:Classic Debugger(同一個視窗,功能少)與 New Debugger(獨立視窗、多頁籤、可看物件與內表全貌)—— 在 Debugger 內 Settings 可切換。
作業(第 6 節):找出 VA03 用了多少個資料表?(抓到感覺即可)
提示:VA03 → /H → Enter → F9 → SELECT → F5/F6/F7/F8 → 取消斷點。
會看到 VBAK、VBAP、VBUK/VBUP(S/4 已併入 VBAK/VBAP 的狀態欄位)、VBEP、VBKD、KONV(S/4 改為 PRCD_ELEMENTS)、KNA1、MARA、TVAK… 幾十張表。這個練習的目的是體會「一個標準交易背後有多複雜」。

16. SE91 Message:模組化的錯誤訊息管理

寫死在程式裡的中文字串無法翻譯、無法統一維護。SE91 的訊息類別(Message Class)就是「訊息的模組化」。

類型名稱行為顯示位置
IInformation使用者確認後,程式從 MESSAGE 敘述之後繼續對話框
SStatus程式正常繼續下一個畫面的狀態列
WWarning視程式情境,出現錯誤對話框、程式停止或終止,使用者須採取行動狀態列
EError視程式情境,程式停止或終止,使用者須採取行動狀態列或對話框
ATermination使用者確認後程式終止,控制權回到上層區域功能表對話框
XExit程式以 short dump 終止,不顯示訊息;dump 內含訊息 ID
REPORT zmessage MESSAGE-ID zmessage.        " 宣告預設訊息類別

* (1) 用預設訊息類別,&1~&4 由 WITH 填入
MESSAGE s003 WITH '第01組' '第02組' '第03組'.

* (2) 明確指定訊息類別
MESSAGE i006(zmessage) WITH lv_msg1 lv_msg2 lv_msg3.

* (3) 完整動態寫法(呼叫 FM/BAPI 後標準的錯誤轉呈)
CALL FUNCTION 'Z_SOMETHING'.
IF sy-subrc <> 0.
  MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
          WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
ENDIF.

* (4) 最簡易的即興訊息(不必建 SE91,適合開發期)
MESSAGE |{ lv_ok_cnt } 筆,更新成功!| TYPE 'I'.

* (5) 改變顯示方式:用 S 型別但長得像錯誤,不中斷流程
MESSAGE '資料不存在' TYPE 'S' DISPLAY LIKE 'E'.

* (6) 把訊息收進 BAPIRET2 內表,最後一次顯示(批次處理標準做法)
MESSAGE e010(zmessage) WITH lv_matnr INTO DATA(lv_dummy).
APPEND VALUE bapiret2( type = sy-msgty id = sy-msgid number = sy-msgno
                       message_v1 = sy-msgv1 message_v2 = sy-msgv2
                       message    = lv_dummy ) TO lt_return.
MESSAGE TYPE 'E' 的行為會隨情境改變:在 START-OF-SELECTION 中它會終止程式並回選擇畫面;在 AT SELECTION-SCREEN 中它會停在該欄位讓使用者改;在迴圈中用 E 會直接中斷整批處理 —— 批次程式請改用「收進 BAPIRET2 內表,跑完再一次顯示」的做法。
Update FMRFC FM 內用 E 更危險(見第 8 節)。
練習 09-3:把 Z202202_26 改寫成用 MESSAGE,只需改兩個地方 ——
1. REPORT z202202_26_000 MESSAGE-ID zmessage.(在 REPORT 行加訊息類別)
2. 把原本的 WRITE 改成 MESSAGE s000 WITH 'ZADD'.

17. 綜合演練:把一支千行報表拆成三層

這是本章所有觀念的收斂。同一支報表用三種模組化程度呈現,你可以看出「重構」實際上在做什麼。

*&--- 第 0 級:全部寫在一起(初學者的樣子,不要留在正式系統)------*
REPORT z_flat.
PARAMETERS p_werks TYPE werks_d.
SELECT matnr, werks, maktx FROM ... INTO TABLE @DATA(lt).
LOOP AT lt INTO DATA(ls). ... ENDLOOP.
cl_salv_table=>factory( ... ).

*&--- 第 1 級:INCLUDE + FORM(講義的目標,可維護)-----------------*
REPORT z_form_level.
INCLUDE z_form_level_top.      " 宣告
INCLUDE z_form_level_f01.      " FORM
START-OF-SELECTION.
  PERFORM get_data.
  PERFORM display_alv.

*&--- 第 2 級:本地類別(S/4 建議的最低標準)-----------------------*
REPORT z_oo_level.
INCLUDE z_oo_level_top.
INCLUDE z_oo_level_c01.        " CLASS DEFINITION
INCLUDE z_oo_level_f01.        " CLASS IMPLEMENTATION
START-OF-SELECTION.
  DATA(lo_app) = NEW lcl_app( iv_werks = p_werks ).
  TRY.
      lo_app->run( ).
    CATCH zcx_app_error INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.
*&--- 第 3 級:全域類別(SE24)+ 薄報表,邏輯可被別人重用 ---------*
* 報表只剩「收參數、呼叫、顯示」三件事
REPORT z_clean.

PARAMETERS: p_werks TYPE werks_d OBLIGATORY.
SELECT-OPTIONS s_matnr FOR ('MARA-MATNR').

START-OF-SELECTION.
  TRY.
      " 所有業務邏輯都在 SE24 的全域類別裡,可被其他報表、FM、RAP 重用
      DATA(lo_srv) = NEW zcl_mm_stock_report( iv_werks = p_werks ).
      DATA(lt_out) = lo_srv->get_stock( it_matnr = s_matnr[] ).

      cl_salv_table=>factory(
        IMPORTING r_salv_table = DATA(lo_alv)
        CHANGING  t_table      = lt_out ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->get_functions( )->set_all( abap_true ).
      lo_alv->display( ).

    CATCH zcx_mm_error cx_salv_msg INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.
老師的第 11 節思考題:「您將如何累積自己的工作經驗?」
建議做法:把每次寫過的通用邏輯(日期計算、檔案上下載、訊息收集、ALV 顯示、Excel 匯出)整理成自己的 Function Group(例如 Z_FG_000)或全域類別(ZCL_UTIL_*),並且寫上註解與範例。三年後你會發現,別人還在複製貼上,你已經有一整套工具箱。這正是「模組化」對個人職涯的意義。

練習與自我檢核

  1. 第 28 個練習:建立 Function Group Z_FG_000 與 Function Module ZFM_CASPER_VBAP,輸入 I_VBELN,輸出 E_VBAK(結構)與 T_VBAP(表型別),找不到時 RAISE VBAK_NOT_EXIST。再寫報表 Z202202_28 呼叫它。
    檢核點:Function Group 與 FM 都要 ACTIVE;輸入 40 要能用 ALPHA 轉成 0000000040;EXCEPTIONS 一定要補 OTHERS。
  2. 建立 ZFM_GET_EBELP:輸入 I_EBELNI_EBELP,回傳最近一筆 EKKO/EKPO/EKES。
    檢核點:「最近一筆」一定要 ORDER BY;sy-subrc 要在 SELECT 之後立刻檢查,不要被 LOOP 覆寫。
  3. 寫三支小程式(或一支程式三個 FORM)分別示範 call by referencecall by valuecall by value and return result,用 WRITE 印出主程式與副程式中的變數值,說明為什麼結果不同。
    答案要點:VALUE( ) 是影本;CHANGING VALUE( ) 在 ENDFORM 才回寫。
  4. 05-1:用 SE80 打開 SAPMV45A(VA03 主程式),找出 Macros 節點,列出三個 SAP 標準用的巨集名稱與用途。
  5. 把 ALV Field Catalog 的建立用 DEFINE add_col 寫一次,再用 VALUE lvc_t_fcat( ) 寫一次,比較行數與可讀性。
    提醒:講義 05-3 的教訓 —— 少放一個 SORT 用到的欄位就 DUMP,且巨集無法 debug。
  6. 7-3:把 Z202202_26 改成 include 模式:Z00_26_TOP(宣告)+Z00_26_FORM(副程式),主程式依序 PERFORM GET_DATA / LAYOUT / FIELDCAT / EVENT / DISPLAY_ALV進階:再把五個 FORM 改成本地類別方法。
  7. 第 6 節作業:用 /H + F9(Breakpoint at Statement: SELECT)追出 VA03 讀了哪些資料表,至少列出 8 張。
  8. 09-3:用 SE91 建訊息類別 ZMESSAGE,把 Z202202_26 的輸出改成 MESSAGE,並示範 I/S/W/E/A 五種類型的行為差異。
  9. 10-6:試用 RFC_READ_TABLE 讀 MARA 的前 10 筆(注意 QUERY_TABLE、DELIMITER、FIELDS、OPTIONS 四個參數的用法,以及 512 字元的回傳限制)。
  10. 10-7:試用 BAPI_MATERIAL_GET_ALL 讀一個物料的完整視圖,並比較它與直接 SELECT MARA/MARC 的差異(權限檢查、單位換算、多語系描述)。
  11. 把第 1 題的 ZFM_CASPER_VBAP 重構成 SE24 全域類別 ZCL_SD_ORDER:用 constructor 帶入 VBELN、用 RETURNING 方法回傳表頭與明細、用自建 ZCX_SD_ORDER_NOT_FOUND(繼承 CX_STATIC_CHECK)取代 RAISE。
    檢核點:呼叫端改用 TRY/CATCH;例外要能帶出 SE91 的訊息文字。
  12. 思考題:CX_STATIC_CHECKCX_DYNAMIC_CHECKCX_NO_CHECK 三者差在哪?自建業務例外應該繼承哪一個?為什麼?
    答案要點:STATIC_CHECK 必須宣告在 RAISING、語法檢查會強迫呼叫端面對它,最適合業務錯誤。
  13. 思考題:為什麼 S/4HANA 到今天還不能完全廢掉 Function Module?
    答案要點:RFC 協定只認 FM、Update Task(V1/V2)只能是 FM、部分 BAdI/Enhancement 的介面仍是 FM、BAPI 本身就是 FM。
  14. 第 11 節:規劃你自己的工具箱 —— 列出五個你已經重複寫過兩次以上的邏輯,把它們設計成 ZCL_UTIL_* 的方法簽章(參數名稱、型別、例外)。

第 3-1 章 選擇畫面:PARAMETERS / SELECT-OPTIONS / SELECTION-SCREEN 全解

這一章把「使用者怎麼把條件交給你的程式」講到底:單值 PARAMETERS、複選 SELECT-OPTIONS 的四欄內表(SIGN/OPTION/LOW/HIGH)、SELECTION-SCREEN 的版面排版、七個選擇畫面事件的觸發順序、動態顯示與隱藏欄位、自製 F4 輔助說明、變式(Variant)與背景執行、以及用 SUBMIT 把條件丟給另一支程式。學完之後,你能做出一支「條件精準、畫面好看、錯誤擋得住、可存成變式排程」的正式報表,也會避開老師在專案上真的踩過的坑——條件用區間(BT)和用列舉(EQ)答案會不一樣

核心 TCODESE38SE80SE11SE16NSE84SE93SA38SE37SE63SM30SM36SM37STVARVSU3SE18SPAU_ENHF.01MB52
關鍵物件SSCRFIELDS、RSPARAMS、DDSHRETVAL、VRM_VALUES、TVARVC、TPARA、SCREEN、F4IF_INT_TABLE_VALUE_REQUEST、F4IF_FIELD_VALUE_REQUEST、DYNP_VALUES_READ、VRM_SET_VALUES、CL_GUI_FRONTEND_SERVICES、CL_SALV_TABLE、RFBILA00、RM07MLBS

1. 選擇畫面在 SAP 的定位

螢幕(Screen)是 ABAP 設計最重要的工作之一。SAP 的單據、主資料維護幾乎都靠螢幕,一支程式可以包含多個螢幕。螢幕上的 Dialog 就是使用者和程式之間的各種互動:輸入資料、選菜單、按按鈕、單擊或雙擊清單項目。

選擇畫面(Selection Screen)不需要你去 SE51 建 Screen Number——它是由原始碼中的宣告語句自動產生的「簡易型螢幕」。這是它最大的優點:

  • 簡單方便:幾行宣告就有一個可用的輸入畫面,不必畫 Layout、不必寫 PBO/PAI 模組。
  • 移植方便:畫面定義就在程式裡,跟著程式一起傳輸,不會漏傳 Screen。
  • 自動型別檢查:使用者輸入的值會自動與欄位型別比對。
  • 自動接上字典:用 DDIC 資料元素宣告,F1 直接顯示欄位文件、F4 直接叫出字典掛的搜尋說明。
  • 多語系文字:選擇文字(Selection Texts)可以多語維護,執行時自動用使用者的登入語言顯示。
  • 可存成變式(Variant):重複使用,也是背景排程的必要條件。
標準選擇畫面的螢幕編號固定是 1000sy-dynnr = '1000')。用 SELECTION-SCREEN BEGIN OF SCREEN n 自行定義的則從 1001 開始由你指定。這個編號在寫 F4 輔助說明(要傳 DYNPNR)時會用到。
老師的定位很清楚:「簡單,但是有很多要注意的技巧;另外條件變式的善用技巧也要能掌握。」 這一章八成的內容都在講那些「技巧」。

2. PARAMETERS:單值輸入的完整語法

PARAMETERS 產生一個單值輸入框,它同時也是程式裡的一個普通變數,可以直接拿來運算、直接寫進 WHERE 條件(用 =)。

PARAMETERS para[(len)] | para [LENGTH len]
  [TYPE type [DECIMALS dec]] | [LIKE dobj]
  [DEFAULT val]
  [OBLIGATORY] [NO-DISPLAY] [LOWER CASE] [VALUE CHECK]
  [AS CHECKBOX] | [RADIOBUTTON GROUP grp] | [AS LISTBOX VISIBLE LENGTH vlen]
  [VISIBLE LENGTH vlen]
  [MEMORY ID pid] [MATCHCODE OBJECT sh] [MODIF ID mid] [USER-COMMAND ucom]
  [AS SEARCH PATTERN] [NO INTERVALS].
附加語法作用注意事項 / 老師提醒
TYPE t / LIKE dobj指定型別。TYPE 接型別(DDIC 資料元素、表欄位、內建型別);LIKE 接既有資料物件新程式一律用 TYPE。用 DDIC 型別才會自動有 F1 文件與 F4 說明
DEFAULT val設定預設值不能用 VALUE!這是 PARAMETERS 與 DATA 最容易混淆的一點
OBLIGATORY必填,欄位左邊出現打勾符號,沒填不讓執行與 NO-DISPLAY 互斥
NO-DISPLAY不顯示在畫面上,但仍是選擇畫面的一部分可用 SUBMIT ... WITH 或變式傳值進來,是「隱藏開關」的標準做法。加了 NO-DISPLAY 之後型別限制放寬(可以用 STRING 等)
LOWER CASE保留小寫,不自動轉大寫檔名、E-mail、密碼、URL 一定要加。忘了加是最常見的「查不到資料」原因之一
AS CHECKBOX核取方塊,型別固定為 CHAR1('X' / space)判斷用 IF p_flag = abap_true.
RADIOBUTTON GROUP g單選鈕,同 group 只能選一個,型別 CHAR1群組名最長 4 碼;一組至少要兩個;要用 DEFAULT 'X' 指定預設選哪個,否則預設是第一個
AS LISTBOX VISIBLE LENGTH n下拉清單,n 是顯示寬度值從 Domain 固定值/檢查表自動來,或用 FM VRM_SET_VALUES 自己灌
VISIBLE LENGTH n只縮短顯示寬度,內部長度不變常配 TYPE STRING 使用(STRING 不加這個會佔滿整行)
MATCHCODE OBJECT sh指定 SE11 的搜尋說明(Search Help)覆寫資料元素原本掛的搜尋說明
MEMORY ID pid與 SAP 記憶體(SPA/GPA)掛勾,進畫面自動 GET、執行時自動 SET傳統上用 3 碼(MAT、WRK、BUK)。ID 要先存在於 TPARA,否則語法檢查會出警告
MODIF ID mid把欄位歸到一個群組(最長 3 碼),供 LOOP AT SCREEN 判斷 screen-group1動態顯示/隱藏/變灰的關鍵,見第 13 節
USER-COMMAND ucom只能給 CHECKBOX / RADIOBUTTON / LISTBOX 用;勾選時觸發 PAI,畫面重繪功能碼由 sscrfields-ucomm 讀取。沒有它,勾了 checkbox 畫面不會有反應
VALUE CHECK對 DDIC 檢查表 / 固定值做輸入檢查只對有檢查表的 DDIC 型別有效
AS SEARCH PATTERN當作主鍵構成的搜尋樣式欄位搭配邏輯資料庫使用,現在很少用
NO INTERVALS在 block 內對齊時壓縮空間常放在 BEGIN OF BLOCK 上,讓整個框架的欄位不留區間位
兩個硬性限制記牢
1. PARAMETERS / SELECT-OPTIONS 的名稱不得超過 8 個字元(含 P_、S_ 前綴)。取名 P_WERKSXX 就會語法錯誤。
2. 預設值只能用 DEFAULT,不能用 VALUE;VALUE 是 DATA 的語法。
命名慣例:PARAMETERS 用 P_ 開頭、SELECT-OPTIONS 用 S_ 開頭、RADIOBUTTON 用 R_ 開頭。這不是語法規定,但全世界的 ABAPer 都這樣寫,維護的人一眼就看得懂。

3. 第 36 個練習:各種 PARAMETERS 控制項一次做完

講義的 Z202202_36 把所有控制項排在一起,是最好的速查範本。這裡用 S/4HANA 的現代寫法重寫(把 (5) TYPE C 這種舊式長度宣告換成 DDIC 型別,並補上實務上一定會加的附加語法):

REPORT z202202_36.

" 1. 一般文字框 + 預設值 + 掛 SAP 記憶體
PARAMETERS p_p1 TYPE char5 DEFAULT 'CASP' MEMORY ID zp1.

" 2. 核取方塊(型別固定 CHAR1),預設打勾,勾選時觸發畫面重繪
PARAMETERS p_p2 AS CHECKBOX DEFAULT abap_true USER-COMMAND flag.

" 3. 必填欄位
PARAMETERS p_p3 TYPE char5 OBLIGATORY.

" 4. 單選鈕群組(同一 GROUP 只能擇一),明確指定預設值
PARAMETERS p_p4 RADIOBUTTON GROUP g1 DEFAULT 'X'.
PARAMETERS p_p5 RADIOBUTTON GROUP g1.

" 5. STRING 型別一定要用 VISIBLE LENGTH,否則畫面會被撐開
PARAMETERS p_p6 TYPE string VISIBLE LENGTH 20 LOWER CASE.

" 6. 下拉清單(值要另外灌,見下一節)
PARAMETERS p_p7 TYPE char5 AS LISTBOX VISIBLE LENGTH 10.

" 7. 指定 SE11 的搜尋說明;USER_COMP 是「系統使用者」的標準搜尋說明
PARAMETERS p_p8 TYPE char12 MATCHCODE OBJECT user_comp.

" 8. 物料號碼:用 DDIC 型別宣告就自動有 F4(MAT1 搜尋說明)與 F1 文件
PARAMETERS p_matnr TYPE mara-matnr.

" 9. 隱藏參數:畫面看不到,但可以被 SUBMIT ... WITH 或變式塞值進來
PARAMETERS p_test TYPE abap_bool NO-DISPLAY.

START-OF-SELECTION.
  WRITE: / '單選鈕 P_P4 =', p_p4, / '核取方塊 P_P2 =', p_p2.
練習(講義第 36 練習的延伸題):做一個 P_MATNR 可以下拉選擇物料編號
兩條路:(a) 直接宣告 TYPE mara-matnr,字典上掛的搜尋說明 MAT1 會自動生效,按 F4 就有;(b) 若要自訂清單(例如只列本廠有庫存的料號),走第 14 節的 F4IF_INT_TABLE_VALUE_REQUEST
TYPE c LENGTH 5 這種「純技術型別」宣告出來的欄位,沒有 F1 說明、沒有 F4 搜尋、沒有轉換常式(Conversion Exit)。物料號碼若用 CHAR40 自己宣告,畫面輸入 100 不會自動補成 000000000000000100,之後查 MARA 一定查不到。能用 DDIC 型別就用 DDIC 型別,這是 S/4 上最容易被忽略的地雷。

4. AS LISTBOX 怎麼灌值,MATCHCODE 的搜尋說明怎麼找

LISTBOX 若欄位的 Domain 有固定值(Fixed Values)或檢查表,值會自動帶出來;否則要自己用 VRM_SET_VALUES 灌,時機放在 INITIALIZATIONAT SELECTION-SCREEN OUTPUT

REPORT z_listbox_demo.

TYPE-POOLS vrm.                       " 提供 vrm_values / vrm_id 型別

PARAMETERS p_type TYPE char4 AS LISTBOX VISIBLE LENGTH 20 OBLIGATORY.

INITIALIZATION.
  " 固定清單:key 是實際存到變數的值,text 是畫面顯示的字
  DATA(lt_list) = VALUE vrm_values(
      ( key = 'FERT' text = '成品 Finished Product' )
      ( key = 'HALB' text = '半成品 Semi-finished' )
      ( key = 'ROH'  text = '原料 Raw Material' ) ).

  CALL FUNCTION 'VRM_SET_VALUES'
    EXPORTING
      id     = 'P_TYPE'               " 必須大寫,且與參數名完全相同
      values = lt_list
    EXCEPTIONS
      id_illegal_name = 1
      OTHERS          = 2.

若清單內容要從資料庫來(例如物料類型描述檔 T134T),就改成先 SELECT 再組:

INITIALIZATION.
  SELECT mtart, mtbez
    FROM t134t
    WHERE spras = @sy-langu
    INTO TABLE @DATA(lt_mtart).

  DATA(lt_values) = VALUE vrm_values(
      FOR ls IN lt_mtart ( key = ls-mtart text = |{ ls-mtart } { ls-mtbez }| ) ).

  CALL FUNCTION 'VRM_SET_VALUES'
    EXPORTING id = 'P_TYPE' values = lt_values.
講義的思考題:「當使用 S/4HANA 時,不像 ECC 能直接找出 Search Help 的名稱,那要如何找得到 USER_COMP?」(依講義提示整理)
四條實用路線:
1. 在任一有該欄位的標準畫面上按 F1 → 技術資訊(Technical Information),畫面下方會列出「搜尋說明」;若 GUI 沒顯示,在說明視窗的設定裡把 Technical Information 打開。
2. SE11 開該欄位的資料元素 → 「更多特性 / Further Characteristics」頁籤 → 看 Search Help 欄位;或看它的檢查表(Check Table),檢查表本身就會產生隱式搜尋說明。
3. SE11Search help → 名稱輸入 *USER* 按 F4 模糊搜尋。
4. SE84(Repository Information System)→ ABAP Dictionary → Search Helps,可用描述文字反查。
搜尋說明的層級順序(由高到低):畫面上的 MATCHCODE OBJECT > 資料元素上掛的 Search Help > 檢查表產生的隱式 Search Help。要換掉標準的 F4,最省事的就是在 PARAMETERS 上加 MATCHCODE OBJECT。

5. SELECT-OPTIONS:四欄內表 SIGN / OPTION / LOW / HIGH

PARAMETERS 只能給一個值,實務上使用者要的是「這批料號」「這段日期」「排除這幾張單」。這就是 SELECT-OPTIONS。

SELECT-OPTIONS sel FOR dobj.   " dobj 必須是已宣告的資料物件或表欄位

執行後,使用者輸入的內容會傳到一個自動產生的內表,這個內表的結構永遠是這四欄:

欄位值域意義
SIGNI / EI = Include 包含E = Exclude 排除不可以留空白
OPTIONEQ NE GT GE LT LE BT NB CP NP比較運算子,見下表
LOW維護項目-前(單值或區間下限)
HIGH維護項目-後(區間上限,只有 BT / NB 才填)
OPTION意義對應 SQL範例(欄位 MATNR)
EQ等於=MATNR = 'DXTR1000'
NE不等於<>MATNR <> 'DXTR1000'
GT大於>MATNR > 'DXTR1000'
GE大於等於>=MATNR >= 'DXTR1000'
LT小於<MATNR < 'DXTR1000'
LE小於等於<=MATNR <= 'DXTR1000'
BT介於(含頭含尾)BETWEENMATNR BETWEEN 'DXTR1000' AND 'DXTR2000'
NB不介於NOT BETWEENMATNR NOT BETWEEN ...
CP模式比對LIKEMATNR LIKE 'DXTR1*'(* 任意字串、+ 任意單一字元、# 逸出)
NP模式不比對NOT LIKEMATNR NOT LIKE 'DXTR1*'
老師強調:「光是這四個欄位就能變化出無窮的條件式,非常靈活、非常強大。善用之,能好好教導前端使用者。」 很多客戶抱怨「報表不好用」,其實是沒人教他們按右邊那顆「多重選擇」按鈕(黃綠色箭頭),可以一次貼上幾百個料號、可以設排除。
Debug 觀察法(老師特別交代一定要會):s_s1 ~ s_s7 每一個都是獨立的內表。在 Debug 時,如果結構(表頭列)與內表同名,要用 s_s1[] 才看得到內表本體;變數旁邊有小帽子(hat)圖示就代表它是 WITH HEADER LINE。可以在程式中直接寫 BREAK-POINT. 或用 BREAK-POINT ID,實務上更常用 BREAK sy-uname.(只對自己中斷,不會影響其他使用者)。

6. 第 37 個練習:SELECT-OPTIONS 的完整附加語法

附加語法作用
DEFAULT l單一預設值(自動 SIGN='I' OPTION='EQ')
DEFAULT l OPTION op SIGN s含判斷條件的單一預設值
DEFAULT l TO h預設區間(自動 SIGN='I' OPTION='BT')
DEFAULT l TO h OPTION op SIGN s預設區間+自訂條件(例如 NB / E)
OBLIGATORY必填,至少要輸入一列
NO-EXTENSION隱藏右邊的「多重選擇」按鈕,只能輸入一列(但仍可輸區間)
NO INTERVALS隱藏 HIGH 欄位,只能輸入單值(但仍可輸多列)
NO-DISPLAY不顯示,供 SUBMIT / 變式傳值
LOWER CASE保留小寫
MEMORY ID pid掛 SAP 記憶體(只作用在 LOW)
MATCHCODE OBJECT sh指定搜尋說明
MODIF ID mid群組代號,供 LOOP AT SCREEN 控制
VISIBLE LENGTH n顯示長度
NO DATABASE SELECTION搭配邏輯資料庫時,不參與 LDB 的自動選取

講義的 Z202202_37 把各種 DEFAULT 組合排出來對照,這是理解 SIGN / OPTION 最快的方法。注意:原講義用了 TABLES vbak. 來提供型別,S/4HANA 的現代寫法應改用一個 DATA 變數當「型別載體」

REPORT z202202_37.

" 現代寫法:不用 TABLES,宣告一個變數純粹當型別來源
DATA gv_vbeln TYPE vbak-vbeln.

SELECT-OPTIONS s_s1 FOR gv_vbeln.                                  " 空的:不限制
SELECT-OPTIONS s_s2 FOR gv_vbeln DEFAULT 44 TO 54.                 " I / BT / 44 / 54
SELECT-OPTIONS s_s3 FOR gv_vbeln DEFAULT 44 OPTION LE SIGN I.      " I / LE / 44
SELECT-OPTIONS s_s4 FOR gv_vbeln DEFAULT 45 TO 55 OPTION NB SIGN I." I / NB / 45 / 55
SELECT-OPTIONS s_s5 FOR gv_vbeln DEFAULT 45 TO 55 OPTION BT SIGN E." E / BT / 45 / 55
SELECT-OPTIONS s_s6 FOR gv_vbeln NO-EXTENSION.                     " 只能一列
SELECT-OPTIONS s_s7 FOR gv_vbeln NO INTERVALS.                     " 只能單值

START-OF-SELECTION.
  " 用 Debug 觀察 s_s1[] ~ s_s7[],確認它們都是四欄內表
  BREAK sy-uname.

  SELECT vbeln, erdat, ernam, netwr, waerk
    FROM vbak
    WHERE vbeln IN @s_s2                 " SELECT-OPTIONS 一律用 IN
    INTO TABLE @DATA(lt_vbak)
    UP TO 100 ROWS.
S/4HANA:不要再寫 TABLES:。TABLES 會產生一個全域的表工作區(等同 WITH HEADER LINE 的近親),在 ABAP Objects(類別/方法)裡根本不允許,也是 ATC 檢查的固定扣分項。SELECT-OPTIONS 只是需要一個「有型別的資料物件」,用 DATA gv_x TYPE vbak-vbeln. 完全等效,而且 F1 / F4 / 轉換常式全部保留。
NO INTERVALS 與 NO-EXTENSION 常被搞混
NO INTERVALS=拿掉「到(HIGH)」欄位 → 只能單值,但可以按多重選擇輸入很多列。
NO-EXTENSION=拿掉右邊那顆多重選擇按鈕 → 只能一列,但那一列還是可以填區間。
兩個一起加,才等於「只能填一個單值」。

7. RANGES 與自建 range 的正確填法

SELECT-OPTIONS 同時做兩件事:畫面元素 + 四欄內表。如果你只要那個四欄內表、不要畫面(例如程式內部自己組條件),那就是 RANGES。

SELECT-OPTIONSRANGES(舊)DATA ... TYPE RANGE OF(新,建議)
產生畫面欄位
產生四欄內表
WITH HEADER LINE是(無法避免)是(因此已過時)
可宣告在方法/類別內可以
可存進變式
" 舊寫法(講義時代):RANGES 會產生 header line,S/4 請避免
"RANGES r_matnr FOR mara-matnr.

" 現代寫法一:直接宣告 range 型別的內表
DATA lr_matnr TYPE RANGE OF mara-matnr.

" 現代寫法二:先定型別,方法簽章/FM 介面可重複使用
TYPES ty_r_matnr TYPE RANGE OF mara-matnr.
DATA  lr_werks   TYPE RANGE OF marc-werks.

" 用 VALUE # 一次組好(sign / option 可以提到括號外共用)
lr_matnr = VALUE #( sign = 'I' option = 'EQ'
                    ( low = 'DXTR1000' )
                    ( low = 'DXTR1001' )
                    ( low = 'DXTR1002' ) ).

" 需要區間就明確寫出 BT,並且 low / high 都要填
APPEND VALUE #( sign = 'I' option = 'BT'
                low = '20250101' high = '20251231' ) TO lr_datum.

" 由內表批次轉成 range(FOR 迴圈)
TYPES ty_r_kunnr TYPE RANGE OF kna1-kunnr.
DATA(lr_kunnr) = VALUE ty_r_kunnr(
    FOR ls IN lt_customer ( sign = 'I' option = 'EQ' low = ls-kunnr ) ).

如果要在程式裡「加料」到畫面上的 SELECT-OPTIONS(例如 INITIALIZATION 給預設條件),建議一律用 方括號 明確指定內表本體,避免踩到 header line:

INITIALIZATION.
  " 明確寫 s_matnr[],不依賴 header line
  APPEND VALUE #( sign = 'I' option = 'CP' low = 'DXTR1*' ) TO s_matnr[].
  APPEND VALUE #( sign = 'E' option = 'EQ' low = 'DXTR1205' ) TO s_matnr[].
自建 range 的五條鐵律(違反任何一條,SELECT 的結果都可能靜默出錯):
1. SIGN 一定要填 'I' 或 'E',大寫,不可空白。空白的 SIGN 在 ABAP SQL 的 IN 條件裡行為未定義。
2. OPTION 一定要填,而且要跟 HIGH 一致:填了 HIGH 就必須用 BT / NB;用 EQ/GT/LE… 時 HIGH 必須留空。填了 HIGH 又用 EQ,SAP 只看 LOW,HIGH 被默默忽略。
3. 空的 range = 不設限,會撈全表。所以做「使用者不填就不查」的邏輯要自己擋:IF lr_matnr IS INITIAL. MESSAGE ... TYPE 'E'. ENDIF.
4. E(排除)列是在 I(包含)列之後才套用:結果 =(所有 I 列的聯集)減去(所有 E 列的聯集)。若表中只有 E 列沒有 I 列,等於「全部資料扣掉排除的」——這常常是使用者以為只會查到幾筆、結果跑了十分鐘的原因。
5. low / high 要用資料庫的內部格式。MATNR、KUNNR、LIFNR、EQUNR 都有 ALPHA 轉換常式,畫面輸入 100、資料庫存的是 000000000000000100。自己塞值要先呼叫 CONVERSION_EXIT_ALPHA_INPUT(或用 |{ lv_matnr ALPHA = IN }|)。
" 自己塞料號時,務必補零成內部格式
DATA(lv_internal) = |{ lv_input ALPHA = IN }|.
APPEND VALUE #( sign = 'I' option = 'EQ' low = lv_internal ) TO lr_matnr.

" 反過來要顯示給人看時
WRITE / |{ ls_mara-matnr ALPHA = OUT }|.

8. 【重點陷阱】區間(BT)與列舉(EQ)的答案為什麼會不一樣

這是老師在真實專案上付出代價換來的經驗:同一批資料,條件用「區間」跟用「列舉」,跑出來的答案不一樣,而用列舉的那個才是對的。

結論先講:能列舉就列舉(I/EQ 一筆一筆列,或 I/CP 加 E/EQ 排除),不要圖省事寫 BT 區間。會踩雷的原因有五個,全部都是「靜默出錯」——不會 dump、不會報錯,只是數字錯了。

原因一:CHAR 欄位的 BT 是「字典順序」比較,不是數值比較。
SAP 裡幾乎所有代碼欄位(MATNR、VBELN、BELNR、KOSTL)都是字元型。字元比較是逐字元比的,所以:

  • 'DXTR1201' BT 'DXTR1210' 會把 DXTR12010、DXTR1205A、DXTR120X 這種「開頭相同但更長/帶字尾」的料號一併撈進來(因為它們排在中間)。
  • 反過來,'9' BT '100' 在字元世界裡是空集合(因為 '9' > '1'),使用者以為查了 9 到 100,結果一筆都沒有。

原因二:ALPHA 轉換常式讓邊界值對不上。
畫面上輸入是有做轉換的(1201 → 000000000000001201),但程式裡自己填 range 的 low/high 沒人幫你轉。用 EQ 列舉時,某一筆對不上就是那一筆查不到,很容易被發現;用 BT 時,邊界對不上會讓整段區間多抓或少抓,而且看起來還很合理。

原因三:實務上的號碼段幾乎不是連續的。
料號 DXTR1201~DXTR1210 中間,可能夾著別的廠、別的類別、甚至別的公司代碼的資料。BT 是「凡是排在這兩端之間的都算」,它不知道你的業務規則;列舉是「我點名的這幾筆才算」。只要編碼帶有業務意義,區間就一定會誤傷。

原因四:編碼規則會變。
今年的區間寫死 'A2025000001' TO 'A2025999999',明年前綴改成 B 就整段失效,而且一樣不報錯,只是報表變成 0 筆。列舉至少會在「找不到某筆」時被人發現。

原因五:BT 的 HIGH 是「含尾」,但只有在長度一致時才直覺。
日期、數量、金額型別沒問題;但字元欄位長度不一致時,BT 的「含尾」語意會跟人的直覺差很多。

所以老師在 ZOOALV 案例裡用的是這個模式:用 I/CP 圈範圍,再用 E/EQ 逐筆挖掉——既省手工,又精準:

" 正確示範:包含 DXTR1* 全部,再明確排除 DXTR1201 ~ DXTR1210 這十筆
INITIALIZATION.
  APPEND VALUE #( sign = 'I' option = 'CP' low = 'DXTR1*' ) TO s_matnr[].

  DO 10 TIMES.
    DATA(lv_num)   = 1200 + sy-index.
    DATA(lv_matnr) = |DXTR{ lv_num }|.
    APPEND VALUE #( sign = 'E' option = 'EQ' low = lv_matnr ) TO s_matnr[].
  ENDDO.

" 錯誤示範(不要學):看起來一樣,實際會誤殺 DXTR12010、DXTR1205A ...
"  APPEND VALUE #( sign = 'E' option = 'BT'
"                  low = 'DXTR1201' high = 'DXTR1210' ) TO s_matnr[].
條件類型SIGNOPTION範例老師評語
包含一整群'I''CP'DXTR1*圈範圍用這個,比 BT 安全
逐筆排除'E''EQ'DXTR1201 … DXTR1210正解:一筆一筆點名
區間排除'E''BT'DXTR1201 ~ DXTR1210會抓錯:長度/字尾不同的會被誤殺
老師口訣:「條件寧可囉唆,不要聰明。」 多寫十行 APPEND,換來答案永遠正確,這筆交易非常划算。真的必須用區間時(例如日期、會計期間、工廠這種格式固定的欄位),請確認:型別是 DATS/NUMC/定長 CHAR、且沒有轉換常式、且業務上確定是連續的。
延伸提醒:「換個寫法答案就不一樣」還有另一個常見兇手——SELECT 沒有排序。
同一筆業務資料在系統裡可能存在多種「角度」(例如成本估算就同時有 H=Item Category 項目類別E=Cost Component 成本組成 兩種構成,兩者的欄位數量還不一樣)。若程式用 SELECT SINGLE 或沒有 ORDER BY 就取第一筆,抓到 H 還是 E 完全看資料庫當下的心情,結果就會時對時錯、不穩定
正確做法是:在 WHERE 裡明確限定角度(例如固定只取 H),再加 ORDER BY 保證順序。「條件要精準」和「取值要有序」是同一個問題的兩面。

9. SELECTION-SCREEN:把畫面排漂亮

SELECTION-SCREEN 用來建立畫面的框架結構,包括螢幕元素的建立與子螢幕的建立。完整指令清單:

指令作用
BEGIN OF BLOCK b [WITH FRAME] [TITLE t] [NO INTERVALS]END OF BLOCK b建立一個(帶標題的)方框群組;最多可巢狀 5 層
BEGIN OF LINEEND OF LINE把多個元素排在同一行(此區間內元素不會自動換行)
SKIP [n]空 n 行
ULINE [/pos(len)]畫一條水平線
POSITION pos把游標移到第 pos 欄;也可用 POS_LOW / POS_HIGH 對齊選項欄位
COMMENT pos(len) name [FOR FIELD f] [MODIF ID m]放一段說明文字;FOR FIELD 讓這段文字的 F1 連到該欄位的說明
PUSHBUTTON pos(len) name USER-COMMAND uc [VISIBLE LENGTH n]畫面上的按鈕
FUNCTION KEY n在應用工具列加按鈕(n = 1~5,功能碼 FC01~FC05)
BEGIN OF SCREEN n [AS SUBSCREEN] [TITLE t]END OF SCREEN n自訂編號的選擇畫面/子畫面
BEGIN OF TABBED BLOCK t FOR n LINES / TAB (len) c USER-COMMAND uc DEFAULT SCREEN s頁籤區塊
INCLUDE BLOCKS b / PARAMETERS p / SELECT-OPTIONS s / COMMENT把別的選擇畫面的元素納進來重用
CALL SELECTION-SCREEN n [STARTING AT c l] [ENDING AT c l]執行時呼叫自訂選擇畫面;加上座標就變成彈出視窗

講義的 Z202202_38(第 38 個練習),示範兩個 BLOCK 加一個自訂畫面 1001:

REPORT z202202_38.

DATA gv_vbeln TYPE vbak-vbeln.      " 型別載體,取代舊寫法的 TABLES vbak

" 第一個方框
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001.
  PARAMETERS p_p7 TYPE vbak-vbeln.
  PARAMETERS p_p8 TYPE vbak-vbeln.
SELECTION-SCREEN END OF BLOCK b1.

" 第二個方框
SELECTION-SCREEN BEGIN OF BLOCK b2 WITH FRAME TITLE TEXT-002.
  PARAMETERS p_pa TYPE vbak-vbeln.
  PARAMETERS p_pb TYPE vbak-vbeln.
SELECTION-SCREEN END OF BLOCK b2.

" 自訂的第二張選擇畫面(編號自 1001 起)
SELECTION-SCREEN BEGIN OF SCREEN 1001 TITLE TEXT-t01.
  SELECTION-SCREEN COMMENT /1(30) TEXT-003.
  PARAMETERS p_p9 TYPE vbak-vbeln.
  PARAMETERS p_p0 TYPE vbak-vbeln.
SELECTION-SCREEN END OF SCREEN 1001.

START-OF-SELECTION.
  " 以彈出視窗方式呼叫;不加 STARTING AT 就是整頁顯示
  CALL SELECTION-SCREEN 1001 STARTING AT 10 5 ENDING AT 70 15.
  IF sy-subrc = 0.
    WRITE: / '使用者在 1001 輸入:', p_p9, p_p0.
  ENDIF.

把多個元素排在同一行、並用 COMMENT 當標籤,是實務上最常用的版面技巧:

SELECTION-SCREEN BEGIN OF BLOCK bsel WITH FRAME TITLE TEXT-001.

  " 同一行:單選鈕 + 文字 + 單選鈕 + 文字 + 文字 + 輸入框
  SELECTION-SCREEN BEGIN OF LINE.
    PARAMETERS       r_plant RADIOBUTTON GROUP g1 USER-COMMAND check.
    SELECTION-SCREEN COMMENT 03(29) TEXT-r11 FOR FIELD r_plant.   " All Plant Matl.
    PARAMETERS       r_user  RADIOBUTTON GROUP g1 DEFAULT 'X'.
    SELECTION-SCREEN COMMENT 35(15) TEXT-r12 FOR FIELD r_user.    " Particular Ver.
    SELECTION-SCREEN COMMENT 50(08) TEXT-r13.                     " User ID:
    PARAMETERS       p_usnam TYPE sy-uname.
  SELECTION-SCREEN END OF LINE.

  SELECTION-SCREEN ULINE.
  SELECTION-SCREEN SKIP 1.

  " 按鈕
  SELECTION-SCREEN PUSHBUTTON /1(20) btn_run USER-COMMAND run.

SELECTION-SCREEN END OF BLOCK bsel.
COMMENT 與 PUSHBUTTON 的 name 會被系統自動宣告成一個字元變數(長度就是括號裡的 len),所以你可以在 INITIALIZATION 直接給它字串:btn_run = '執行查詢'.。如果寫成 TEXT-xxx,則是取文字符號(Text Symbol)的值。

10. TABBED BLOCK、AS SUBSCREEN、FUNCTION KEY

條件多到一頁塞不下時,用頁籤(Tabbed Block)。做法是:先把每組條件做成 AS SUBSCREEN 的選擇畫面,再用 TABBED BLOCK 把它們掛上去。

REPORT z_tabbed_demo.

TABLES sscrfields.                  " 唯一必須保留 TABLES 的地方:讀功能碼

DATA gv_matnr TYPE mara-matnr.
DATA gv_werks TYPE marc-werks.

" --- 子畫面 1:基本條件 ---
SELECTION-SCREEN BEGIN OF SCREEN 1100 AS SUBSCREEN.
  SELECT-OPTIONS s_matnr FOR gv_matnr.
  SELECT-OPTIONS s_werks FOR gv_werks.
SELECTION-SCREEN END OF SCREEN 1100.

" --- 子畫面 2:進階條件 ---
SELECTION-SCREEN BEGIN OF SCREEN 1200 AS SUBSCREEN.
  PARAMETERS p_zero  AS CHECKBOX DEFAULT abap_true.
  PARAMETERS p_layou TYPE disvariant-variant.
SELECTION-SCREEN END OF SCREEN 1200.

" --- 頁籤區塊:FOR n LINES 是頁籤區要佔幾行 ---
SELECTION-SCREEN BEGIN OF TABBED BLOCK tb FOR 12 LINES.
  SELECTION-SCREEN TAB (20) tab1 USER-COMMAND uc1 DEFAULT SCREEN 1100.
  SELECTION-SCREEN TAB (20) tab2 USER-COMMAND uc2 DEFAULT SCREEN 1200.
SELECTION-SCREEN END OF BLOCK tb.

INITIALIZATION.
  tab1 = '基本條件'.
  tab2 = '進階設定'.
  tb-prog = sy-repid.
  tb-dynnr = 1100.                  " 預設顯示哪一個子畫面
  tb-activetab = 'UC1'.

AT SELECTION-SCREEN.
  CASE sscrfields-ucomm.
    WHEN 'UC1'. tb-dynnr = 1100. tb-activetab = 'UC1'.
    WHEN 'UC2'. tb-dynnr = 1200. tb-activetab = 'UC2'.
  ENDCASE.

FUNCTION KEY 是把按鈕放到上方應用工具列(不是畫面內),最多 5 個,功能碼固定是 FC01~FC05:

TABLES sscrfields.

SELECTION-SCREEN FUNCTION KEY 1.
SELECTION-SCREEN FUNCTION KEY 2.

INITIALIZATION.
  " 用 smp_dyntxt 可以同時設定文字、圖示與提示
  DATA(ls_btn1) = VALUE smp_dyntxt( icon_id   = icon_export
                                    icon_text = '匯出'
                                    quickinfo = '把結果匯出成 Excel' ).
  sscrfields-functxt_01 = ls_btn1.

  DATA(ls_btn2) = VALUE smp_dyntxt( icon_id   = icon_information
                                    icon_text = '說明' ).
  sscrfields-functxt_02 = ls_btn2.

AT SELECTION-SCREEN.
  CASE sscrfields-ucomm.
    WHEN 'FC01'.  MESSAGE '你按了匯出' TYPE 'I'.
    WHEN 'FC02'.  MESSAGE '你按了說明' TYPE 'I'.
  ENDCASE.
本章唯一必須保留 TABLES sscrfields. 的地方就在這裡——選擇畫面的功能碼只能透過 SSCRFIELDS 這個結構讀取,SAP 至今仍是這樣設計。除此之外,其他 TABLES 一律拿掉。
另外:在選擇畫面上請讀 sscrfields-ucomm,不要讀 sy-ucomm,後者在選擇畫面上不一定被填。

11. 文字元素與多語翻譯

在 SELECT-OPTIONS 與 PARAMETERS 的設計中,畫面上顯示的名稱預設就是物件的定義名(P_P1、S_MATNR),這對使用者完全沒有意義。SAP 提供 Text Elements(文字元素) 來自訂,路徑:ABAP 編輯器 → 轉到(Goto)→ 文字元素(Text Elements)。共三類:

類別用途引用方式
清單標題 List Headings定義 Report 的標題與欄位抬頭,配合 WRITE 輸出使用系統自動套用在傳統清單上方
選擇文字 Selection Texts設定 PARAMETERS / SELECT-OPTIONS 的抬頭文字自動顯示;勾「字典」欄可直接沿用資料元素的短文
文字符號 Text Symbols自訂任意文字,三位代碼TEXT-001,或 '預設文字'(001)
" 兩種引用寫法的差別:
WRITE / TEXT-001.              " 若 001 未維護,畫面上是空白,很難查
WRITE / '銷售訂單清單'(001).    " 若 001 未維護,就顯示括號前的字面值(建議!)

" 訊息也可以用文字符號
MESSAGE |{ TEXT-e01 }:{ p_werks }| TYPE 'E'.
老師的三個實作提醒
1. 「'字面值'(001)」這種寫法最安全——文字符號沒維護時至少還看得到字,而且雙擊就能建立。
2. 文字元素要單獨啟用(Activate)!很多人改完 Selection Texts 存檔就走,結果畫面沒變,就是忘了對文字元素按啟用。
3. 設定值與當前登入語言綁定;程式若要支援多語,每種語言都要各自維護。

翻譯路徑

  • 編輯器內:More(更多)→ Goto(轉到)→ Translation(翻譯),選來源語言與目標語言。
  • 正式翻譯工具:SE63 → Translation → ABAP Objects → Short Texts,可批次處理程式的所有短文。
  • 下拉選單裡能選的語言,必須是系統已經安裝的語言;沒裝的語言選不到。

12. 選擇畫面的事件順序(背下來)

ABAP 可執行程式就是「一組針對各事件的處理塊」。如果程式裡一個事件關鍵字都沒寫,所有語句會被自動歸給隱含的 START-OF-SELECTION

#事件觸發時機典型用途
1LOAD-OF-PROGRAM程式載入記憶體時(一次)建立單例物件、初始化全域常數
2INITIALIZATION選擇畫面顯示之前(一次)設預設值、灌 LISTBOX 值、設按鈕文字、預填 SELECT-OPTIONS
3AT SELECTION-SCREEN OUTPUT畫面 PBO;每次顯示與每次按 Enter/F8 都會跑LOOP AT SCREEN 動態顯示/隱藏/變灰
4aAT SELECTION-SCREEN ON VALUE-REQUEST FOR f使用者按 F4自製搜尋說明
4bAT SELECTION-SCREEN ON HELP-REQUEST FOR f使用者按 F1自製欄位說明
5aAT SELECTION-SCREEN ON fPAI,逐一欄位,依宣告順序單一欄位檢核(錯誤時只有該欄可輸入)
5bAT SELECTION-SCREEN ON END OF sel多重選擇彈窗關閉之後檢核整個 SELECT-OPTIONS 內表
5cAT SELECTION-SCREEN ON RADIOBUTTON GROUP g單選鈕群組整體檢核群組層級驗證
5dAT SELECTION-SCREEN ON BLOCK b該 BLOCK 內任一操作跨欄位、同框架的關聯檢核
5eAT SELECTION-SCREEN ON EXIT-COMMAND按 Back / Exit / Cancel(功能碼型別 E)離開前提醒「資料未儲存」
6AT SELECTION-SCREENPAI 的最後,整張畫面跨欄位總檢核、處理按鈕功能碼
7START-OF-SELECTION按下執行(F8)之後抓資料(GET_DATA)
8GET <node>邏輯資料庫逐筆讀取舊式 LDB 程式(MB52 等標準報表)
9END-OF-SELECTION所有資料選取完成、輸出畫面尚未顯示之前結果檢驗、呼叫 ALV 顯示
10TOP-OF-PAGE / END-OF-PAGE / AT LINE-SELECTION清單輸出期間傳統清單的頁首頁尾與互動
REPORT z_event_demo.

TABLES sscrfields.

PARAMETERS qual_day TYPE d DEFAULT sy-datum.
SELECTION-SCREEN PUSHBUTTON /1(20) btn_save USER-COMMAND save.

INITIALIZATION.
  " 把日期推到「上個月的最後一天」
  qual_day+6(2) = '01'.             " 先變成本月 1 號
  qual_day      = qual_day - 1.     " 再減 1 天 = 上月月底
  btn_save      = '儲存'.           " 給按鈕文字(現代寫法,不用 MOVE)

AT SELECTION-SCREEN OUTPUT.
  " 每次畫面重繪都會跑到這裡

AT SELECTION-SCREEN.
  CASE sscrfields-ucomm.
    WHEN 'SAVE'.  MESSAGE '假裝存檔成功' TYPE 'S'.
  ENDCASE.

START-OF-SELECTION.
  WRITE: / 'DATE =', qual_day.

END-OF-SELECTION.
  " <> 與 >< 都是「不等於」,S/4 建議統一寫 <>
  IF qual_day <> sy-datum.
    WRITE: / 'TODAY =', sy-datum.
  ELSE.
    WRITE: / 'QUAL_DAY =', qual_day.
  ENDIF.
三個最容易搞錯的事件細節
1. INITIALIZATION 設的值會被變式(Variant)和 SUBMIT ... WITH 傳進來的值覆蓋。 這是設計如此(否則變式就沒意義),但常讓人以為「我的預設值不見了」。
2. AT SELECTION-SCREEN OUTPUT 每次按 Enter 都會重跑。 如果你在裡面 APPEND 東西進 SELECT-OPTIONS,會愈按愈多筆——預填資料請放 INITIALIZATION。
3. 錯誤訊息放的位置決定使用者體驗:在 AT SELECTION-SCREEN ON p_werks 裡發 MESSAGE ... TYPE 'E'只有 p_werks 這一格可以輸入,其他欄位變灰;在 AT SELECTION-SCREEN(無 ON)裡發,則全部欄位都可以再輸入。要求使用者只改那一格就用前者,要讓他重看全部就用後者。

13. 第 39 個練習:MODIF ID + LOOP AT SCREEN 動態顯示/隱藏

這是選擇畫面最實用的技巧:勾了某個核取方塊,才顯示相關欄位。三個關鍵零件:MODIF ID(分組)+ USER-COMMAND(觸發重繪)+ AT SELECTION-SCREEN OUTPUT 裡的 LOOP AT SCREEN(改屬性)。

REPORT z202202_39.

DATA gv_vbeln TYPE vbak-vbeln.

SELECT-OPTIONS s_s1 FOR gv_vbeln.

" 有 USER-COMMAND 才會觸發 AT SELECTION-SCREEN,畫面才會重繪
PARAMETERS p_p1 AS CHECKBOX USER-COMMAND flag.

" 加了 MODIF ID F1,這兩個欄位就屬於 screen-group1 = 'F1'
PARAMETERS p_p2 TYPE vbak-vbeln MODIF ID f1.
PARAMETERS p_p3 TYPE vbak-vbeln MODIF ID f1.

INITIALIZATION.
  " 預填一筆選擇條件(用 s_s1[] 明確指定內表本體)
  APPEND VALUE #( sign = 'I' option = 'EQ' low = '555' ) TO s_s1[].

AT SELECTION-SCREEN OUTPUT.
  LOOP AT SCREEN.
    IF screen-group1 = 'F1'.
      " 沒勾 P_P1 就整個藏起來;勾了才出現
      screen-active = COND #( WHEN p_p1 = abap_true THEN 1 ELSE 0 ).
      MODIFY SCREEN.
    ENDIF.
  ENDLOOP.
SCREEN 欄位0 的意思1 的意思常見用途
ACTIVE完全不顯示(連標籤一起消失)正常顯示隱藏整組欄位
INPUT唯讀(灰色,看得到不能改)可輸入顯示但不給改
INVISIBLE正常顯示成 *(遮蔽)密碼欄位
REQUIRED非必填必填依情境動態變成必填
OUTPUT不輸出輸出較少單獨使用
隱藏 SELECT-OPTIONS 要多藏一個東西:一個 SELECT-OPTIONS 在畫面上其實有三個元素——S_MATNR-LOWS_MATNR-HIGH,還有右邊那顆多重選擇按鈕,它的技術名稱是 %_S_MATNR_%_APP_%。用 MODIF ID 分組時三個都會被涵蓋;但若你是靠 screen-name 逐一判斷,記得把 %_..._%_APP_% 也藏掉,否則會留下一顆孤零零的按鈕。
" 依 screen-name 精準控制(不用 MODIF ID 時的做法)
AT SELECTION-SCREEN OUTPUT.
  LOOP AT SCREEN.
    IF screen-name CS 'S_MATNR' OR screen-name = '%_S_MATNR_%_APP_%'.
      screen-input = 0.             " 變灰、唯讀
      MODIFY SCREEN.
    ENDIF.
  ENDLOOP.
MODIF ID 的代號最多 3 個字元screen-group1 是 CHAR3),寫超過會語法錯誤。另外 screen-group1 的值一律會被轉成大寫,所以 MODIF ID f1 要用 IF screen-group1 = 'F1' 來比對,寫成 'f1' 永遠不會成立。

14. 自製 F4 輔助說明

當 DDIC 掛的搜尋說明不夠用(例如要「只列本廠有庫存的料號」「依前一個欄位動態過濾」),就自己寫。

函數用途
F4IF_INT_TABLE_VALUE_REQUEST自己組出來的內表當值清單(最常用)
F4IF_FIELD_VALUE_REQUEST直接叫出某個 DDIC 表欄位的標準搜尋說明
DYNP_VALUES_READ讀取畫面上「尚未按 Enter」的其他欄位值,做連動 F4
DYNP_VALUES_UPDATE把值寫回畫面欄位
CL_GUI_FRONTEND_SERVICES檔案/目錄選擇對話框

講義的 Z202202_39 用 FORM 實作,這裡改寫成 S/4 建議的本地類別寫法:

REPORT z_f4_demo.

DATA gv_vbeln TYPE vbak-vbeln.
SELECT-OPTIONS s_s1 FOR gv_vbeln.

CLASS lcl_f4 DEFINITION.
  PUBLIC SECTION.
    " CHANGING 傳址,才能把選到的值寫回畫面欄位
    CLASS-METHODS vbeln_help CHANGING cv_field TYPE any.
ENDCLASS.

CLASS lcl_f4 IMPLEMENTATION.
  METHOD vbeln_help.

    " 1. 準備要顯示的清單(欄位順序=畫面欄位順序)
    SELECT vbeln, erdat, ernam, netwr
      FROM vbak
      INTO TABLE @DATA(lt_value)
      UP TO 200 ROWS
      ORDER BY vbeln.               " 有序才穩定

    DATA lt_return TYPE TABLE OF ddshretval.

    " 2. 叫出 F4 視窗
    CALL FUNCTION 'F4IF_INT_TABLE_VALUE_REQUEST'
      EXPORTING
        retfield        = 'VBELN'   " 回傳哪一欄
        dynpprog        = sy-repid  " 當前程式名稱
        dynpnr          = sy-dynnr  " 當前螢幕編號
        value_org       = 'S'       " S = 內表為結構化(多欄);C = 單欄
      TABLES
        value_tab       = lt_value  " 下拉選擇的內容
        return_tab      = lt_return " 使用者選中的列
      EXCEPTIONS
        parameter_error = 1
        no_values_found = 2
        OTHERS          = 3.

    IF sy-subrc <> 0.
      MESSAGE '取得輔助說明失敗' TYPE 'S' DISPLAY LIKE 'W'.
      RETURN.
    ENDIF.

    " 3. 把選到的值寫回欄位
    IF line_exists( lt_return[ 1 ] ).
      cv_field = lt_return[ 1 ]-fieldval.
    ENDIF.

  ENDMETHOD.
ENDCLASS.

" F4:LOW 與 HIGH 要各寫一個事件
AT SELECTION-SCREEN ON VALUE-REQUEST FOR s_s1-low.
  lcl_f4=>vbeln_help( CHANGING cv_field = s_s1-low ).

AT SELECTION-SCREEN ON VALUE-REQUEST FOR s_s1-high.
  lcl_f4=>vbeln_help( CHANGING cv_field = s_s1-high ).

" F1:自製欄位說明
AT SELECTION-SCREEN ON HELP-REQUEST FOR s_s1-low.
  MESSAGE '這裡輸入銷售訂單號碼,可用多重選擇一次貼上多筆' TYPE 'I'.
另一種回寫方式:把 dynprofield = 'S_S1-LOW' 也一起傳給 F4IF_INT_TABLE_VALUE_REQUEST,函數會自動把值填回畫面,你就不必自己讀 RETURN_TAB。缺點是欄位名寫死成字串,重構時容易漏改。兩種都可以,挑一種團隊統一就好。
VALUE_ORG'S' 代表 value_tab 是「結構化(多欄)」的內表;'C' 代表單欄字串清單。傳錯會顯示成一團亂碼似的欄位。

連動 F4(選了工廠,料號清單只列該廠的)——關鍵是用 DYNP_VALUES_READ 讀出畫面上「還沒送出」的工廠值:

AT SELECTION-SCREEN ON VALUE-REQUEST FOR s_matnr-low.

  DATA lt_dynpfields TYPE TABLE OF dynpread.
  APPEND VALUE #( fieldname = 'P_WERKS' ) TO lt_dynpfields.

  CALL FUNCTION 'DYNP_VALUES_READ'
    EXPORTING
      dyname     = sy-repid
      dynumb     = sy-dynnr
      translate_to_upper = abap_true
    TABLES
      dynpfields = lt_dynpfields
    EXCEPTIONS
      OTHERS     = 1.

  DATA(lv_werks) = COND werks_d( WHEN line_exists( lt_dynpfields[ 1 ] )
                                 THEN lt_dynpfields[ 1 ]-fieldvalue ).

  SELECT a~matnr, b~maktx
    FROM marc AS a
    INNER JOIN makt AS b ON b~matnr = a~matnr AND b~spras = @sy-langu
    WHERE a~werks = @lv_werks
    INTO TABLE @DATA(lt_help)
    UP TO 500 ROWS
    ORDER BY a~matnr.

檔案路徑 F4(上傳/下載程式一定會用到):

PARAMETERS p_file TYPE string LOWER CASE VISIBLE LENGTH 60 OBLIGATORY.

AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_file.

  CONSTANTS lc_filter TYPE string
            VALUE 'Excel (*.XLSX)|*.XLSX|文字檔 (*.TXT)|*.TXT|所有檔案 (*.*)|*.*|'.

  DATA lt_files TYPE filetable.
  DATA lv_rc    TYPE i.

  cl_gui_frontend_services=>file_open_dialog(
    EXPORTING
      window_title      = '請選擇要上傳的檔案'
      default_extension = 'XLSX'
      file_filter       = lc_filter
    CHANGING
      file_table        = lt_files
      rc                = lv_rc
    EXCEPTIONS
      OTHERS            = 1 ).

  IF lv_rc > 0 AND line_exists( lt_files[ 1 ] ).
    p_file = lt_files[ 1 ]-filename.
  ENDIF.
前端檔案對話框不能在背景執行(Batch Job)裡用——沒有 SAP GUI 就會 dump。若程式要支援排程,請改讀應用伺服器路徑AL11 上的目錄,用 OPEN DATASET 讀寫),並用 IF sy-batch = abap_true. 分流。另外在 Fiori / 瀏覽器環境下 CL_GUI_FRONTEND_SERVICES 完全不可用。

15. 第 40 個練習:ON BLOCK 驗證與終止指令

老師點出這個練習的三個重點:(1) 理解 ON BLOCK 事件的觸發時間點;(2) 理解 START-OF-SELECTION 就是按下執行鍵之後的動作(一般放 GET_DATA);(3) 抓資料時,PARAMETERS 用 =,SELECT-OPTIONS 用 IN

REPORT z202202_40.

DATA gv_vbeln TYPE vbak-vbeln.
DATA gv_erdat TYPE vbak-erdat.

SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001.
  PARAMETERS       p_p2 TYPE vbak-vbeln.
  SELECT-OPTIONS   s_erdat FOR gv_erdat.
SELECTION-SCREEN END OF BLOCK b1.

" 在 block b1 內的任何操作都會觸發此事件
AT SELECTION-SCREEN ON BLOCK b1.
  IF p_p2 IS NOT INITIAL AND s_erdat[] IS NOT INITIAL.
    MESSAGE '訂單號碼與建立日期擇一輸入即可' TYPE 'E'.
  ENDIF.

" 按下退出鍵(Back / Exit / Cancel)時觸發
AT SELECTION-SCREEN ON EXIT-COMMAND.
  DATA lv_answer TYPE c LENGTH 1.
  CALL FUNCTION 'POPUP_TO_CONFIRM'
    EXPORTING
      titlebar              = '確認離開'
      text_question         = '尚未執行查詢,確定要離開嗎?'
      default_button        = '2'
    IMPORTING
      answer                = lv_answer          " 一定要接回答!
    EXCEPTIONS
      OTHERS                = 1.

START-OF-SELECTION.
  " GET DATA:PARAMETERS 用 = ,SELECT-OPTIONS 用 IN
  SELECT vbeln, erdat, ernam, netwr, waerk
    FROM vbak
    WHERE vbeln = @p_p2                " PARAMETERS → =
      AND erdat IN @s_erdat            " SELECT-OPTIONS → IN
    INTO TABLE @DATA(lt_vbak)
    ORDER BY vbeln.

  IF lt_vbak IS INITIAL.
    MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  cl_salv_table=>factory(
    IMPORTING r_salv_table = DATA(lo_alv)
    CHANGING  t_table      = lt_vbak ).
  lo_alv->display( ).

END-OF-SELECTION.
講義註記「AT SELECTION-SCREEN ON EXIT-COMMAND(20250611 試了無效)」的原因(依講義步驟整理)
1. 這個事件只在功能碼型別為 E(Back/Exit/Cancel)時才觸發,按 Enter 或 F8 不會進來。
2. 原範例呼叫 POPUP_TO_CONFIRM_DATA_LOSS 時把 ANSWER 註解掉了,沒有接使用者的回答,所以不管按什麼都一樣「看起來沒作用」。
3. 這個事件的定位是「離開前做善後」(提示、清記憶體、寫 log),不要把關鍵控制邏輯壓在這裡

四個終止指令的差別(老師特別列出來對照,很容易考也很容易寫錯):

指令可用於行為
STOPINITIALIZATION、AT SELECTION-SCREEN、START-OF-SELECTION、GET在 INITIALIZATION 中:直接送出畫面;在其他事件中:直接跳到 END-OF-SELECTION。不想執行 END-OF-SELECTION 就改用 RETURN 或 EXIT
EXIT迴圈(DO/WHILE/LOOP/SELECT)與處理模組迴圈中:只跳出當前這一層迴圈;在 START-OF-SELECTION / GET / END-OF-SELECTION 中:直接觸發清單輸出、不執行 END-OF-SELECTION;沒有清單就回到選擇畫面。SAP 建議 EXIT 用於迴圈
RETURN事件塊、Dialog Module、程序(FM/方法/副程式)無條件離開當前處理模組,不管包了幾層迴圈(這是與 EXIT 最大的差別),不影響後續模組
CHECK迴圈與處理模組迴圈中:條件不成立就跳過本次迴圈剩餘部分,繼續下一圈(相當於 CONTINUE);事件中:中斷本事件後續程式碼,不影響其他模組。SAP 建議 CHECK 用於迴圈
REJECT邏輯資料庫的 GET 事件類似 CHECK,但即使在迴圈內也會退出本次紀錄(含子節點)的處理,直接讀下一筆
老師的建議照抄 SAPEXIT 和 CHECK 只用在迴圈裡;要離開處理模組(FORM/METHOD/事件塊)一律用 RETURN。 這樣讀程式的人不必猜「這個 EXIT 是跳出迴圈還是跳出整個模組」。

16. 第 34、35 個練習:SAP 記憶體與 ABAP 記憶體

兩種記憶體常被混為一談,作用範圍完全不同:

SAP Memory(SPA/GPA)ABAP Memory
指令SET / GET PARAMETER IDEXPORT / IMPORT … TO/FROM MEMORY ID
作用範圍同一使用者的所有 Session(Mode)之間,登出才清掉同一個呼叫堆疊(internal session)內,例如 SUBMIT/CALL TRANSACTION 的上下游
能存什麼只能存單一字串值(畫面欄位值)任何資料物件,包括內表、結構
典型用途畫面欄位預設值(工廠、公司代碼),配合 SU3 使用者參數程式間傳大量資料
登錄位置參數 ID 需存在於 TPARA(SE80 建立 / SM30 維護)不需登錄,ID 自取即可(建議 Z 開頭)

第 34 個練習:SET / GET PARAMETER ID(在 Session 之間共用變數):

*&---------------------------------------------------------------------*
*& Report Z202202_34_SET  ── 先執行這一支
*&---------------------------------------------------------------------*
REPORT z202202_34_set.

DATA(lv_data1) = CONV char6( 'CASPER' ).
SET PARAMETER ID 'ZP1' FIELD lv_data1.
MESSAGE |已寫入 SAP 記憶體 ZP1 = { lv_data1 }| TYPE 'S'.

*&---------------------------------------------------------------------*
*& Report Z202202_34_GET  ── 再執行這一支(可以在另一個 Session)
*&---------------------------------------------------------------------*
REPORT z202202_34_get.

DATA lv_data2 TYPE char6.
GET PARAMETER ID 'ZP1' FIELD lv_data2.
WRITE: / '從 SAP 記憶體讀到:', lv_data2.
執行要領:先執行 Z202202_34_SET,再執行 Z202202_34_GET(講義原文此處誤植為兩次 SET),並確認讀到的值正確。想確認跨 Session 有效,就開一個新視窗(/o)再跑 GET。

第 35 個練習:EXPORT / IMPORT MEMORY ID + SUBMIT(在 ABAP 程式之間共用變數):

*&---------------------------------------------------------------------*
*& Report Z202202_35_EXPORT
*&---------------------------------------------------------------------*
REPORT z202202_35_export.

TYPES: BEGIN OF ty_item,
         matnr TYPE mara-matnr,
         menge TYPE i,
       END OF ty_item,
       ty_t_item TYPE STANDARD TABLE OF ty_item WITH EMPTY KEY.

DATA(lv_data) = CONV char6( 'CASPER' ).
DATA(lt_item) = VALUE ty_t_item( ( matnr = 'M001' menge = 10 )
                                 ( matnr = 'M002' menge = 20 ) ).

" 建議用「具名參數」寫法:兩邊變數名稱就不必相同
EXPORT p_flag = lv_data
       p_item = lt_item
       TO MEMORY ID 'ZP2'.

SUBMIT z202202_35_import AND RETURN.

FREE MEMORY ID 'ZP2'.               " 用完要清,否則佔記憶體也可能誤讀舊值
WRITE: / '[EXPORT 程式] LV_DATA =', lv_data.

*&---------------------------------------------------------------------*
*& Report Z202202_35_IMPORT
*&---------------------------------------------------------------------*
REPORT z202202_35_import.

" 型別必須兩邊一致:放在共用的 INCLUDE、Type Pool 或 DDIC 結構
TYPES: BEGIN OF ty_item,
         matnr TYPE mara-matnr,
         menge TYPE i,
       END OF ty_item,
       ty_t_item TYPE STANDARD TABLE OF ty_item WITH EMPTY KEY.

DATA lv_receive TYPE char6.         " 名稱可以跟來源不同
DATA lt_receive TYPE ty_t_item.

IMPORT p_flag = lv_receive
       p_item = lt_receive
       FROM MEMORY ID 'ZP2'.

IF sy-subrc <> 0.
  MESSAGE 'ABAP 記憶體 ZP2 沒有資料' TYPE 'E'.
ENDIF.

WRITE: / '[IMPORT 程式] 收到', lv_receive, '共', lines( lt_receive ), '筆'.
講義說「此法變數名稱二方要一致」,那是簡寫形式的限制。 只要改用具名寫法 EXPORT p_flag = lv_data / IMPORT p_flag = lv_receive,兩邊變數名就完全解耦,這是 S/4 上建議的寫法。
另外:IMPORT 之後一定要檢查 sy-subrc;記憶體是空的時候 IMPORT 不會報錯,變數就保持原值,很容易誤判。
老師提醒:「SUBMIT 與 CALL TRANSACTION 是在程式之中執行其他程式,一定要妥善規畫,否則管理不易。一般比較常見的是用資料表當做多個程式之間共用變數。」 S/4 時代還多了兩個選項:類別的靜態屬性(同一 session 內最乾淨)與 Shared ObjectsSHMA,跨 session 的共用記憶區)。

17. SUBMIT:把條件丟給另一支程式

SUBMIT prog
  [VIA SELECTION-SCREEN]                 " 顯示對方的選擇畫面讓使用者確認
  [USING SELECTION-SET 'VAR01']          " 套用對方的某個變式
  [WITH p_werks EQ '1000']               " 對方的 PARAMETERS
  [WITH s_matnr IN lr_matnr]             " 對方的 SELECT-OPTIONS(整個 range 丟過去)
  [WITH s_matnr BETWEEN 'A' AND 'B']     " 單一區間寫法
  [WITH SELECTION-TABLE lt_seltab]       " 動態組條件
  [EXPORTING LIST TO MEMORY]             " 把對方的清單輸出攔到記憶體
  [AND RETURN].                          " 執行完回到本程式(沒有它就一去不回)
REPORT z_submit_demo.

TYPES ty_r_matnr TYPE RANGE OF mara-matnr.

DATA(lr_matnr) = VALUE ty_r_matnr(
    ( sign = 'I' option = 'CP' low = 'DXTR1*' )
    ( sign = 'E' option = 'EQ' low = 'DXTR1205' ) ).

" 方式一:直接指定(欄位名稱要跟對方程式一致,編譯期就會檢查)
SUBMIT zreport_stock
  WITH p_werks EQ 'NCU1'
  WITH s_matnr IN lr_matnr
  AND RETURN.

" 方式二:動態組條件表(欄位名是字串,可在執行期決定)
DATA lt_seltab TYPE TABLE OF rsparams.
APPEND VALUE #( selname = 'P_WERKS' kind = 'P'
                sign = 'I' option = 'EQ' low = 'NCU1' ) TO lt_seltab.
APPEND VALUE #( selname = 'S_MATNR' kind = 'S'
                sign = 'I' option = 'CP' low = 'DXTR1*' ) TO lt_seltab.

SUBMIT zreport_stock
  WITH SELECTION-TABLE lt_seltab
  VIA SELECTION-SCREEN                  " 先讓使用者看一眼條件
  AND RETURN.
RSPARAMS 欄位說明
SELNAME對方程式的參數/選項名稱(大寫)
KIND'P' = PARAMETERS;'S' = SELECT-OPTIONS
SIGN / OPTION / LOW / HIGH同 SELECT-OPTIONS 四欄;KIND = 'P' 時只用 LOW
要抓對方程式的清單輸出,用 SUBMIT … EXPORTING LIST TO MEMORY AND RETURN. 之後呼叫 LIST_FROM_MEMORY 取出,再用 LIST_TO_ASCI 轉成文字列。這是「包裝標準報表」的經典手法(例如把 MB52 的結果撈出來再加工)。
反過來,要讀取自己這支程式當下的選擇條件(做 log 或做標題),用 FM RS_REFRESH_FROM_SELECTOPTIONS
SUBMIT 傳入的值會覆蓋 INITIALIZATION 設的預設值,這是刻意的設計。但要注意:如果對方的參數是 OBLIGATORY 而你沒傳值、又沒有 VIA SELECTION-SCREEN,程式會停在選擇畫面等人——在背景 job 裡就是 job 卡死。背景排程一律要確認所有必填欄位都有值或有變式。

18. 變式(Variant)與背景執行

把選擇畫面填好的一整組條件存起來重複使用,就是變式。這是老師說的「條件變式的善用技巧」,也是背景排程的前提。

  1. 在選擇畫面填好條件 → 點儲存(Save)圖示 → 輸入變式名稱與描述 → 儲存。
  2. 執行時:Goto → Variants → Get(或畫面上的「取得變式」按鈕)。
  3. 維護/刪除:SE38 → 選 Variants → Change。

變式屬性(Attributes)是很多人沒用過但很好用的功能:

屬性作用
Only for Background Processing只給背景 job 用,前台看不到
Protect Variant只有建立者能改
Do not display in catalog不出現在變式清單(隱藏用)
Save field without values該欄位不存值(每次執行都空白)
Protect field套用變式後該欄位變唯讀,使用者不能改
Hide field / Hide field 'BIS'套用變式後隱藏該欄位/只隱藏 HIGH 欄
Selection variable動態值D = 動態日期計算(今天、本月最後一天、上月…);T = 取 TVARVC 表變數;B = 使用者出口
排程報表最實用的組合:日期欄位設 Selection variable = D「目前日期」或「上個月的第一天/最後一天」,這樣一支 job 每天/每月自動跑出正確區間,不必每個月改變式。若多支程式要共用同一組值(例如「目前結帳期間」),就用 T(TVARVC),在 STVARV(部分系統為 STVARVC)維護一次,全部程式跟著變。

背景執行

  • SE38 → Program → Execute in Background(或 F9),選印表機、選日期/時間、選變式。
  • SM36 建立 Job(可串多個步驟、設定事件觸發、設定週期);SM37 監控 Job(看 Log、看 Spool)。
  • 程式裡用 sy-batch 判斷是否在背景,避開所有 GUI 呼叫。
START-OF-SELECTION.
  DATA(lt_result) = lcl_report=>get_data( ).

  IF sy-batch = abap_true.
    " 背景:不能用 ALV Grid、不能用 CL_GUI_FRONTEND_SERVICES
    " 只能寫 WRITE 清單(進 Spool)、寫檔到 AL11、或寄信
    LOOP AT lt_result INTO DATA(ls_r).
      WRITE: / ls_r-matnr, ls_r-werks, ls_r-labst.
    ENDLOOP.
  ELSE.
    cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                            CHANGING  t_table      = lt_result ).
    lo_alv->display( ).
  ENDIF.
背景執行的三大地雷:(1) MESSAGE … TYPE 'I''E' 在背景會讓 job 直接失敗(Cancelled),背景請用 TYPE 'S' 或寫進 Application Log(SLG1/BAL_* 系列 FM)。(2) 任何 CL_GUI_* 前端物件都會 dump。(3) 必填欄位沒值又沒變式,job 會卡在選擇畫面。

19. 實務案例一:工廠預設與使用者版本

這支是老師的實戰範本(09-2),示範「單選鈕切換 → 動態清空/鎖定欄位 → 執行前總檢核」的完整套路。已改寫成 S/4 寫法(拿掉 TABLES、MOVE,改用 LOOP AT SCREEN 鎖欄位而非只是清空):

REPORT z_plant_default.

DATA gv_matnr TYPE marc-matnr.
DATA gv_mtart TYPE mara-mtart.
DATA gv_matkl TYPE mara-matkl.
DATA gv_dispo TYPE marc-dispo.
DATA gv_beskz TYPE marc-beskz.

SELECTION-SCREEN BEGIN OF BLOCK mm1 WITH FRAME TITLE TEXT-001.

  SELECT-OPTIONS: s_matnr FOR gv_matnr MODIF ID sc1,
                  s_mtart FOR gv_mtart,
                  s_matkl FOR gv_matkl,
                  s_dispo FOR gv_dispo,
                  s_beskz FOR gv_beskz.

  PARAMETERS:     p_werks  TYPE marc-werks OBLIGATORY DEFAULT 'NCU1',
                  p_exc_ba AS CHECKBOX DEFAULT abap_true,   " 排除採購申請 PR
                  p_exc_pa AS CHECKBOX DEFAULT abap_true,   " 排除計畫訂單
                  p_updt   TYPE flag DEFAULT space.         " 需要時可改 NO-DISPLAY

  SELECTION-SCREEN BEGIN OF LINE.
    PARAMETERS:       r_plant RADIOBUTTON GROUP g1 USER-COMMAND check.
    SELECTION-SCREEN  COMMENT 03(29) TEXT-r11 FOR FIELD r_plant. " All Plant Matl.
    PARAMETERS:       r_user  RADIOBUTTON GROUP g1 DEFAULT 'X'.
    SELECTION-SCREEN  COMMENT 35(15) TEXT-r12 FOR FIELD r_user.  " Particular Ver.
    SELECTION-SCREEN  COMMENT 50(08) TEXT-r13.                   " User ID:
    PARAMETERS:       p_usnam TYPE sy-uname MODIF ID usr.
  SELECTION-SCREEN END OF LINE.

SELECTION-SCREEN END OF BLOCK mm1.

INITIALIZATION.
  p_werks = 'NCU1'.                 " 現代寫法:直接指派,不用 MOVE
  p_usnam = sy-uname.               " 預設就是登入者本人

AT SELECTION-SCREEN OUTPUT.
  LOOP AT SCREEN.
    IF screen-group1 = 'USR'.
      " 選「全廠」時,使用者版本欄位鎖起來(比只清空更清楚)
      screen-input = COND #( WHEN r_plant = abap_true THEN 0 ELSE 1 ).
      MODIFY SCREEN.
    ENDIF.
  ENDLOOP.

  IF r_plant = abap_true.
    CLEAR p_usnam.
  ENDIF.

AT SELECTION-SCREEN.
  " 總檢核放這裡,比放 START-OF-SELECTION 好:訊息 E 會留在選擇畫面
  IF r_user = abap_true AND p_usnam IS INITIAL.
    MESSAGE 'Must Entry User Version!' TYPE 'E'.
  ENDIF.
  IF r_plant = abap_true AND p_usnam IS NOT INITIAL.
    MESSAGE 'Must not Entry User Version!' TYPE 'E'.
  ENDIF.

START-OF-SELECTION.
  PERFORM prepare_data.
  PERFORM get_data.
  PERFORM update_db_data.

END-OF-SELECTION.
檢核該放哪裡? 原講義把檢核放在 START-OF-SELECTION,用 MESSAGE 'xxx' TYPE 'I'EXIT。改放 AT SELECTION-SCREENTYPE 'E' 更好:使用者會直接留在選擇畫面、游標停在要改的欄位,不必再按一次返回。規則:凡是「條件正不正確」的檢查,一律放 AT SELECTION-SCREEN。

20. 實務案例二:年度預設(RFBILA00 / F.01 的選擇畫面解讀)

老師要求:先學會操作 F.01(財務報表),再回頭解讀它的程式碼 RFBILA00,看懂 PARAMETERS 與 SELECT-OPTIONS 在標準程式裡怎麼用。這段程式碼濃縮了本章幾乎所有技巧:

" ── 標準程式 RFBILA00(F.01)選擇畫面骨架,重點註解版 ──

SELECTION-SCREEN BEGIN OF LINE.
  " COMMENT ... FOR FIELD:讓這段文字的 F1 連到後面那個欄位
  SELECTION-SCREEN COMMENT  1(30) TEXT-030 FOR FIELD bilavers.  " 財務報表版本
  SELECTION-SCREEN POSITION 35.                                 " 對齊到第 35 欄
  PARAMETERS bilavers TYPE t011t-versn MEMORY ID bil OBLIGATORY." 掛 SAP 記憶體
  SELECTION-SCREEN COMMENT 40(8) TEXT-031 FOR FIELD bilaspra.   " 語言
  SELECTION-SCREEN POSITION 49.
  PARAMETERS bilaspra TYPE t011-dspra DEFAULT sy-langu OBLIGATORY.
SELECTION-SCREEN END OF LINE.

PARAMETERS     bilbjahr TYPE bkpf-gjahr OBLIGATORY.             " 報表年度
SELECT-OPTIONS b_monate FOR rfsdo-bilabmon NO-EXTENSION OBLIGATORY. " 期間(只能一列)
PARAMETERS     bilvjahr TYPE bkpf-gjahr OBLIGATORY.             " 比較年度
SELECT-OPTIONS v_monate FOR rfsdo-bilavmon NO-EXTENSION OBLIGATORY.
PARAMETERS     planvers TYPE skc1a-rvers DEFAULT space.         " 計畫版本

SELECTION-SCREEN SKIP 1.

" ── 輸出格式方框:三個單選鈕各自佔一行,配 COMMENT 當標籤 ──
SELECTION-SCREEN BEGIN OF BLOCK list WITH FRAME TITLE TEXT-204.

  SELECTION-SCREEN BEGIN OF LINE.
    PARAMETERS bilalist TYPE rfbila_alv_settings-classic RADIOBUTTON GROUP alv.
    SELECTION-SCREEN COMMENT 3(20) TEXT-205 FOR FIELD bilalist.  " Classical list
  SELECTION-SCREEN END OF LINE.

  SELECTION-SCREEN BEGIN OF LINE.
    PARAMETERS bilagrid TYPE rfbila_alv_settings-grid RADIOBUTTON GROUP alv.
    SELECTION-SCREEN COMMENT  3(20) TEXT-206 FOR FIELD bilagrid. " ALV grid control
    SELECTION-SCREEN COMMENT 25(10) TEXT-209 FOR FIELD bilagvar. " Layout
    SELECTION-SCREEN POSITION 40.
    PARAMETERS bilagvar TYPE slis_vari MEMORY ID gl_bspl_alv_grid_var.
  SELECTION-SCREEN END OF LINE.

  SELECTION-SCREEN BEGIN OF LINE.
    SELECTION-SCREEN POSITION 4.
    PARAMETERS bilastsl TYPE rfbila_alv_settings-strucblnce AS CHECKBOX.
    SELECTION-SCREEN COMMENT 6(40) TEXT-208 FOR FIELD bilastsl.
  SELECTION-SCREEN END OF LINE.

SELECTION-SCREEN END OF BLOCK list.
從這段標準程式可以學到的四件事
1. 期間用 SELECT-OPTIONS … NO-EXTENSION OBLIGATORY:既要區間(1 月到 12 月),又不准使用者輸入多列,是「會計期間」的標準寫法。
2. 版本欄位掛 MEMORY ID:使用者上次選什麼,下次進來自動帶出。
3. COMMENT … FOR FIELD 搭配 POSITION:標準程式全部用這一招做出整齊的欄位對齊,值得照抄。
4. ALV Layout 用 TYPE slis_vari(或 disvariant-variant)加 MEMORY ID:這是讓報表記住使用者版面配置的標準做法。

21. 綜合案例:複雜條件 + OO ALV

第 11 節配合 ABAP3-2 的 ZOOALV_2_GRID,把「INITIALIZATION 預填複雜條件」與 ALV 顯示接起來,這是本章的集大成:

REPORT zooalv_2_grid.

DATA gv_matnr TYPE mara-matnr.

SELECTION-SCREEN BEGIN OF BLOCK blk1 WITH FRAME TITLE TEXT-001.
  SELECT-OPTIONS s_matnr FOR gv_matnr.
SELECTION-SCREEN END OF BLOCK blk1.

" 預設條件:包含 DXTR1*,逐筆排除 DXTR1201 ~ DXTR1210
INITIALIZATION.
  APPEND VALUE #( sign = 'I' option = 'CP' low = 'DXTR1*' ) TO s_matnr[].

  DO 10 TIMES.
    DATA(lv_num)   = 1200 + sy-index.
    DATA(lv_matnr) = |DXTR{ lv_num }|.
    APPEND VALUE #( sign = 'E' option = 'EQ' low = lv_matnr ) TO s_matnr[].
  ENDDO.

START-OF-SELECTION.
  SELECT a~matnr, a~mtart, a~matkl, a~meins, b~maktx
    FROM mara AS a
    LEFT OUTER JOIN makt AS b ON b~matnr = a~matnr AND b~spras = @sy-langu
    WHERE a~matnr IN @s_matnr          " SELECT-OPTIONS 用 IN,逸出字元 @ 不可少
    INTO TABLE @DATA(lt_mara)
    ORDER BY a~matnr.                  " 一定要排序,結果才穩定

  IF lt_mara IS INITIAL.
    MESSAGE '查無符合條件的物料' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_mara ).

      lo_alv->get_functions( )->set_all( abap_true ).       " 開啟全部工具列功能
      lo_alv->get_columns( )->set_optimize( abap_true ).    " 欄寬自動最佳化
      lo_alv->get_display_settings( )->set_list_header( |物料清單({ lines( lt_mara ) } 筆)| ).
      lo_alv->display( ).

    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.
講義最後那句 WRITE: space.「關鍵!觸發畫面顯示」是舊 ALV 的權宜之計。
它的用途是:當你用 CL_GUI_ALV_GRID 掛在 Docking Container 上、而程式只有選擇畫面沒有任何螢幕時,必須先產生一個清單(List)畫面,Grid 才有地方可掛。
在 S/4HANA 上,全螢幕報表請直接用 CL_SALV_TABLE——它自己會建立畫面,完全不需要這一招;只有需要「可編輯欄位、複雜事件、多個 Grid 併排」時才回頭用 CL_GUI_ALV_GRID + 自建 Screen(見第 3-2 章)。

22. 選擇畫面設計檢查清單(Code Review 用)

  • 參數/選項名稱 ≤ 8 碼,前綴 P_ / S_ / R_ 一致。
  • 全部用 DDIC 型別宣告(有 F1/F4/轉換常式),不要用 CHARn 硬幹。
  • 沒有 TABLES:(唯一例外:TABLES sscrfields.)。
  • 檔名、E-mail、路徑類欄位有 LOWER CASE
  • 條件能列舉就列舉,避免 BT 區間;自建 range 的 SIGN/OPTION 都有填、大寫、與 HIGH 一致。
  • 「使用者什麼都不填就撈全表」的情況已被擋掉(OBLIGATORY 或 AT SELECTION-SCREEN 檢核)。
  • SELECT 一律明列欄位、帶 @ 逸出主機變數、加 ORDER BY
  • 條件檢核放 AT SELECTION-SCREEN(訊息 TYPE 'E'),不要放 START-OF-SELECTION。
  • 預填資料放 INITIALIZATION,不要放 AT SELECTION-SCREEN OUTPUT(會愈跑愈多筆)。
  • Selection Texts 全部維護好並單獨啟用;多語系客戶要做翻譯(SE63)。
  • 需要排程的程式:確認 sy-batch 分流、沒有 CL_GUI_* 呼叫、必填欄位有變式。
  • 需要重複執行的條件,做成變式並考慮用 Selection variable(D/T)處理日期。

練習與自我檢核

  1. 第 34 練習:寫 Z202202_34_SET 與 Z202202_34_GET,用 SET/GET PARAMETER ID 在兩個 Session 之間傳值。
    要點:先跑 SET 再跑 GET;參數 ID 建議登錄到 TPARA;驗證開新視窗(/o)也讀得到。
  2. 第 35 練習:用 EXPORT/IMPORT MEMORY ID + SUBMIT 在兩支程式之間傳值。
    要點:改用具名寫法 p_flag = lv_data,兩邊變數名可以不同;IMPORT 後檢查 sy-subrc;用完 FREE MEMORY ID。
  3. 第 36 練習:把 CHECKBOX、RADIOBUTTON、LISTBOX、MATCHCODE、STRING+VISIBLE LENGTH、OBLIGATORY、MEMORY ID 全部做一次。
    延伸:讓 P_MATNR 可以下拉選擇物料編號(先試 DDIC 型別的內建 F4,再試自製 F4)。
  4. Search Help 找名字:在 S/4HANA 上找出 USER_COMP 這個搜尋說明的四種方法(F1 技術資訊、SE11 資料元素、SE11 模糊搜尋、SE84)。
  5. 第 37 練習:把 s_s1~s_s7 七種 DEFAULT 組合寫出來,用 Debug 觀察 s_s1[]~s_s7[] 的 SIGN/OPTION/LOW/HIGH 各是什麼。
    要點:注意 DEFAULT 44 TO 54 自動變成 I/BT;DEFAULT 45 TO 55 OPTION BT SIGN E 變成排除區間。
  6. NO INTERVALS vs NO-EXTENSION:不看筆記說出兩者的差別,並說出「只准填一個單值」要怎麼寫。
  7. 區間 vs 列舉:舉出至少三個「用 BT 會抓錯、用 EQ 列舉才對」的實際理由,並把 E/BT 'DXTR1201' ~ 'DXTR1210' 改寫成正確的列舉版本。
  8. 第 38 練習:用 BEGIN OF BLOCK / WITH FRAME TITLE / BEGIN OF LINE / COMMENT / ULINE / SKIP 排出一個「三個方框、其中一行有兩個單選鈕加一個輸入框」的畫面,並用 CALL SELECTION-SCREEN 1001 STARTING AT … 做出彈出視窗。
  9. 事件順序:不看筆記寫出 INITIALIZATION → AT SELECTION-SCREEN OUTPUT → ON field/ON BLOCK/ON VALUE-REQUEST → AT SELECTION-SCREEN → START-OF-SELECTION → END-OF-SELECTION 的順序,並說明「訊息放 ON field 和放 AT SELECTION-SCREEN 有什麼不同」。
  10. 第 39 練習:用 MODIF ID + USER-COMMAND + LOOP AT SCREEN,做出「勾了核取方塊才出現兩個欄位」的效果;再改成「不勾時變灰(唯讀)而非消失」。
  11. F4 自製:用 F4IF_INT_TABLE_VALUE_REQUEST 做一個銷售訂單的 F4,LOW 與 HIGH 都要能用;再做一個「先選工廠、料號 F4 只列該廠料號」的連動 F4(用 DYNP_VALUES_READ)。
  12. 第 40 練習:實作 AT SELECTION-SCREEN ON BLOCK 與 ON EXIT-COMMAND,並解釋為什麼原範例的 EXIT-COMMAND「試了無效」。
    要點:POPUP 的 ANSWER 沒接回來;只有功能碼型別 E 才觸發。
  13. 終止指令:說出 STOP/EXIT/RETURN/CHECK/REJECT 各自的作用範圍,特別是「EXIT 在巢狀迴圈中只跳一層,RETURN 直接離開整個模組」。
  14. SUBMIT:用 WITH SELECTION-TABLE(RSPARAMS)動態組條件呼叫另一支程式,並加上 VIA SELECTION-SCREEN 與 AND RETURN 觀察差別。
  15. 變式與排程:建立一個變式,把日期欄位設成 Selection variable = D「上個月的最後一天」,再用 SM36 排一個每月執行的 job,SM37 看結果。
  16. 10-1(自我驗收):把第 2 章的 Z202202_26 改寫成「使用者可以輸入區間條件,且建檔者預設為登錄者(sy-uname)」。
    要點:SELECT-OPTIONS + INITIALIZATION 設 p_ernam = sy-uname。
  17. 10-2:打開標準程式 MB52(庫存清單)研究它的選擇畫面。老師的問題:「懂 ERP 模組的人來看程式,跟懂程式的人來學存貨評價,哪一個容易?」
  18. 10-3:程式中看不懂的部分請自學到第 3-4 章(第三、四代增強)。例如標準程式 RM07MLBS 裡的 ENHANCEMENT-POINT rm07mlbs_g4 SPOTS es_rm07mlbs [STATIC]
    ENHANCEMENT-POINT 是 Enhancement Framework 在標準碼裡預留的擴充點,可以插自訂邏輯而不修改原廠程式。
    SPOTS es_rm07mlbs 是這些增強點所屬的 Enhancement Spot(增強區域),用來把相關增強點組織在一起。
    STATIC 表示這是靜態增強點(可插入宣告式的程式碼,位置固定、始終可用);沒有 STATIC 的則是動態增強點。
    ・實作/查看:SE18 輸入 es_rm07mlbs;升級後檢查增強是否失效用 SPAU_ENH
  19. 綜合題:配合第 3-2 章的 ZOOALV_2_GRID,加上「包含 DXTR1*、排除 DXTR1201~DXTR1210」的複雜條件,並用 CL_SALV_TABLE 顯示;比較用 BT 排除與用 EQ 逐筆排除的筆數差異,把差異的料號列出來。
  20. 結業自問:老師說「恭喜您學完模組顧問所需要的 ABAP 知識」。要成為真正的 ABAPer,接下來還要走完:OOALV、Screen(Dialog)、SmartForms、Enhancement、RFC、BAPI——對照本圖書館第 3-2 到第 4-4 章,列出自己還沒掌握的項目。

第 3-2 章 OOALV 與物件導向初探、Screen 設計入門

這一章是從「會寫報表」跨到「會寫系統」的分水嶺。前半把 ABAP 物件導向的骨架講清楚——類別與物件、封裝的三道門(PUBLIC/PROTECTED/PRIVATE)、建構子、靜態成員、繼承與多型、介面、事件與 SET HANDLER;後半把這些觀念直接落地成 SAP 最經典的組合:SE51 畫一個 Screen + 放一個 Custom Control + CL_GUI_CUSTOM_CONTAINER + CL_GUI_ALV_GRID。學完之後,你能自己做出一支可編輯、可雙擊、可自訂按鈕、上下兩塊分割視窗的互動式 ALV,也知道為什麼在 S/4HANA 時代「不學 OO 就沒有下一步」——ABAP Cloud 與 RAP 的入場券就是 OO。

核心 TCODESE24SE51SE41SE80SE38SE11SE37SE93SE84ABAPDOCUSATST22
關鍵物件CL_GUI_CUSTOM_CONTAINER、CL_GUI_ALV_GRID、CL_GUI_SPLITTER_CONTAINER、CL_GUI_DOCKING_CONTAINER、CL_GUI_DIALOGBOX_CONTAINER、CL_GUI_CONTAINER、CL_GUI_CFW、CL_SALV_TABLE、CL_SALV_EVENTS_TABLE、CL_SALV_FORM_LAYOUT_GRID、CL_SALV_FUNCTIONS_LIST、LVC_T_FCAT、LVC_S_LAYO、LVC_S_PRNT、LVC_S_SORT、LVC_S_FILT、LVC_FIELDCATALOG_MERGE、CX_ROOT、SFLIGHT、EKKO、MARA、MARC、ZVBAP_VBAK

1. 為什麼一定要學 OO:ALV 三代演進與 S/4HANA 的現實

老師在講義裡把 ALV 的三個世代並排比較,這張表其實就是 ABAP 三十年的縮影:

 Function ALVGRID ALV(OOALV)SALV(OOALV)
年代199620002004
平台SAP R/3 3.1ISAP R/3 4.5SAP NetWeaver
主要 APIREUSE_ALV_LIST_DISPLAY
REUSE_ALV_GRID_DISPLAY
CL_GUI_ALV_GRID
REUSE_ALV_GRID_DISPLAY_LVC
CL_SALV_TABLE
CL_SALV_FUNCTIONS_LIST
要不要自己畫螢幕不需要螢幕控制就能顯示需要螢幕控制(SE51 + Custom Control)不需要螢幕控制也能顯示(也可內嵌到 Container)
是不是 OO
功能普通豐富(可編輯、事件多)豐富但受控(唯讀為主)
本圖書館案例第 2-3 章整章本章第 5~10 節本章第 11~12 節
老師的一句總結:「OOALV 與 Function ALV 最大的差異就是——前者把程式放在各個物件中,不像後者一目了然。但更適合規畫複雜系統,需要更多的程式技巧。」

到了 S/4HANA,這已經不只是「哪個比較好用」的品味問題,而是硬性條件:

  • ABAP Cloud 開發模型只允許使用 released API(C1 contract)。絕大多數 REUSE_ALV_* 這類經典函數模組都沒有釋出給 Cloud,Function ALV 在 ABAP Cloud / BTP ABAP Environment 上根本編譯不過。
  • RAP(ABAP RESTful Application Programming Model)的 Behavior Implementation 全部是 CLASS ... IMPLEMENTATION + 介面 IF_ABAP_BEHAVIOR_HANDLER,不會 OO 就寫不出一行。
  • 單元測試(ABAP Unit)是類別導向的(FOR TESTINGCL_ABAP_UNIT_ASSERT),沒有類別就沒有測試。
  • Clean ABAP 官方風格指南預設所有新程式都以類別為單位,FORM/PERFORM 已被標記為 obsolete。
S/4HANA 上 FORM / PERFORM(Subroutine)與 TABLES:WITH HEADER LINE 在 ABAP Cloud 語法檢查中直接是語法錯誤;在 On-Premise 的標準 ABAP 也只是「還能編譯的過時語法」。本章講義原文大量使用 FORM,我們保留它的教學順序,但範例一律改寫成本地類別(Local Class)的方法

2. OO 基本觀念:類別、物件、封裝

講義開宗明義的定義,先背下來:

  • 類(Class):用來描述「具有相同屬性和方法的物件」的集合。它定義了該集合中每個物件所共有的屬性(Attributes)和方法(Methods)。類別是藍圖
  • 物件(Object):是類的實例(Instance),是一個現實實體的抽象。一個物件可以被認為是「把資料(屬性)和程式(方法)封裝在一起」的實體。
  • 方法(Methods):對物件的操作。
  • 物件是動態的概念:其中的屬性反映了物件當前的狀態
  • OOALV 就是利用 ALV 的一個實例(Instance)來顯示的。

ABAP 的類別分兩種存放位置,兩者語法完全相同,差別只在「誰看得到」:

 本地類別 Local Class全域類別 Global Class
建立工具直接寫在程式(SE38/SE80)裡,或 Include *_CL1SE24 或 SE80 → Class/Interface;Eclipse ADT 建 Class
命名習慣用 LCL_ 開頭(Local CLass)客製一律 ZCL_ / YCL_;SAP 標準是 CL_
可見範圍只有該支程式看得到整個系統都能用,可被別的程式、BAdI、RAP 引用
什麼時候用ALV 事件 handler、只給這支程式用的小工具共用邏輯、要被單元測試、要在 SE24 被 F4 找到
類比程式內的 FORMFunction Module(SE37)
老師特別把「SE24 vs SE37」擺在一起講:SE37 的 Function Module 是「一包程序」,SE24 的 Class 是「一包狀態 + 一包程序」。學 OO 的第一個練習,就是用 SE24 打開 CL_GUI_CUSTOM_CONTAINER,觀察三個頁籤:Attributes(屬性)/Methods(方法)/Events(事件)——不必看懂全部,重點是建立「原來一個 SAP 標準物件長這樣」的直覺。

類別的完整宣告骨架(DEFINITION / IMPLEMENTATION 分離):

"====================================================================
" 本地類別:定義區(DEFINITION)——只宣告「有什麼」
"====================================================================
CLASS lcl_flight DEFINITION.

  PUBLIC SECTION.
    "-- 常數:全大寫,只能讀不能寫
    CONSTANTS: c_currency TYPE waers VALUE 'TWD'.

    "-- 靜態屬性(類別屬性):所有物件共用同一份
    CLASS-DATA: gv_count TYPE i.

    "-- 實例屬性:每個物件各有一份
    DATA: mv_carrid TYPE sflight-carrid READ-ONLY,
          mv_connid TYPE sflight-connid READ-ONLY.

    "-- 建構子:CREATE OBJECT / NEW #( ) 時自動執行
    METHODS constructor
      IMPORTING iv_carrid TYPE sflight-carrid
                iv_connid TYPE sflight-connid.

    "-- 一般實例方法
    METHODS get_seats_free
      RETURNING VALUE(rv_free) TYPE i.

    METHODS describe
      RETURNING VALUE(rv_text) TYPE string.

    "-- 靜態方法:不需要物件就能呼叫,用 => 存取
    CLASS-METHODS get_instance_count
      RETURNING VALUE(rv_count) TYPE i.

  PROTECTED SECTION.
    "-- 只有自己和「子類別」看得到
    METHODS read_db.

  PRIVATE SECTION.
    "-- 只有自己看得到(外面完全碰不到)
    DATA: ms_flight TYPE sflight.

ENDCLASS.

"====================================================================
" 本地類別:實作區(IMPLEMENTATION)——寫「怎麼做」
"====================================================================
CLASS lcl_flight IMPLEMENTATION.

  METHOD constructor.
    mv_carrid = iv_carrid.
    mv_connid = iv_connid.
    read_db( ).                    " 呼叫自己的 protected 方法,不必寫 me->
    gv_count = gv_count + 1.       " 靜態屬性:每建一個物件就 +1
  ENDMETHOD.

  METHOD read_db.
    "-- 明列欄位,主機變數一律加 @
    SELECT SINGLE carrid, connid, fldate, seatsmax, seatsocc
      FROM sflight
      WHERE carrid = @mv_carrid
        AND connid = @mv_connid
      INTO CORRESPONDING FIELDS OF @ms_flight.
  ENDMETHOD.

  METHOD get_seats_free.
    rv_free = ms_flight-seatsmax - ms_flight-seatsocc.
  ENDMETHOD.

  METHOD describe.
    "-- 字串範本比 CONCATENATE 清楚得多
    rv_text = |{ mv_carrid }-{ mv_connid } 剩餘座位 { get_seats_free( ) }|.
  ENDMETHOD.

  METHOD get_instance_count.
    rv_count = gv_count.
  ENDMETHOD.

ENDCLASS.
可見性區段誰能存取實務原則
PUBLIC SECTION任何人(外部程式、子類別、自己)只放「對外承諾的介面」。屬性盡量不要放 public,要放就加 READ-ONLY
PROTECTED SECTION自己 + 所有子類別給繼承用的鉤子(hook)方法放這裡
PRIVATE SECTION只有自己(同一個類別)預設全部放這裡,需要才往上開。這就是「封裝」
口訣:「屬性 private、方法 public、繼承用 protected」。想改屬性就寫 SET_ 方法,想讀就寫 GET_ 方法或加 READ-ONLY——這樣未來欄位改型別,外部程式不必跟著改。

3. 建立物件:CREATE OBJECT 與 NEW #( )、參照變數的生死

ABAP 的物件變數叫做參照變數(Reference Variable),宣告時用 TYPE REF TO。它本身不是物件,只是一根指向物件的指標;剛宣告時是 INITIAL(空指標),必須「建立物件」之後才指得到東西。

"====================================================================
" 三種建立物件的寫法(結果相同,新程式一律用第三種)
"====================================================================
DATA lo_flight TYPE REF TO lcl_flight.

"-- 舊:CREATE OBJECT(講義原文的寫法,S/4 仍可用但不推薦)
CREATE OBJECT lo_flight
  EXPORTING
    iv_carrid = 'AA'
    iv_connid = '0017'.

"-- 中:NEW 建構式運算子,變數已宣告 → 型別可用 #
lo_flight = NEW #( iv_carrid = 'AA' iv_connid = '0017' ).

"-- 新(推薦):內嵌宣告 + NEW,一行搞定
DATA(lo_flight2) = NEW lcl_flight( iv_carrid = 'AA' iv_connid = '0017' ).

"-- 呼叫方法:實例用 ->,靜態用 =>
DATA(lv_free) = lo_flight2->get_seats_free( ).
DATA(lv_txt)  = lo_flight2->describe( ).
DATA(lv_cnt)  = lcl_flight=>get_instance_count( ).   " 靜態方法不需要物件

"-- 判斷參照是否有效:用 IS BOUND,不要用 IS INITIAL 判斷「物件活著沒」
IF lo_flight2 IS BOUND.
  cl_demo_output=>write( lv_txt ).
ENDIF.

"-- 解除參照(讓 Garbage Collector 有機會回收)
CLEAR lo_flight2.        " 或 FREE lo_flight2.
講義提醒:CREATE OBJECT 這個關鍵字後面「接冒號」或「不接冒號」都可以——CREATE OBJECT: a EXPORTING ... 是鏈式寫法。混用時很容易漏掉句點,這是初學者第一個常見編譯錯誤。
參照變數 ≠ 物件lo_a = lo_b 是把「指標」複製過去,兩個變數指向同一個物件,改 A 就會影響 B。要真正複製內容必須自己寫 copy 方法或用 CORRESPONDING #( ) 搬結構。

4. 建構子、靜態成員、繼承、多型、介面、FRIENDS

4-1 建構子 CONSTRUCTOR 與 CLASS_CONSTRUCTOR

  • CONSTRUCTOR(實例建構子):每次建立物件時自動執行一次,只能有 IMPORTING 參數與 RAISING不能有 RETURNING / EXPORTING
  • CLASS_CONSTRUCTOR(靜態建構子):整個程式執行期間,在第一次用到這個類別之前自動執行一次,沒有任何參數。適合載入設定檔、讀常數表。
  • 子類別的 CONSTRUCTOR 必須先呼叫 super->constructor( ) 才能碰自己的屬性。
CLASS lcl_config DEFINITION CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS class_constructor.          " 靜態建構子,無參數
    CLASS-METHODS get_plant_name
      IMPORTING iv_werks       TYPE werks_d
      RETURNING VALUE(rv_name) TYPE name1.
  PRIVATE SECTION.
    CLASS-DATA gt_t001w TYPE SORTED TABLE OF t001w
                        WITH UNIQUE KEY werks.
ENDCLASS.

CLASS lcl_config IMPLEMENTATION.

  METHOD class_constructor.
    "-- 全程式只會跑這一次:把工廠主檔一次讀進記憶體當快取
    SELECT werks, name1
      FROM t001w
      INTO CORRESPONDING FIELDS OF TABLE @gt_t001w.
  ENDMETHOD.

  METHOD get_plant_name.
    "-- 用 line_exists 判斷,比 READ TABLE + sy-subrc 乾淨
    IF line_exists( gt_t001w[ werks = iv_werks ] ).
      rv_name = gt_t001w[ werks = iv_werks ]-name1.
    ELSE.
      rv_name = |工廠 { iv_werks } 不存在|.
    ENDIF.
  ENDMETHOD.

ENDCLASS.
CLASS ... DEFINITION CREATE PRIVATE 表示「外面不准 NEW 我」,只能透過我提供的靜態工廠方法取得實例——這就是 Singleton/Factory 樣式。CL_SALV_TABLE=>FACTORY( ) 用的正是這一招,所以你永遠不會寫 NEW cl_salv_table( )

4-2 繼承 INHERITING FROM、多型、REDEFINITION

"-- 父類別:抽象類別不能被實例化,只能被繼承
CLASS lcl_doc DEFINITION ABSTRACT.
  PUBLIC SECTION.
    METHODS constructor IMPORTING iv_id TYPE char10.
    "-- 抽象方法:只有簽章沒有實作,子類別「必須」實作
    METHODS get_title ABSTRACT RETURNING VALUE(rv_title) TYPE string.
    "-- 一般方法:子類別可以選擇性覆寫
    METHODS print.
  PROTECTED SECTION.
    DATA mv_id TYPE char10.
ENDCLASS.

CLASS lcl_doc IMPLEMENTATION.
  METHOD constructor.
    mv_id = iv_id.
  ENDMETHOD.
  METHOD print.
    cl_demo_output=>write( |【{ get_title( ) }】{ mv_id }| ).
  ENDMETHOD.
ENDCLASS.

"-- 子類別 1:銷售訂單
CLASS lcl_so DEFINITION INHERITING FROM lcl_doc.
  PUBLIC SECTION.
    METHODS get_title REDEFINITION.
ENDCLASS.
CLASS lcl_so IMPLEMENTATION.
  METHOD get_title.
    rv_title = '銷售訂單'.
  ENDMETHOD.
ENDCLASS.

"-- 子類別 2:採購單,FINAL 表示不准再被繼承
CLASS lcl_po DEFINITION INHERITING FROM lcl_doc FINAL.
  PUBLIC SECTION.
    METHODS get_title REDEFINITION.
    METHODS print     REDEFINITION.
ENDCLASS.
CLASS lcl_po IMPLEMENTATION.
  METHOD get_title.
    rv_title = '採購單'.
  ENDMETHOD.
  METHOD print.
    super->print( ).                       " 先做父類別原本要做的事
    cl_demo_output=>write( '(需採購主管核准)' ).
  ENDMETHOD.
ENDCLASS.

"-- 用父類別參照當內表行型別,才裝得下所有子類別的物件
TYPES ty_doc_tab TYPE STANDARD TABLE OF REF TO lcl_doc WITH EMPTY KEY.

START-OF-SELECTION.
  "-- 多型(Polymorphism):同一個父型別的參照,跑出不同行為
  DATA(lt_docs) = VALUE ty_doc_tab(
      ( NEW lcl_so( '4500001234' ) )
      ( NEW lcl_po( '4700005678' ) ) ).

  LOOP AT lt_docs INTO DATA(lo_doc).
    lo_doc->print( ).      " 呼叫的是同一個 print,執行的卻是各自的版本
  ENDLOOP.
關鍵字意思常見用途
INHERITING FROM繼承(ABAP 只支援單一繼承共用邏輯放父類別
ABSTRACT(類別)不能被實例化當共同基底
ABSTRACT(方法)只有簽章沒有實作,子類別必須 REDEFINITION強制子類別填空
FINAL(類別)不能再被繼承大部分實作類別都該加 FINAL
FINAL(方法)子類別不能再覆寫這個方法保護關鍵演算法
REDEFINITION覆寫父類別方法(不可改簽章多型的實作手段
super->呼叫父類別的版本擴充而非取代原行為
me->指向自己這個物件區分同名的區域變數與屬性
REDEFINITION 不能改參數。想「多加一個參數」只能新增一個方法,或改用介面。另外 PRIVATE 的方法不能被 REDEFINITION——要讓子類別能覆寫,父類別至少要放 PROTECTED

4-3 介面 INTERFACE:ABAP 版的「多重繼承」

"-- 介面:只有宣告,沒有實作;沒有可見性區段(一律 public)
INTERFACE lif_printable.
  METHODS to_line RETURNING VALUE(rv_line) TYPE string.
  CONSTANTS c_sep TYPE c LENGTH 1 VALUE ';'.
ENDINTERFACE.

INTERFACE lif_checkable.
  METHODS is_valid RETURNING VALUE(rv_ok) TYPE abap_bool.
ENDINTERFACE.

"-- 一個類別可以實作「多個」介面,這就是 ABAP 的多重繼承替代方案
CLASS lcl_material DEFINITION FINAL.
  PUBLIC SECTION.
    INTERFACES: lif_printable,
                lif_checkable.
    METHODS constructor IMPORTING iv_matnr TYPE matnr.
  PRIVATE SECTION.
    DATA mv_matnr TYPE matnr.
ENDCLASS.

CLASS lcl_material IMPLEMENTATION.
  METHOD constructor.
    mv_matnr = iv_matnr.
  ENDMETHOD.

  "-- 實作介面方法時,名稱要用「介面名~方法名」(波浪號)
  METHOD lif_printable~to_line.
    rv_line = |MATNR{ lif_printable=>c_sep }{ mv_matnr }|.
  ENDMETHOD.

  METHOD lif_checkable~is_valid.
    SELECT SINGLE @abap_true
      FROM mara
      WHERE matnr = @mv_matnr
      INTO @rv_ok.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  "-- 用介面型別的參照持有物件:呼叫端只認介面,不認實作類別
  DATA(lo_p) = CAST lif_printable( NEW lcl_material( '000000000000001234' ) ).
  cl_demo_output=>write( lo_p->to_line( ) ).

  "-- 想換回別的介面 / 檢查型別,用 CAST 搭配 TRY,或用 IS INSTANCE OF
  DATA(lo_obj) = CAST object( lo_p ).
  IF lo_obj IS INSTANCE OF lif_checkable.
    DATA(lo_c) = CAST lif_checkable( lo_obj ).
    cl_demo_output=>write( COND string( WHEN lo_c->is_valid( ) THEN '料號存在'
                                        ELSE '料號不存在' ) ).
  ENDIF.
介面在 SAP 標準隨處可見,而且是最好的「白名單」:IF_SALV_C_SELECTION_MODEIF_SALV_C_REFRESHIF_ABAP_BEHAVIOR_HANDLER(RAP)、IF_OO_ADT_CLASSRUN(Eclipse 的 main 方法)。看到 IF_ 開頭就知道是介面。

4-4 FRIENDS:把私有大門開一條縫

"-- 讓 lcl_alv_handler 可以直接存取 lcl_report 的 PRIVATE 成員
CLASS lcl_alv_handler DEFINITION DEFERRED.      " 前向宣告(尚未定義先報名)

CLASS lcl_report DEFINITION FINAL
  FRIENDS lcl_alv_handler.                      " 我把 private 開放給這位朋友
  PUBLIC SECTION.
    METHODS run.
  PRIVATE SECTION.
    DATA mt_data TYPE STANDARD TABLE OF sflight WITH EMPTY KEY.
ENDCLASS.
FRIENDS 是單向的,而且會破壞封裝。實務上只在兩個地方合理使用:(1) 單元測試類別要看被測類別的內部狀態(LOCAL FRIENDS ltcl_test);(2) 同一支程式裡 ALV 事件 handler 需要直接改主程式的內表。除此之外請改用 public 方法。

5. 事件 EVENTS:RAISE EVENT 與 SET HANDLER

事件是 OOALV 的靈魂——「使用者雙擊了某一格」這件事,是 CL_GUI_ALV_GRID 物件丟出來(RAISE)的,你要事先登記(SET HANDLER)才收得到。先用自製類別把機制看懂:

"====================== 事件發送端 ======================
CLASS lcl_stock DEFINITION.
  PUBLIC SECTION.
    "-- 宣告事件;EXPORTING 的參數一律是 VALUE( ) 傳值
    EVENTS: below_safety
              EXPORTING VALUE(ev_matnr) TYPE matnr
                        VALUE(ev_qty)   TYPE menge_d.
    METHODS issue IMPORTING iv_matnr TYPE matnr
                            iv_qty   TYPE menge_d.
  PRIVATE SECTION.
    DATA mv_onhand TYPE menge_d VALUE 100.
ENDCLASS.

CLASS lcl_stock IMPLEMENTATION.
  METHOD issue.
    mv_onhand = mv_onhand - iv_qty.
    IF mv_onhand < 20.
      "-- 丟出事件;有登記的 handler 才會被叫到,沒人登記就自動忽略
      RAISE EVENT below_safety
        EXPORTING ev_matnr = iv_matnr
                  ev_qty   = mv_onhand.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

"====================== 事件接收端 ======================
CLASS lcl_alert DEFINITION.
  PUBLIC SECTION.
    "-- FOR EVENT 事件名 OF 類別名;IMPORTING 只列你想收的參數(名稱要一致)
    METHODS on_low FOR EVENT below_safety OF lcl_stock
      IMPORTING ev_matnr ev_qty sender.
ENDCLASS.

CLASS lcl_alert IMPLEMENTATION.
  METHOD on_low.
    "-- sender 是系統自動提供的:誰丟出這個事件
    MESSAGE |料號 { ev_matnr } 低於安全庫存,剩 { ev_qty NUMBER = USER }| TYPE 'I'.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  DATA(lo_stock) = NEW lcl_stock( ).
  DATA(lo_alert) = NEW lcl_alert( ).

  "-- 登記:把 lo_alert 的 on_low 掛到 lo_stock 這個物件的事件上
  SET HANDLER lo_alert->on_low FOR lo_stock.

  "-- 想一次登記「所有同類別的物件」:FOR ALL INSTANCES
  " SET HANDLER lo_alert->on_low FOR ALL INSTANCES.

  lo_stock->issue( iv_matnr = '000000000000001234' iv_qty = 90 ).

  "-- 取消登記
  SET HANDLER lo_alert->on_low FOR lo_stock ACTIVATION space.
語法說明
EVENTS ev EXPORTING VALUE(p) TYPE t宣告實例事件(參數只能傳值)
CLASS-EVENTS靜態事件,用 FOR ALL INSTANCES 或直接對類別登記
RAISE EVENT ev EXPORTING ...發送事件;同步執行所有 handler,執行完才回到原處
FOR EVENT ev OF cls宣告 handler 方法
IMPORTING ... senderhandler 可以額外收 sender(發送者參照)
SET HANDLER h FOR o登記單一物件
SET HANDLER h FOR ALL INSTANCES登記所有同類別物件(含未來建立的)
... ACTIVATION space取消登記
SET HANDLER 會建立一個「強參照」:只要事件發送者還活著,handler 物件就不會被回收。ALV 程式如果每次 PBO 都 NEW 一個 handler 又 SET HANDLER,記憶體會一路長上去(Memory Leak)。正解是:handler 物件只建立一次(用 IF lo_handler IS NOT BOUND 包起來),這也是本章第 8 節那個 IF wcl_alv IS INITIAL 判斷式真正的用意。

6. 例外處理:TRY / CATCH cx_root

講義第 15 節「從程式師的角度學 OOP」用的是最經典的例子:除以零不再 short dump,而是被 catch 起來。這正是 OO 帶來的最大實務好處之一——類別式例外(Class-based Exception)。

REPORT zrep_error_check_01.

DATA: gv_result TYPE p LENGTH 8 DECIMALS 2.

"-- 現代寫法:例外參照用內嵌宣告 DATA(lx),型別由 CATCH 決定
DATA: gv_dividend TYPE i VALUE 19,
      gv_divisor  TYPE i VALUE 0.

TRY.
    "-- 除以零:舊時代直接 dump,現在會丟 CX_SY_ZERODIVIDE
    gv_result = gv_dividend / gv_divisor.
    cl_demo_output=>write( |結果 = { gv_result }| ).

  CATCH cx_sy_zerodivide INTO DATA(lx_zero).
    "-- 抓「具體」的例外,訊息才有意義
    cl_demo_output=>write( |除數為零:{ lx_zero->get_text( ) }| ).

  CATCH cx_root INTO DATA(lx_root).
    "-- cx_root 是所有例外的共同祖先,放最後當保險絲
    cl_demo_output=>write( lx_root->get_text( ) ).

  CLEANUP.
    "-- 只有在例外「往外傳」時才會執行,用來釋放資源(關檔、解鎖)

ENDTRY.

"-- 另一個常見陷阱:字串取子字串超出長度 → CX_SY_RANGE_OUT_OF_BOUNDS
TRY.
    DATA(lv_s) = CONV string( '1234567890' ).
    DATA(lv_c) = lv_s+11(1).           " 位置 11 超出範圍
  CATCH cx_sy_range_out_of_bounds INTO DATA(lx_rng).
    cl_demo_output=>write( lx_rng->get_text( ) ).
ENDTRY.
例外類別繼承自意義
CX_STATIC_CHECKCX_ROOT語法檢查會強制你 CATCH 或在方法上宣告 RAISING(自訂商業例外用這個)
CX_DYNAMIC_CHECKCX_ROOT執行期才檢查(例:CX_SY_CONVERSION_NO_NUMBER)
CX_NO_CHECKCX_ROOT不強制處理,一路往上傳(例:CX_SY_DYNAMIC_OSQL_SEMANTICS)
自訂例外請在 SE24ZCX_XXX,繼承 CX_STATIC_CHECK,勾「with message class」就能把 T100 訊息掛進去,get_text( ) 直接回傳中文訊息。
不要寫 CATCH cx_root 之後什麼都不做(空的 CATCH 區塊)。這是 SAP 程式碼審查最常被打槍的寫法,錯誤被吃掉之後線上問題完全查不到。至少要 MESSAGE 或寫 Application Log(BAL_*)。

7. Screen 設計入門:SE51、PBO / PAI、MODULE

OOALV 的第一步不是寫程式,而是畫一個螢幕。這一節把 Dialog 螢幕的基本結構講清楚,第 4-2 章會再深入 Tabstrip 與 Table Control。

7-1 一個 Screen 由四個部分組成

組成SE51 頁籤 / 工具內容
Attributes(屬性)AttributesScreen number、Short description、Screen type(Normal/Subscreen/Modal dialog box)、Next screen、OK code 欄位名稱
Element list(元素清單)Element list畫面上每個欄位的名稱、型別、長度、輸入/輸出屬性
Layout(版面)Layout(Screen Painter)用滑鼠拉出 Text、I/O Field、Checkbox、Radio button、Pushbutton、Frame、Custom Control、Subscreen area、Tabstrip、Table Control
Flow logic(流程邏輯)Flow logicPROCESS BEFORE OUTPUT / PROCESS AFTER INPUT / PROCESS ON VALUE-REQUEST(F4)/ PROCESS ON HELP-REQUEST(F1)

7-2 事件流程:PBO → 使用者操作 → PAI

  • PBO(Process Before Output):畫面顯示之前執行。負責:SET PF-STATUSSET TITLEBAR、把內部變數的值搬到螢幕欄位、LOOP AT SCREEN 控制欄位顯示/唯讀、建立 ALV 物件。
  • 畫面顯示,使用者輸入、按按鈕或選單。
  • PAI(Process After Input):使用者按下任何功能鍵之後執行。負責:檢核輸入、依 OK_CODE 決定下一步。
  • 螢幕欄位與程式中同名的全域變數會自動雙向搬運(PBO 前程式→螢幕,PAI 前螢幕→程式)。這是 Dialog 程式最重要的潛規則。
*-- SE51 → Flow logic(螢幕 8000)------------------------------------
PROCESS BEFORE OUTPUT.
  MODULE status_8000.          " 設狀態列與標題
  MODULE display_alv.          " 建立 / 刷新 ALV

PROCESS AFTER INPUT.
  MODULE exit_8000 AT EXIT-COMMAND.   " 型別 E 的功能碼,跳過所有檢核先執行
  MODULE user_command_8000.

*-- 欄位層級檢核:CHAIN ... ENDCHAIN 讓整組欄位一起「亮起來」可重新輸入
  CHAIN.
    FIELD: p_werks, p_matnr.
    MODULE check_material ON CHAIN-REQUEST.   " 只有值被改過才執行
  ENDCHAIN.

  FIELD p_menge MODULE check_qty ON REQUEST.  " 單一欄位檢核

*-- F4(搜尋說明)與 F1(欄位說明)
PROCESS ON VALUE-REQUEST.
  FIELD p_matnr MODULE f4_matnr.

PROCESS ON HELP-REQUEST.
  FIELD p_matnr MODULE f1_matnr.
CHAIN / FIELD 的關鍵作用:在 FIELDCHAIN 區塊內的 MODULE 中發出 MESSAGE ... TYPE 'E',只有該欄位(或整個 CHAIN 內的欄位)會變成可輸入,其他欄位鎖住;若 MODULE 沒有寫在 FIELD/CHAIN 裡就丟 E 訊息,整個畫面會全部鎖死,使用者只能按「取消」離開——這是 Dialog 程式最常見的災難。

7-3 OK_CODE 的正確處理方式

*-- 主程式宣告:兩個變數,一個接畫面、一個給邏輯用
DATA: ok_code TYPE sy-ucomm,      " 螢幕 Attributes 的「OK code」欄位要填 OK_CODE
      save_ok TYPE sy-ucomm.

MODULE user_command_8000 INPUT.

  save_ok = ok_code.
  CLEAR ok_code.        "★★ 一定要清空!否則下一次進 PAI 會沿用舊功能碼

  CASE save_ok.
    WHEN 'BACK' OR 'EXIT' OR 'CANCEL'.
      "-- LEAVE TO SCREEN 0:回到呼叫者(正常退出)
      LEAVE TO SCREEN 0.

    WHEN 'ERP300'.                      " 自訂按鈕(講義 Home Work 第 3 題)
      MESSAGE 'ERP300' TYPE 'I'.
      LEAVE PROGRAM.                    " 直接結束整支程式

    WHEN 'REFRESH'.
      PERFORM get_data.                 " 或呼叫方法重讀資料
      go_grid->refresh_table_display(
        EXPORTING is_stable = VALUE lvc_s_stbl( row = abap_true col = abap_true )
        EXCEPTIONS finished = 1 OTHERS = 2 ).

    WHEN OTHERS.
  ENDCASE.

ENDMODULE.

*-- AT EXIT-COMMAND 專用模組(功能碼型別必須在 SE41 設成 E)
MODULE exit_8000 INPUT.
  CASE ok_code.
    WHEN 'ECAN'.
      LEAVE TO SCREEN 0.
  ENDCASE.
ENDMODULE.
離開指令行為用在哪
LEAVE TO SCREEN 0結束目前螢幕,回到呼叫它的地方(呼叫堆疊往回一層)最標準的退出方式
LEAVE TO SCREEN 9000跳到指定螢幕(不回原處)畫面串接
SET SCREEN 0. LEAVE SCREEN.等同於 LEAVE TO SCREEN 0(舊寫法)維護舊碼會看到
LEAVE PROGRAM直接結束整支程式(不管堆疊)講義範例用它;但若程式被 SUBMIT 呼叫會一起被砍,正式程式建議用 LEAVE TO SCREEN 0
LEAVE TO TRANSACTION 'VA03'跳到別的交易碼單據跳轉
CALL SCREEN 8100 STARTING AT 10 5 ENDING AT 80 20彈出對話視窗(螢幕型別要設 Modal dialog box)明細視窗
講義親身踩過的坑:「測試時若無法返回,應是 NEXT SCREEN 沒清空。」
・SE51 的 Next screen 若留空 → 執行完 PAI 之後停在原畫面(自己叫自己),這是互動式畫面要的行為。
・若填 0 → PAI 跑完就離開。
・老師 2024/12/27 的補充:「畫面上按自己追加的鈕才會結束(注意 SCREEN 的 NEXT SCREEN 也要設 8000)」——填自己的螢幕號 8000,效果等同留空,都是「留在原畫面」,這樣才有機會讓自訂按鈕決定何時退出。可參考程式 Z2409_OO_300

7-4 GUI 狀態列與標題列(SE41 Menu Painter)

講義第 10 節的做法是複製 SAP 標準狀態列,這是最省事的一招:

  • SE41 → 功能表 Status → Copy Status。
  • 來源程式:SAPLSLVC_FULLSCREEN,來源狀態:STANDARD_FULLSCREEN
  • 目標程式:你的程式(例:Z202202_27),目標狀態:STANDARD
  • 複製過來就有完整的 ALV 工具列(排序、篩選、Excel 下載、版面配置)與 BACK/EXIT/CANCEL 三個按鈕。
  • (依講義步驟整理)複製後回 SE41 打開 STANDARD,在 Application toolbar 空格填自己的功能碼(例 ERP300)、按 F6 設定圖示與文字,並確認 BACK/EXIT/CANCEL 的功能碼型別為 E(Exit command)。
MODULE status_8000 OUTPUT.
  SET PF-STATUS 'STANDARD'.
  SET TITLEBAR  'T8000' WITH sy-uname.   " 標題列可帶參數 &1 &2 ...

  "-- 講義 Home Work 第 4 題:在畫面上顯示您的帳號
  gv_user = sy-uname.                    " 螢幕上放一個同名的輸出欄位即可

  "-- 動態控制畫面元素(第 3-1 章的 LOOP AT SCREEN 在 Dialog 一樣能用)
  LOOP AT SCREEN INTO DATA(ls_screen).
    IF ls_screen-group1 = 'DSP'.
      ls_screen-input = 0.               " 唯讀
      MODIFY SCREEN FROM ls_screen.
    ENDIF.
  ENDLOOP.
ENDMODULE.
想「暫時拿掉某幾個按鈕」不必再複製一個狀態列,用 SET PF-STATUS 'STANDARD' EXCLUDING lt_fcode.,把不要的功能碼放進 lt_fcodeTYPE TABLE OF sy-ucomm)即可。

8. OOALV 十大步驟:CL_GUI_CUSTOM_CONTAINER + CL_GUI_ALV_GRID

講義把整個流程拆成十個步驟,這是本章的主線。先看骨架,再看完整程式。

步驟做什麼關鍵
1SE51 建立螢幕 8000Screen type = Normal;Next screen 留空或填 8000
2Layout 上拉一個 Custom Control,命名 CONTAINER快捷鍵 Ctrl+Shift+F5(善用 C-S-F5);同時建立 OK_CODE 欄位
3主程式定義四個變數Container、Grid、Fieldcat、Layout
4SE24 觀察 CL_GUI_CUSTOM_CONTAINER看 Attributes/Methods/Events
5建立三支 MODULE(STATUS_8000、DISPLAY_ALV、USER_COMMAND_8000)+ 一支 FORM雙擊名稱即可自動產生
6核心:建立 Container 物件,再以它為 I_PARENT 建立 Grid父子關係:Screen → Container → Grid
7取得欄位目錄 FIELDCAT手動填 vs LVC_FIELDCATALOG_MERGE 自動產生
8設定 LAYOUTzebra、grid_title、smalltitle、cwidth_opt
9讀取資料到內表 → SET_TABLE_FOR_FIRST_DISPLAY第二次進來改呼叫 REFRESH_TABLE_DISPLAY
10CALL SCREEN 8000. + SE41 狀態列 + PAI 事件可以試跑了

OO-ALV 的四大控制結構(講義特別列出,全部是 LVC_ 開頭的字典型別,可用 SE11 查):

控制項目參考結構/型別常用欄位
欄位目錄 Field catalogLVC_T_FCAT(行結構 LVC_S_FCAT)fieldname、ref_table、ref_field、coltext、seltext、outputlen、no_out、edit、hotspot、do_sum、just、checkbox
佈局控制 LayoutLVC_S_LAYOzebra、grid_title、smalltitle、cwidth_opt、sel_mode、no_toolbar、excp_fname、info_fname、stylefname、ctab_fname
列印參數LVC_S_PRNTprntgrplst、no_colwopt、reservelns
排序參數LVC_T_SORT(行 LVC_S_SORT)fieldname、spos、up、down、subtot、group
過濾參數LVC_T_FILT(行 LVC_S_FILT)fieldname、sign、option、low、high
LVC vs SLIS:OOALV(CL_GUI_ALV_GRID)用的是 LVC_* 系列型別;舊的 Function ALV 用的是 SLIS_* 系列。兩者不能互相指派。要轉換可用函數 LVC_TRANSFER_TO_SLIS / LVC_TRANSFER_FROM_SLIS。老師的坑:從 2-3 章的範例把 fieldcat 直接複製過來會編不過,就是這個原因。

8-1 講義原始骨架(保留教學脈絡,但已現代化)

*-- 步驟 3:定義變數(名稱刻意與 Pattern 樣板一致,方便產碼)
DATA: wcl_container TYPE REF TO cl_gui_custom_container,  " 存放 ALV 的容器(SE24)
      wcl_alv       TYPE REF TO cl_gui_alv_grid,          " ALV 網格(SE24)
      gt_fieldcat   TYPE lvc_t_fcat,                      " 欄位目錄(SE11)
      gs_layout     TYPE lvc_s_layo.                      " 版面(SE11)

*-- 要顯示的內表:不要再用 WITH HEADER LINE
DATA: gt_list TYPE STANDARD TABLE OF sflight WITH EMPTY KEY.

DATA: ok_code TYPE sy-ucomm,
      save_ok TYPE sy-ucomm.

*-- 步驟 6:核心——建立 Container,再以它為 parent 建立 Grid
MODULE display_alv OUTPUT.

  IF wcl_alv IS INITIAL.               " 第一次進來才建物件

    wcl_container = NEW cl_gui_custom_container(
                      container_name = 'CONTAINER' ).   " 對應 SE51 的元素名稱

    IF wcl_container IS BOUND.
      wcl_alv = NEW cl_gui_alv_grid( i_parent = wcl_container ).
    ENDIF.

    PERFORM prepare_field_catalog CHANGING gt_fieldcat.
    PERFORM prepare_layout        CHANGING gs_layout.
    PERFORM get_data.

    wcl_alv->set_table_for_first_display(
      EXPORTING
        is_layout                     = gs_layout
      CHANGING
        it_outtab                     = gt_list
        it_fieldcatalog               = gt_fieldcat
      EXCEPTIONS
        invalid_parameter_combination = 1
        program_error                 = 2
        too_many_lines                = 3
        OTHERS                        = 4 ).
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE 'S' NUMBER sy-msgno
              WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 DISPLAY LIKE 'E'.
    ENDIF.

  ELSE.                                " 之後每次 PBO 只要刷新
    wcl_alv->refresh_table_display(
      EXPORTING  is_stable = VALUE lvc_s_stbl( row = abap_true col = abap_true )
      EXCEPTIONS finished  = 1
                 OTHERS    = 2 ).
  ENDIF.

ENDMODULE.
講義說「這段程式不是一行一行寫出來的,是利用 Pattern(樣板) 建立的」。做法:SE38 編輯畫面按 Ctrl+F6 → 選 ABAP Objects Patterns → 輸入類別 CL_GUI_ALV_GRID、方法 SET_TABLE_FOR_FIRST_DISPLAY,系統會自動把完整參數列印出來。同理 LVC_FIELDCATALOG_MERGE 也用 Ctrl+F6 選 CALL FUNCTION。

8-2 欄位目錄:手動 vs 自動

*-- 方式 A:手動逐欄填(最靈活,但很冗長)——講義示範
FORM prepare_field_catalog_manual CHANGING pt_fieldcat TYPE lvc_t_fcat.

  "-- 用 VALUE #( ) 一次組完,比 APPEND 一行一行快也好讀
  pt_fieldcat = VALUE #(
    ( fieldname = 'CARRID' ref_table = 'SFLIGHT' ref_field = 'CARRID'
      outputlen = 3  coltext = 'Carrier ID'    seltext = 'Carrier ID' )
    ( fieldname = 'CONNID' ref_table = 'SFLIGHT' ref_field = 'CONNID'
      outputlen = 3  coltext = 'Connection ID' seltext = 'Connection ID' )
    ( fieldname = 'FLDATE' ref_table = 'SFLIGHT' ref_field = 'FLDATE'
      coltext = '航班日期' )
    ( fieldname = 'PRICE'  ref_table = 'SFLIGHT' ref_field = 'PRICE'
      coltext = '票價' do_sum = abap_true just = 'R' )
    ( fieldname = 'SEATSOCC' ref_table = 'SFLIGHT' ref_field = 'SEATSOCC'
      coltext = '已訂位' edit = abap_true )      " 這一欄可編輯
  ).

ENDFORM.

*-- 方式 B:依結構自動產生(本練習採用)
FORM prepare_field_catalog CHANGING pt_fieldcat TYPE lvc_t_fcat.

  CALL FUNCTION 'LVC_FIELDCATALOG_MERGE'
    EXPORTING
      i_structure_name       = 'SFLIGHT'
    CHANGING
      ct_fieldcat            = pt_fieldcat   "★ 用形參,不要直接寫全域 GT_FIELDCAT
    EXCEPTIONS
      inconsistent_interface = 1
      program_error          = 2
      OTHERS                 = 3.
  IF sy-subrc <> 0.
    MESSAGE '欄位目錄產生失敗' TYPE 'E'.
  ENDIF.

  "-- 產生之後再「微調」:這是最實用的組合技
  LOOP AT pt_fieldcat ASSIGNING FIELD-SYMBOL(<fs_fcat>).
    CASE <fs_fcat>-fieldname.
      WHEN 'MANDT'.  <fs_fcat>-no_out  = abap_true.   " 隱藏
      WHEN 'CARRID'. <fs_fcat>-hotspot = abap_true.   " 變成可單擊連結
      WHEN 'PRICE'.  <fs_fcat>-do_sum  = abap_true.   " 自動加總
      WHEN OTHERS.
    ENDCASE.
  ENDLOOP.

ENDFORM.
講義原文寫 ct_fieldcat = GT_FIELDCAT[](直接用全域變數),後面又自己註記「ct_fieldcat = P_GT_FIELDCAT 這樣寫比較好」——老師是對的。方法/副程式裡直接讀寫全域變數,是最難除錯的寫法之一。改寫成方法時更要注意:一律用參數傳遞。
S/4HANA 陷阱LVC_FIELDCATALOG_MERGEi_structure_name 只吃 DDIC 字典結構/資料表。若你的內表是程式內用 TYPES BEGIN OF 自訂的,這個函數會失敗(或必須加 i_internal_tabname + 讓內表是全域宣告)。現代解法是用 CL_SALV_TABLE 產生 fieldcat:cl_salv_table=>factory( ) → get_columns( ),或用 cl_abap_structdescr 自己跑 RTTI。

8-3 Layout 與資料讀取

FORM prepare_layout CHANGING ps_layout TYPE lvc_s_layo.
  ps_layout = VALUE lvc_s_layo(
                zebra      = abap_true          " 斑馬紋
                grid_title = 'Flights'          " 網格標題
                smalltitle = abap_true          " 小字標題
                cwidth_opt = abap_true          " 欄寬自動最佳化
                sel_mode   = 'D'                " D = 多列 + 儲存格皆可選
                no_rowmark = space ).
ENDFORM.

FORM get_data.
  "-- 現代寫法:明列欄位、@ 主機變數、INTO TABLE @gt_list
  SELECT carrid, connid, fldate, planetype,
         seatsmax, seatsocc, price, currency
    FROM sflight
    UP TO 30 ROWS
    INTO CORRESPONDING FIELDS OF TABLE @gt_list.

  IF sy-subrc <> 0.
    MESSAGE 'SFLIGHT 無資料' TYPE 'S' DISPLAY LIKE 'W'.
  ENDIF.
ENDFORM.
講義親自標註的坑:「注意,在 GBIKE 的主機,SFLIGHT 是空的。所以我們會換成 ZVBAP_VBAK。」 很多教學系統的 SAP Flight Data Model 沒有跑過 SE38SAPBC_DATA_GENERATOR 產生資料,所以 SFLIGHT/SPFLI/SBOOK 都是空的。先用 SE16N 確認有沒有資料再寫程式,不然會誤以為程式寫錯。

9. 一支完整可跑的 OOALV(現代化整合版)

把上面所有片段組成一支正式程式。這是本章的核心產出:TOP 宣告 + 本地事件 handler 類別 + PBO + PAI,並且加上選擇畫面條件(對應講義 Home Work 第 2 題)、自訂結束按鈕(第 3 題)、畫面顯示帳號(第 4 題)。

9-1 主程式(TOP 區+類別定義+類別實作)

*&---------------------------------------------------------------------*
*& Report Z2409_OO300   OOALV 完整範例(S/4HANA 現代寫法)
*&---------------------------------------------------------------------*
REPORT z2409_oo300.

*----------------------------------------------------------------------*
* 1. 型別與全域資料
*----------------------------------------------------------------------*
TYPES: BEGIN OF ty_out,
         vbeln TYPE vbak-vbeln,          " 銷售訂單
         posnr TYPE vbap-posnr,          " 項次
         matnr TYPE vbap-matnr,          " 料號
         pstyv TYPE vbap-pstyv,          " 項目類別
         kwmeng TYPE vbap-kwmeng,        " 訂單數量
         netwr  TYPE vbap-netwr,         " 淨值
         waerk  TYPE vbak-waerk,
         erdat  TYPE vbak-erdat,         " 建立日期
         light  TYPE c LENGTH 1,         " 紅綠燈欄位
       END OF ty_out,
       tt_out TYPE STANDARD TABLE OF ty_out WITH EMPTY KEY.

DATA: gt_out    TYPE tt_out,
      gs_layout TYPE lvc_s_layo,
      gt_fcat   TYPE lvc_t_fcat.

DATA: go_container TYPE REF TO cl_gui_custom_container,
      go_grid      TYPE REF TO cl_gui_alv_grid.

DATA: ok_code TYPE sy-ucomm,
      save_ok TYPE sy-ucomm,
      gv_user TYPE sy-uname.              " 螢幕上顯示帳號(Home Work 4)

*----------------------------------------------------------------------*
* 2. 事件處理類別(Local Class)
*    ALV 的雙擊、單擊、工具列、自訂指令都由它接手
*----------------------------------------------------------------------*
CLASS lcl_alv_event DEFINITION FINAL.
  PUBLIC SECTION.
    METHODS:
      "-- 雙擊某一格
      on_double_click  FOR EVENT double_click  OF cl_gui_alv_grid
                         IMPORTING e_row e_column es_row_no,
      "-- 單擊 hotspot 欄位
      on_hotspot_click FOR EVENT hotspot_click OF cl_gui_alv_grid
                         IMPORTING e_row_id e_column_id,
      "-- 在 ALV 工具列上加自己的按鈕
      on_toolbar       FOR EVENT toolbar       OF cl_gui_alv_grid
                         IMPORTING e_object e_interactive,
      "-- 按下自訂按鈕之後
      on_user_command  FOR EVENT user_command  OF cl_gui_alv_grid
                         IMPORTING e_ucomm,
      "-- 儲存格被改動(可編輯 ALV 才需要)
      on_data_changed  FOR EVENT data_changed  OF cl_gui_alv_grid
                         IMPORTING er_data_changed.
ENDCLASS.

CLASS lcl_alv_event IMPLEMENTATION.

  METHOD on_double_click.
    "-- e_row-index 是「畫面上的列號」,es_row_no-row_id 是排序後的實際列號
    READ TABLE gt_out INTO DATA(ls_out) INDEX e_row-index.
    CHECK sy-subrc = 0.

    CASE e_column-fieldname.
      WHEN 'VBELN'.
        "-- 跳到 VA03 顯示銷售訂單(用 SET PARAMETER ID 帶值)
        SET PARAMETER ID 'AUN' FIELD ls_out-vbeln.
        CALL TRANSACTION 'VA03' AND SKIP FIRST SCREEN.
      WHEN 'MATNR'.
        SET PARAMETER ID 'MAT' FIELD ls_out-matnr.
        CALL TRANSACTION 'MM03' AND SKIP FIRST SCREEN.
      WHEN OTHERS.
        MESSAGE |您雙擊了 { e_column-fieldname },第 { e_row-index } 列| TYPE 'I'.
    ENDCASE.
  ENDMETHOD.

  METHOD on_hotspot_click.
    READ TABLE gt_out INTO DATA(ls_out) INDEX e_row_id-index.
    CHECK sy-subrc = 0.
    MESSAGE |訂單 { ls_out-vbeln } 項次 { ls_out-posnr }| TYPE 'S'.
  ENDMETHOD.

  METHOD on_toolbar.
    "-- 在工具列尾端插入分隔線與一顆自訂按鈕
    APPEND VALUE stb_button( butn_type = 3 )                     " 3 = 分隔線
           TO e_object->mt_toolbar.
    APPEND VALUE stb_button( function  = 'ZEXPORT'
                             icon      = '@49@'                  " Excel 圖示
                             quickinfo = '匯出 Excel'
                             text      = '匯出'
                             butn_type = 0 )
           TO e_object->mt_toolbar.
    APPEND VALUE stb_button( function  = 'ZEND'
                             icon      = '@0W@'
                             quickinfo = '結束程式'
                             text      = '結束'
                             butn_type = 0 )
           TO e_object->mt_toolbar.
  ENDMETHOD.

  METHOD on_user_command.
    CASE e_ucomm.
      WHEN 'ZEXPORT'.
        MESSAGE '請改用工具列的 Excel 下載圖示' TYPE 'I'.
      WHEN 'ZEND'.
        "-- Home Work 第 3 題:按了自訂鈕才離開
        LEAVE TO SCREEN 0.
      WHEN OTHERS.
    ENDCASE.
  ENDMETHOD.

  METHOD on_data_changed.
    "-- 檢核使用者改過的每一格
    LOOP AT er_data_changed->mt_good_cells INTO DATA(ls_cell).
      IF ls_cell-fieldname = 'KWMENG' AND ls_cell-value CO ' 0'.
        er_data_changed->add_protocol_entry(
          i_msgid = '00' i_msgty = 'E' i_msgno = '001'
          i_msgv1 = '數量不可為零'
          i_fieldname = ls_cell-fieldname
          i_row_id    = ls_cell-row_id ).
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

ENDCLASS.

DATA go_handler TYPE REF TO lcl_alv_event.

*----------------------------------------------------------------------*
* 3. 選擇畫面(Home Work 第 2 題:訂單日期區間)
*----------------------------------------------------------------------*
"-- SELECT-OPTIONS 的參照變數(不用 TABLES: vbak)
DATA: gv_erdat TYPE vbak-erdat,
      gv_vbeln TYPE vbak-vbeln.

SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001.
SELECT-OPTIONS: s_erdat FOR gv_erdat,               " 訂單建立日期(Home Work 2)
                s_vbeln FOR gv_vbeln.               " 訂單號碼
PARAMETERS:     p_edit  AS CHECKBOX.                " 勾選才允許編輯
SELECTION-SCREEN END OF BLOCK b1.

*----------------------------------------------------------------------*
* 4. 主流程
*----------------------------------------------------------------------*
START-OF-SELECTION.
  PERFORM get_data.
  IF gt_out IS INITIAL.
    MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.
  CALL SCREEN 8000.

9-2 資料讀取(JOIN + 紅綠燈)

FORM get_data.

  CLEAR gt_out.

  "-- 一次 JOIN 抬頭與明細;欄位明列、主機變數加 @
  SELECT k~vbeln, p~posnr, p~matnr, p~pstyv,
         p~kwmeng, p~netwr, k~waerk, k~erdat
    FROM vbak AS k
    INNER JOIN vbap AS p ON p~vbeln = k~vbeln
    WHERE k~erdat IN @s_erdat
      AND k~vbeln IN @s_vbeln
    ORDER BY k~vbeln, p~posnr
    INTO CORRESPONDING FIELDS OF TABLE @gt_out
    UP TO 500 ROWS.

  "-- 用 FIELD-SYMBOL 直接改內表,不必再 MODIFY
  LOOP AT gt_out ASSIGNING FIELD-SYMBOL(<ls>).
    <ls>-light = COND #( WHEN <ls>-netwr >= 100000 THEN '1'    " 紅
                         WHEN <ls>-netwr >=  10000 THEN '2'    " 黃
                         ELSE '3' ).                            " 綠
  ENDLOOP.

ENDFORM.
S/4HANA 提醒:JOIN 之後如果只是要顯示,優先考慮直接建一個 CDS ViewDEFINE VIEW ENTITY),把 JOIN 推到 HANA 資料庫層做,程式端只要 SELECT FROM ZI_SalesOrderItem。這也是「Code Push-Down」的基本功。另外 SELECT ... UP TO n ROWS 若沒有 ORDER BY,HANA 不保證回傳順序——上面範例刻意加了 ORDER BY。

9-3 PBO:建立容器、掛事件、顯示 ALV

*&---------------------------------------------------------------------*
*& Module STATUS_8000 OUTPUT
*&---------------------------------------------------------------------*
MODULE status_8000 OUTPUT.
  SET PF-STATUS 'STANDARD'.          " SE41 由 SAPLSLVC_FULLSCREEN/STANDARD_FULLSCREEN 複製
  SET TITLEBAR  'T8000'.
  gv_user = sy-uname.                " Home Work 4:畫面上顯示帳號
ENDMODULE.

*&---------------------------------------------------------------------*
*& Module DISPLAY_ALV OUTPUT
*&---------------------------------------------------------------------*
MODULE display_alv OUTPUT.

  IF go_grid IS NOT BOUND.
    "================ 第一次:建立所有物件 ================

    "-- 1) 容器:container_name 必須與 SE51 Custom Control 的名稱完全一致
    go_container = NEW cl_gui_custom_container(
                     container_name = 'CONTAINER' ).

    "-- 2) 網格:以容器為 parent
    go_grid = NEW cl_gui_alv_grid( i_parent = go_container ).

    "-- 3) 欄位目錄:自訂結構無法用 MERGE,這裡改用 SALV 產生再微調
    PERFORM build_fieldcat CHANGING gt_fcat.

    "-- 4) 版面:紅綠燈欄位用 excp_fname 指定
    gs_layout = VALUE lvc_s_layo(
                  zebra      = abap_true
                  cwidth_opt = abap_true
                  sel_mode   = 'D'
                  grid_title = |訂單明細({ lines( gt_out ) } 筆)|
                  smalltitle = abap_true
                  excp_fname = 'LIGHT' ).      " 紅綠燈

    "-- 5) 事件 handler:★只建立一次,避免 memory leak / 事件重複觸發
    go_handler = NEW lcl_alv_event( ).
    SET HANDLER go_handler->on_double_click  FOR go_grid.
    SET HANDLER go_handler->on_hotspot_click FOR go_grid.
    SET HANDLER go_handler->on_toolbar       FOR go_grid.
    SET HANDLER go_handler->on_user_command  FOR go_grid.
    SET HANDLER go_handler->on_data_changed  FOR go_grid.

    "-- 6) 可編輯模式(勾了 p_edit 才開)
    IF p_edit = abap_true.
      go_grid->set_ready_for_input( 1 ).
      "-- 按 Enter 就觸發 data_changed,不必等按儲存
      go_grid->register_edit_event(
        i_event_id = cl_gui_alv_grid=>mc_evt_modified ).
    ENDIF.

    "-- 7) 首次顯示
    go_grid->set_table_for_first_display(
      EXPORTING
        is_layout                     = gs_layout
        i_save                        = 'A'      " 允許存版面配置(Layout Variant)
        i_default                     = abap_true
      CHANGING
        it_outtab                     = gt_out
        it_fieldcatalog               = gt_fcat
      EXCEPTIONS
        invalid_parameter_combination = 1
        program_error                 = 2
        too_many_lines                = 3
        OTHERS                        = 4 ).
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE 'S' NUMBER sy-msgno
              WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 DISPLAY LIKE 'E'.
    ENDIF.

  ELSE.
    "================ 之後每次 PBO:只刷新 ================
    go_grid->refresh_table_display(
      EXPORTING  is_stable = VALUE lvc_s_stbl( row = abap_true col = abap_true )
      EXCEPTIONS finished  = 1
                 OTHERS    = 2 ).
  ENDIF.

  "-- 讓所有 GUI 控制項的指令真正送到前端
  cl_gui_cfw=>flush( ).

ENDMODULE.

*&---------------------------------------------------------------------*
*& Form BUILD_FIELDCAT(自訂結構也能產生 fieldcat)
*&---------------------------------------------------------------------*
FORM build_fieldcat CHANGING pt_fcat TYPE lvc_t_fcat.

  TRY.
      "-- 借 SALV 幫我們做 RTTI 分析,再轉成 LVC 格式
      cl_salv_table=>factory(
        IMPORTING r_salv_table = DATA(lo_salv)
        CHANGING  t_table      = gt_out ).

      pt_fcat = cl_salv_controller_metadata=>get_lvc_fieldcatalog(
                  r_columns      = lo_salv->get_columns( )
                  r_aggregations = lo_salv->get_aggregations( ) ).

    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.

  "-- 微調每一欄
  LOOP AT pt_fcat ASSIGNING FIELD-SYMBOL(<fc>).
    CASE <fc>-fieldname.
      WHEN 'VBELN'.  <fc>-coltext = '銷售訂單'. <fc>-hotspot = abap_true.
      WHEN 'POSNR'.  <fc>-coltext = '項次'.
      WHEN 'MATNR'.  <fc>-coltext = '料號'.
      WHEN 'PSTYV'.  <fc>-coltext = '項目類別'.
      WHEN 'KWMENG'. <fc>-coltext = '訂單數量'. <fc>-edit = p_edit.
      WHEN 'NETWR'.  <fc>-coltext = '淨值'.
                     <fc>-do_sum = abap_true.
                     <fc>-cfieldname = 'WAERK'.   " 幣別參照欄
      WHEN 'ERDAT'.  <fc>-coltext = '建立日期'.
      WHEN 'LIGHT'.  <fc>-no_out  = abap_true.    " 紅綠燈欄位本身不顯示
      WHEN OTHERS.
    ENDCASE.
  ENDLOOP.

ENDFORM.

9-4 PAI + Screen Flow Logic

*&---------------------------------------------------------------------*
*& Module USER_COMMAND_8000 INPUT
*&---------------------------------------------------------------------*
MODULE user_command_8000 INPUT.

  save_ok = ok_code.
  CLEAR ok_code.

  "-- 編輯模式:先把使用者改的值收回內表(會觸發 data_changed 事件)
  IF p_edit = abap_true AND go_grid IS BOUND.
    go_grid->check_changed_data( ).
  ENDIF.

  CASE save_ok.
    WHEN 'BACK' OR 'EXIT' OR 'CANCEL'.
      PERFORM free_objects.
      LEAVE TO SCREEN 0.

    WHEN 'ERP300'.                    " 講義 Home Work 第 3 題的自訂按鈕
      MESSAGE 'ERP300' TYPE 'I'.
      PERFORM free_objects.
      LEAVE PROGRAM.

    WHEN 'REFRESH'.
      PERFORM get_data.
      go_grid->refresh_table_display(
        EXPORTING  is_stable = VALUE lvc_s_stbl( row = abap_true col = abap_true )
        EXCEPTIONS OTHERS = 0 ).

    WHEN OTHERS.
  ENDCASE.

ENDMODULE.

*&---------------------------------------------------------------------*
*& Form FREE_OBJECTS —— 離開前釋放控制項,避免記憶體洩漏
*&---------------------------------------------------------------------*
FORM free_objects.
  IF go_grid IS BOUND.
    go_grid->free( EXCEPTIONS OTHERS = 0 ).
    CLEAR go_grid.
  ENDIF.
  IF go_container IS BOUND.
    go_container->free( EXCEPTIONS OTHERS = 0 ).
    CLEAR go_container.
  ENDIF.
  CLEAR go_handler.
  cl_gui_cfw=>flush( ).
ENDFORM.

SE51 螢幕 8000 的 Flow logic(講義第 13 節「完整 Screen 代碼」):

PROCESS BEFORE OUTPUT.
  MODULE status_8000.
  MODULE display_alv.        " 程式也放在 parent class(講義原註)

PROCESS AFTER INPUT.
  MODULE user_command_8000.
SE51 建立步驟速記(依講義步驟整理):SE38 程式編輯畫面 → 對著程式中的 CALL SCREEN 80008000 雙擊 → 系統問要不要建立螢幕 → 填 Short description、Screen type 選 NormalNext screen 留空 → 進 Layout(Screen Painter)→ 左側工具箱點 Custom Control(或按 Ctrl+Shift+F5)→ 在畫面上拉出一塊矩形 → 雙擊它,Name 填 CONTAINER → 再拉一個 I/O Field 命名 GV_USER 並取消 Input(只輸出)→ 存檔啟動 → 回 Attributes 頁籤,OK code 欄填 OK_CODE → 進 Flow logic 把 MODULE 名稱寫上,對著模組名稱雙擊即可自動產生程式框架。

10. CL_GUI_ALV_GRID 常用方法與事件速查

方法用途備註
SET_TABLE_FOR_FIRST_DISPLAY首次顯示只能呼叫一次;it_outtab 是 CHANGING(ALV 直接綁著你的內表)
REFRESH_TABLE_DISPLAY資料變了要重畫is_stable(row/col) = 'X' 才能保留捲軸位置
SET_FRONTEND_FIELDCATALOG / GET_FRONTEND_FIELDCATALOG執行中換欄位目錄改完要 refresh
SET_FRONTEND_LAYOUT / GET_FRONTEND_LAYOUT執行中換版面改標題常用
GET_SELECTED_ROWS / GET_SELECTED_CELLS取得使用者選了哪幾列/哪幾格回傳 lvc_t_row / lvc_t_cell
SET_SELECTED_ROWS程式指定選取列refresh 後要重設
SET_READY_FOR_INPUT( 1 / 0 )整張表切換可編輯/唯讀個別欄位用 fieldcat-edit
REGISTER_EDIT_EVENT設定何時觸發 data_changedmc_evt_enter(按 Enter)/mc_evt_modified(離開欄位)
CHECK_CHANGED_DATA把前端改的值同步回內表PAI 一定要呼叫,否則抓不到使用者輸入
SET_TOOLBAR_INTERACTIVE強制重繪工具列toolbar 事件加按鈕後使用
FREE釋放控制項離開前務必呼叫
事件觸發時機常用 IMPORTING
DOUBLE_CLICK雙擊儲存格e_row、e_column、es_row_no
HOTSPOT_CLICK單擊 hotspot 欄位e_row_id、e_column_id、es_row_no
TOOLBAR工具列繪製時e_object(->mt_toolbar)、e_interactive
USER_COMMAND按下 ALV 工具列的按鈕e_ucomm
AFTER_USER_COMMAND標準功能執行完之後e_ucomm、e_saved、e_not_processed
DATA_CHANGED儲存格內容被改(前端檢核)er_data_changed(->mt_good_cells、add_protocol_entry)
DATA_CHANGED_FINISHED所有變更處理完e_modified、et_good_cells
MENU_BUTTON下拉式按鈕展開e_object、e_ucomm
ONF4自訂欄位的 F4e_fieldname、es_row_no、er_event_data
PRINT_TOP_OF_PAGE列印抬頭
DOUBLE_CLICK 的列號陷阱e_row-index 是「使用者看到的那一列」的索引。一旦使用者排序或篩選過,這個索引就不等於內表 gt_out 的索引!正確做法是用 es_row_no-row_id,或在內表放一個自己的 key 欄位(例如遞增流水號),READ 時用 key 比對而不是 INDEX。

11. 其他容器:SPLITTER / DOCKING / DIALOGBOX / SCREEN0

CL_GUI_CUSTOM_CONTAINER 只是容器家族的一員。所有容器都繼承自 CL_GUI_CONTAINER,所以任何一個都可以當 ALV 的 i_parent

容器類別特性需要 SE51 螢幕嗎
CL_GUI_CUSTOM_CONTAINER放在螢幕上你畫的 Custom Control 區塊裡(且要有 Custom Control 元素)
CL_GUI_DOCKING_CONTAINER停靠在螢幕的上/下/左/右邊,可自動撐滿要有螢幕,但不必畫 Custom Control
CL_GUI_SPLITTER_CONTAINER把一塊區域切成 rows × columns 的格子,每格再取出子容器依 parent 而定
CL_GUI_EASY_SPLITTER_CONTAINER簡化版分割器,只切兩塊,可拖曳依 parent 而定
CL_GUI_DIALOGBOX_CONTAINER獨立的浮動視窗(可移動、可關閉)不必畫元素;要處理 CLOSE 事件
CL_GUI_CONTAINER=>SCREEN0「整個螢幕」這個現成容器(靜態屬性)要有螢幕(如講義第 16 節的 ZREP_CLS_040)
CL_GUI_CONTAINER=>DEFAULT_SCREEN不必自己建螢幕,直接用系統的清單畫面當容器不必(講義第 17 節用的就是它)
*-- A) DOCKING:連 Custom Control 都不必畫,最快做出全螢幕 ALV
DATA(go_dock) = NEW cl_gui_docking_container(
                  repid     = sy-repid
                  dynnr     = sy-dynnr
                  side      = cl_gui_docking_container=>dock_at_left
                  extension = 2000                 " 幾乎佔滿
                  ratio     = 90 ).
DATA(go_grid_d) = NEW cl_gui_alv_grid( i_parent = go_dock ).

*-- B) SPLITTER:上下二塊(講義第 17 節的做法)
DATA(go_split) = NEW cl_gui_splitter_container(
                   parent  = cl_gui_container=>default_screen
                   no_autodef_progid_dynnr = abap_true
                   rows    = 2
                   columns = 1 ).
go_split->set_row_height( id = 1 height = 40 ).      " 上方佔 40%
DATA(go_top)    = go_split->get_container( row = 1 column = 1 ).
DATA(go_bottom) = go_split->get_container( row = 2 column = 1 ).

*-- C) DIALOGBOX:浮動視窗(記得處理 CLOSE 事件,否則按 X 沒反應)
DATA(go_box) = NEW cl_gui_dialogbox_container(
                 width   = 800
                 height  = 400
                 top     = 100
                 left    = 100
                 caption = '明細資料' ).

*-- D) SCREEN0:整個螢幕當容器(講義第 16 節 ZREP_CLS_040 的寫法)
DATA(go_grid_s) = NEW cl_gui_alv_grid( i_parent = cl_gui_container=>screen0 ).
CL_GUI_DIALOGBOX_CONTAINER 一定要 SET HANDLER ... FOR EVENT close,並在 handler 裡呼叫 sender->free( ),否則使用者按右上角 X 之後視窗只是隱藏、物件還活著,再開一次就會出現「Control already exists」的錯誤。

12. CL_SALV_TABLE:三種顯示模式與內嵌到 Screen

SALV(2004 年出的第三代)最大的價值是「同一段程式碼,可以全螢幕顯示,也可以塞進容器」。講義第 16 節的 ZREP_CLS_041 是最精簡的版本:

*&---------------------------------------------------------------------*
*& Report ZREP_CLS_041(2004 年 SALV)—— 現代化改寫
*&---------------------------------------------------------------------*
REPORT zrep_cls_041.

SELECT ebeln, bukrs, bsart, lifnr, ekorg, ekgrp, waers, bedat
  FROM ekko
  UP TO 100 ROWS
  ORDER BY ebeln
  INTO TABLE @DATA(gt_ekko).

TRY.
    "-- 模式一:全螢幕(r_container 不傳)
    cl_salv_table=>factory(
      IMPORTING r_salv_table = DATA(go_table)
      CHANGING  t_table      = gt_ekko ).

    "-- 打開全部標準功能鈕(排序、篩選、Excel、版面)
    go_table->get_functions( )->set_all( abap_true ).
    go_table->get_columns( )->set_optimize( abap_true ).
    go_table->get_display_settings( )->set_striped_pattern( abap_true ).
    go_table->get_display_settings( )->set_list_header( '採購單抬頭 EKKO' ).
    go_table->get_selections( )->set_selection_mode(
      if_salv_c_selection_mode=>row_column ).

    go_table->display( ).

  CATCH cx_salv_msg INTO DATA(lx).
    MESSAGE lx->get_text( ) TYPE 'E'.
ENDTRY.

模式二:內嵌到 SE51 螢幕的 Custom Control——這是很多人不知道 SALV 也能做的事:

MODULE display_salv OUTPUT.

  IF go_salv IS NOT BOUND.

    TRY.
        "-- 用 container_name 直接指定 SE51 的 Custom Control 名稱
        cl_salv_table=>factory(
          EXPORTING
            r_container    = CAST cl_gui_container(
                               NEW cl_gui_custom_container(
                                     container_name = 'CONTAINER' ) )
            container_name = 'CONTAINER'
          IMPORTING
            r_salv_table   = go_salv
          CHANGING
            t_table        = gt_out ).

        "★ 內嵌到自己的螢幕時,SALV 的工具列必須靠 set_screen_status 指定
        "   否則畫面上不會有任何按鈕
        go_salv->set_screen_status(
          pfstatus      = 'SALV_TABLE_STANDARD'
          report        = 'SAPLSALV_METADATA_STATUS'   " 借用標準狀態列
          set_functions = cl_salv_table=>c_functions_all ).

        go_salv->get_columns( )->set_optimize( abap_true ).
        go_salv->get_display_settings( )->set_striped_pattern( abap_true ).

        "-- 抬頭區(TOP-OF-PAGE)用 CL_SALV_FORM_LAYOUT_GRID 排版
        PERFORM build_header CHANGING go_salv.

        "-- 掛雙擊事件
        go_evt_handler = NEW lcl_salv_event( ).
        SET HANDLER go_evt_handler->on_double_click FOR go_salv->get_event( ).

        go_salv->display( ).

      CATCH cx_salv_msg cx_salv_not_found INTO DATA(lx_salv).
        MESSAGE lx_salv->get_text( ) TYPE 'E'.
    ENDTRY.

  ELSE.
    go_salv->refresh( refresh_mode = if_salv_c_refresh=>full ).
  ENDIF.

  cl_gui_cfw=>flush( ).

ENDMODULE.

*&---------------------------------------------------------------------*
*& Form BUILD_HEADER —— 用 SALV 表單物件做出漂亮的抬頭
*&---------------------------------------------------------------------*
FORM build_header CHANGING po_salv TYPE REF TO cl_salv_table.

  DATA(lo_grid) = NEW cl_salv_form_layout_grid( ).

  "-- 第一列跨欄的大標題
  lo_grid->create_header_information(
    row     = 1
    column  = 1
    text    = '銷售訂單明細報表'
    tooltip = 'Sales Order Item Report' ).

  "-- 之後每一列一個「標籤 : 值」
  lo_grid->create_label( row = 2 column = 1 text = '執行人員' ).
  lo_grid->create_text ( row = 2 column = 2 text = CONV string( sy-uname ) ).

  lo_grid->create_label( row = 3 column = 1 text = '執行日期' ).
  lo_grid->create_text ( row = 3 column = 2
                         text = |{ sy-datum DATE = USER } { sy-uzeit TIME = USER }| ).

  lo_grid->create_label( row = 4 column = 1 text = '資料筆數' ).
  lo_grid->create_text ( row = 4 column = 2 text = |{ lines( gt_out ) }| ).

  po_salv->set_top_of_list( lo_grid ).        " 螢幕上的抬頭
  po_salv->set_top_of_list_print( lo_grid ).  " 列印時的抬頭

ENDFORM.
SALV 的三個限制,決定了什麼時候該退回 CL_GUI_ALV_GRID:
1. 不能編輯(SALV 沒有官方 edit 模式)。要讓使用者改資料,就得用 CL_GUI_ALV_GRID。
2. 事件較少:只有 double_click、link_click、added_function、before_salv_function、after_salv_function 等,沒有 data_changed。
3. 內嵌到容器時預設沒有工具列,必須自己呼叫 set_screen_status( );而且 display( ) 之後不能再改欄位設定。
SALV 加自訂按鈕的做法:先在 SE41 複製 SALV_TABLE_STANDARD(來源程式 SAPLSALV_METADATA_STATUS)到自己的程式,加上功能碼;然後 SET HANDLER on_added_function FOR go_salv->get_event( ),在 FOR EVENT added_function ... IMPORTING e_salv_function 裡處理。

13. 進階案例:兩個視窗的 ALV(主檔 → 明細連動)

講義第 17 節 ZOOALV_2_GRID_SIMPLE:上方 MARA 物料主檔,雙擊料號後下方顯示該料號的 MARC 工廠資料。這個案例把 SPLITTER 容器 + SALV + 事件三件事串在一起。以下是完全現代化的改寫版(原文用 FORM 與全域變數,這裡改成一個本地類別,事件 handler 直接存取自己的屬性,不再依賴全域變數)。

*&---------------------------------------------------------------------*
*& Report ZOOALV_2_GRID   雙視窗連動 ALV(S/4HANA 現代寫法)
*&---------------------------------------------------------------------*
REPORT zooalv_2_grid.

TYPES: BEGIN OF ty_mara,
         matnr TYPE mara-matnr,
         mtart TYPE mara-mtart,
         matkl TYPE mara-matkl,
         meins TYPE mara-meins,
       END OF ty_mara,
       BEGIN OF ty_marc,
         matnr TYPE marc-matnr,
         werks TYPE marc-werks,
         ekgrp TYPE marc-ekgrp,
         mmsta TYPE marc-mmsta,
         dispo TYPE marc-dispo,
       END OF ty_marc.

"-- SELECT-OPTIONS 內表的型別(給方法參數用)
TYPES ty_matnr_range TYPE RANGE OF mara-matnr.

*----------------------------------------------------------------------*
* 主控類別:把容器、兩個 SALV、資料、事件全部封裝起來
*----------------------------------------------------------------------*
CLASS lcl_app DEFINITION FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    METHODS run IMPORTING it_matnr TYPE ty_matnr_range.

    "-- 事件處理方法:SALV 的 double_click 帶 row / column
    METHODS on_double_click FOR EVENT double_click OF cl_salv_events_table
      IMPORTING row column.

  PRIVATE SECTION.
    DATA: mt_mara    TYPE STANDARD TABLE OF ty_mara WITH EMPTY KEY,
          mt_marc    TYPE STANDARD TABLE OF ty_marc WITH EMPTY KEY,
          mo_split   TYPE REF TO cl_gui_splitter_container,
          mo_top     TYPE REF TO cl_gui_container,
          mo_bottom  TYPE REF TO cl_gui_container,
          mo_salv_up TYPE REF TO cl_salv_table,
          mo_salv_dn TYPE REF TO cl_salv_table.

    METHODS: get_mara      IMPORTING it_matnr TYPE ty_matnr_range,
             build_screen,
             show_marc     IMPORTING iv_matnr TYPE matnr.
ENDCLASS.

CLASS lcl_app IMPLEMENTATION.

  METHOD run.
    get_mara( it_matnr ).
    IF mt_mara IS INITIAL.
      MESSAGE '查無物料主檔' TYPE 'S' DISPLAY LIKE 'E'.
      RETURN.
    ENDIF.
    build_screen( ).
  ENDMETHOD.

  METHOD get_mara.
    SELECT matnr, mtart, matkl, meins
      FROM mara
      WHERE matnr IN @it_matnr
      ORDER BY matnr
      INTO TABLE @mt_mara
      UP TO 200 ROWS.
  ENDMETHOD.

  METHOD build_screen.

    "-- 用 DEFAULT_SCREEN 當 parent:不必去 SE51 畫任何螢幕
    mo_split = NEW cl_gui_splitter_container(
                 parent                  = cl_gui_container=>default_screen
                 no_autodef_progid_dynnr = abap_true
                 rows                    = 2
                 columns                 = 1 ).
    mo_split->set_row_height( id = 1 height = 45 ).

    mo_top    = mo_split->get_container( row = 1 column = 1 ).
    mo_bottom = mo_split->get_container( row = 2 column = 1 ).

    TRY.
        cl_salv_table=>factory(
          EXPORTING r_container  = mo_top
          IMPORTING r_salv_table = mo_salv_up
          CHANGING  t_table      = mt_mara ).

        mo_salv_up->get_functions( )->set_all( abap_true ).
        mo_salv_up->get_columns( )->set_optimize( abap_true ).
        mo_salv_up->get_display_settings( )->set_list_header( 'MARA 物料主檔(雙擊料號看工廠資料)' ).
        mo_salv_up->get_display_settings( )->set_striped_pattern( abap_true ).
        mo_salv_up->get_selections( )->set_selection_mode(
          if_salv_c_selection_mode=>row_column ).

        "-- 掛事件:handler 就是自己(me),所以能直接改自己的內表
        SET HANDLER me->on_double_click FOR mo_salv_up->get_event( ).

        mo_salv_up->display( ).

      CATCH cx_salv_msg INTO DATA(lx).
        MESSAGE lx->get_text( ) TYPE 'E'.
    ENDTRY.

    cl_gui_cfw=>flush( ).
  ENDMETHOD.

  METHOD on_double_click.
    CHECK column = 'MATNR'.
    "-- row 是列號;用 line_exists 之前先確認索引合法
    CHECK row >= 1 AND row <= lines( mt_mara ).
    show_marc( mt_mara[ row ]-matnr ).
  ENDMETHOD.

  METHOD show_marc.

    CLEAR mt_marc.
    SELECT matnr, werks, ekgrp, mmsta, dispo
      FROM marc
      WHERE matnr = @iv_matnr
      ORDER BY werks
      INTO TABLE @mt_marc.

    IF sy-subrc <> 0.
      MESSAGE |料號 { iv_matnr } 查無 MARC 工廠資料| TYPE 'S' DISPLAY LIKE 'W'.
    ENDIF.

    IF mo_salv_dn IS NOT BOUND.
      "-- 第一次:建立下方 SALV
      TRY.
          cl_salv_table=>factory(
            EXPORTING r_container  = mo_bottom
            IMPORTING r_salv_table = mo_salv_dn
            CHANGING  t_table      = mt_marc ).

          mo_salv_dn->get_functions( )->set_all( abap_true ).
          mo_salv_dn->get_columns( )->set_optimize( abap_true ).
          mo_salv_dn->get_display_settings( )->set_striped_pattern( abap_true ).
          mo_salv_dn->get_display_settings( )->set_list_header(
            |MARC 工廠資料:{ iv_matnr }| ).
          mo_salv_dn->display( ).

        CATCH cx_salv_msg INTO DATA(lx2).
          MESSAGE lx2->get_text( ) TYPE 'E'.
      ENDTRY.

    ELSE.
      "-- 之後:只換標題並 refresh(內表是同一個,SALV 會自動看到新內容)
      mo_salv_dn->get_display_settings( )->set_list_header(
        |MARC 工廠資料:{ iv_matnr }| ).
      mo_salv_dn->refresh( refresh_mode = if_salv_c_refresh=>full ).
    ENDIF.

    cl_gui_cfw=>flush( ).
  ENDMETHOD.

ENDCLASS.

*----------------------------------------------------------------------*
"-- SELECT-OPTIONS 需要一個「已存在的資料物件」當參照,這裡用一個宣告變數
"   (比 TABLES: mara 乾淨,S/4 也不建議再用 TABLES)
DATA gv_matnr TYPE mara-matnr.

SELECTION-SCREEN BEGIN OF BLOCK blk1 WITH FRAME TITLE TEXT-001.
SELECT-OPTIONS s_matnr FOR gv_matnr.
SELECTION-SCREEN END OF BLOCK blk1.

START-OF-SELECTION.
  NEW lcl_app( )->run( s_matnr[] ).

  "★ 關鍵!用 DEFAULT_SCREEN 當容器時,一定要有這一行「產生清單畫面」
  "   否則程式跑完就結束,什麼都看不到
  WRITE space.
講義親自標註的「關鍵!觸發畫面顯示」:用 cl_gui_container=>default_screen 當 parent 時,控制項是掛在「清單處理程序(List Processor)」的畫面上。如果 START-OF-SELECTION 裡完全沒有輸出動作,SAP 不會產生清單畫面,你的 SPLITTER 就沒有地方顯示,程式一閃就結束。所以要補一行 WRITE space.(或 WRITE: /.)。這是雙視窗 ALV 最常見的「明明沒錯卻看不到畫面」。
另一個常見錯誤no_autodef_progid_dynnr = abap_true 不能漏。不加這個參數時,SPLITTER 會嘗試把自己自動註冊到 sy-repid / sy-dynnr,在 DEFAULT_SCREEN 模式下會拋出 CNTL_ERROR
講義的延伸作業:「將檔身換成 VBAP-MATNR、VBAP-VBELN、VBAP-POSNR、VBAP-PSTYV」,也就是上方 MARA、下方改成該料號出現在哪些銷售訂單。做法只要把 show_marc 改成查 VBAP,型別 ty_marc 換掉即可——這正是封裝的好處:改一個方法,其他都不動。老師說「往後可繼續朝三個視窗的 ALV 邁進」,作法是把 SPLITTER 改成 rows = 3 或巢狀 SPLITTER。

14. 記憶體洩漏、物件釋放與 CL_GUI_CFW

GUI 控制項(Container、Grid、Tree、Picture…)是前端物件:ABAP 這邊只有一個代理(proxy),真正的控制項活在 SAP GUI 裡。因此它們有一套自己的生命週期規則。

方法做什麼什麼時候呼叫
cl_gui_cfw=>flush( )把累積的前端指令一次送到 SAP GUI 並等待回應建立/修改控制項之後、需要立即看到效果時。不要在迴圈裡呼叫(每次都是一次網路往返)
cl_gui_cfw=>dispatch( )把前端事件分派給對應的 handlerPAI 中若事件沒被觸發時使用(多數情況 ALV 會自動處理)
lo_ctrl->free( )銷毀前端控制項並釋放資源離開螢幕前、或要重建控制項前
CLEAR lo_ctrl清掉 ABAP 端的參照free( ) 之後緊接著做
*-- 標準的釋放順序:子先於父,handler 最後
METHOD cleanup.

  IF mo_grid IS BOUND.
    "-- 先取消事件登記,切斷循環參照
    SET HANDLER mo_handler->on_double_click FOR mo_grid ACTIVATION space.
    SET HANDLER mo_handler->on_toolbar      FOR mo_grid ACTIVATION space.

    mo_grid->free( EXCEPTIONS cntl_error = 1 cntl_system_error = 2 OTHERS = 3 ).
    CLEAR mo_grid.
  ENDIF.

  IF mo_split IS BOUND.
    mo_split->free( EXCEPTIONS OTHERS = 0 ).
    CLEAR: mo_split, mo_top, mo_bottom.
  ENDIF.

  IF mo_container IS BOUND.
    mo_container->free( EXCEPTIONS OTHERS = 0 ).
    CLEAR mo_container.
  ENDIF.

  CLEAR mo_handler.
  cl_gui_cfw=>flush( ).

ENDMETHOD.
三個真正會出事的記憶體陷阱
1. 每次 PBO 都 NEW 容器:第二次進 PBO 時前端已經有一個叫 CONTAINER 的控制項,會拋 CNTL_ERROR,或畫面上疊了兩層 ALV。所以一定要包 IF ... IS NOT BOUND
2. SET HANDLER 沒解除的循環參照:Grid 指向 handler、handler 指向 Grid,兩邊互相咬住,Garbage Collector 回收不了。長時間執行的 Dialog 交易會慢慢吃光 roll area。
3. ALV 內表是 CHANGING 綁定set_table_for_first_displayit_outtab 是 CHANGING,ALV 直接指向你的內表記憶體。不要在顯示中 REFRESH 或重新指派那個內表變數,會造成 dump 或顯示垃圾資料;要換資料就 CLEAR 後重新 APPEND,再 refresh_table_display( )
除錯工具:ST22 看 dump(找 CNTL_ERRORCNTL_SYSTEM_ERROR)、SAT(ABAP Trace)看是不是 flush 呼叫太多次、SM04 看 session 記憶體。

15. 三代 ALV 完整對照範例

講義第 16 節用同一份 EKKO 資料,寫了三支程式來對照。以下把三支都現代化重寫,放在一起最能看出差別。

*&=====================================================================*
*& 【1996】ZREP_CLS_039 — Function ALV:三行搞定,但功能最陽春
*&=====================================================================*
REPORT zrep_cls_039.

SELECT ebeln, bukrs, bsart, lifnr, waers, bedat
  FROM ekko UP TO 10 ROWS ORDER BY ebeln
  INTO TABLE @DATA(gt_ekko_1).

"(僅供對照,正式程式請勿再用;S/4 ABAP Cloud 不可用)
DATA gt_disp TYPE STANDARD TABLE OF ekko WITH EMPTY KEY.
gt_disp = CORRESPONDING #( gt_ekko_1 ).

CALL FUNCTION 'REUSE_ALV_LIST_DISPLAY'
  EXPORTING i_structure_name = 'EKKO'
  TABLES    t_outtab         = gt_disp.

*&=====================================================================*
*& 【2000】ZREP_CLS_040 — GRID ALV:要有螢幕,但可編輯、事件豐富
*&=====================================================================*
REPORT zrep_cls_040.

DATA: go_alv  TYPE REF TO cl_gui_alv_grid,
      gt_ekko TYPE STANDARD TABLE OF ekko WITH EMPTY KEY.

START-OF-SELECTION.
  SELECT * FROM ekko UP TO 50 ROWS ORDER BY ebeln
    INTO TABLE @gt_ekko.               " 這裡真的要整列,故用 *
  CALL SCREEN 100.

MODULE status_0100 OUTPUT.
  SET PF-STATUS 'SCREEN0'.
  SET TITLEBAR  'OO GRID ALV'.

  IF go_alv IS NOT BOUND.
    "-- 用系統現成的 SCREEN0 容器,不必畫 Custom Control
    go_alv = NEW cl_gui_alv_grid( i_parent = cl_gui_container=>screen0 ).
    go_alv->set_table_for_first_display(
      EXPORTING i_structure_name = 'EKKO'
      CHANGING  it_outtab        = gt_ekko
      EXCEPTIONS OTHERS = 4 ).
  ENDIF.
ENDMODULE.

MODULE user_command_0100 INPUT.
  CASE sy-ucomm.
    WHEN 'BACK' OR 'EXIT' OR 'CANCEL'.
      LEAVE TO SCREEN 0.
  ENDCASE.
ENDMODULE.

*&=====================================================================*
*& 【2004】ZREP_CLS_041 — SALV:不必螢幕,功能全開,程式最短
*&=====================================================================*
REPORT zrep_cls_041.

SELECT ebeln, bukrs, bsart, lifnr, waers, bedat
  FROM ekko UP TO 50 ROWS ORDER BY ebeln
  INTO TABLE @DATA(gt_salv).

TRY.
    cl_salv_table=>factory( IMPORTING r_salv_table = DATA(go_table)
                            CHANGING  t_table      = gt_salv ).
    go_table->get_functions( )->set_all( abap_true ).
    go_table->display( ).
  CATCH cx_salv_msg INTO DATA(lx_msg).
    MESSAGE lx_msg->get_text( ) TYPE 'E'.
ENDTRY.
講義第 16 節提到 CALL SCREEN 100 之後「對著程式中的 100 雙擊,出現畫面照填就好」,這裡兩個地方要注意:(1) 螢幕 100 的 Next screen 要留空;(2) 狀態列 SCREEN0 要在 SE41 建立,至少要有 BACK/EXIT/CANCEL 三個功能碼,否則使用者無法離開,只能關掉 SAP GUI。
S/4HANA 選型建議
・純顯示、不編輯、要快 → CL_SALV_TABLE(首選)。
・要編輯、要 data_changed 檢核、要複雜工具列 → CL_GUI_ALV_GRID
・要跑在 ABAP Cloud / BTP / Fiori → 都不能用,改走 RAP + Fiori Elements;ALV 只存在於 SAP GUI 世界。
REUSE_ALV_* → 只維護舊碼時看得懂即可,不要寫新的。

16. 常見錯誤與除錯清單

症狀原因解法
畫面一片空白,看不到 ALVCustom Control 的名稱與 container_name 不一致(大小寫也算)SE51 元素名稱與程式字串必須完全相同,一律用大寫
執行後閃一下就結束用 DEFAULT_SCREEN 卻沒有任何清單輸出WRITE space.
按 BACK 沒反應/回不去Next screen 沒清空;或狀態列沒有功能碼;或 PAI 沒接 OK_CODENext screen 留空、SE41 補功能碼、Attributes 的 OK code 欄填 OK_CODE
第二次進畫面出現 CNTL_ERROR重複建立同名容器IF go_grid IS NOT BOUND;離開時 free( )
使用者改了值,程式抓不到PAI 沒呼叫 check_changed_data( )PAI 一開始就呼叫
每按一次按鈕就重複執行上一次的動作OK_CODE 沒有 CLEARsave_ok = ok_code. CLEAR ok_code.
雙擊抓到錯的那一列使用者排序過,e_row-index 與內表索引不同步es_row_no-row_id 或自建 key 欄位
refresh 後捲軸跳回第一列沒傳 is_stableis_stable = VALUE lvc_s_stbl( row = 'X' col = 'X' )
LVC_FIELDCATALOG_MERGE 失敗傳的是程式內自訂結構,不是 DDIC 結構改用 cl_salv_controller_metadata=>get_lvc_fieldcatalog,或建一個 DDIC 結構
SFLIGHT 沒資料教學系統未產生 Flight Data執行 SAPBC_DATA_GENERATOR,或改用 EKKO/MARA/ZVBAP_VBAK
fieldcat 型別編譯不過SLIS_* 與 LVC_* 混用OOALV 一律用 LVC_*;必要時用 LVC_TRANSFER_FROM_SLIS 轉換
訊息一出現整個畫面鎖死MODULE 沒寫在 FIELD/CHAIN 區塊裡卻丟 E 訊息用 CHAIN ... FIELD ... MODULE ... ON CHAIN-REQUEST 包起來

練習與自我檢核

  1. 十大步驟複習:不看講義,說出從「SE51 建 8000 畫面」到「CALL SCREEN 8000 跑起來」的十個步驟,並指出哪一步是「原封不動貼上就好、但一定要懂」的核心程式(答案:第 5 節的 DISPLAY_ALV——建立 Container → 以它為 parent 建立 Grid → fieldcat → layout → 資料 → SET_TABLE_FOR_FIRST_DISPLAY)。
  2. SE24 觀察:用 SE24 打開 CL_GUI_CUSTOM_CONTAINERCL_GUI_ALV_GRID,各找出三個 Attributes、三個 Methods、三個 Events,並說明 CL_GUI_ALV_GRID 繼承自哪個類別(提示:CL_GUI_CONTROL → CL_GUI_OBJECT)。
  3. 2024/09 Home Work(原題):把 Z202202_27 做四項改造——
    (1) 資料表由 SFLIGHT 改成 ZVBAP_VBAK(提示:找出程式中所有出現 sflight 的地方逐一修改,包含 fieldcat 的 i_structure_name)。
    (2) 加上查詢條件「訂單日期區間」:SELECT-OPTIONS s_erdat FOR zvbap_vbak-erdat. 放在 CALL SCREEN 8000 之前。
    (3) 設一個按鈕(功能碼 ERP300),按了才退出系統。
    (4) 在畫面上顯示你的帳號(sy-uname)。
  4. 2024/12/27 補充題:把程式改成「只有按自己追加的按鈕才會結束」。注意 SE51 的 Next screen 也要設 8000(或留空),否則 PAI 跑完就自動離開。參考程式 Z2409_OO_300
  5. 十項錯誤修正檢核表(講義原文的 01~10,用來驗收 Z202202_27):
    01 兩個 ACTIVE——程式與螢幕都要啟動;02 8000——螢幕編號一致;03 MARA——把列印內容換成 MARA;04 CONTAINER——Custom Control 名稱與程式字串一致(注意講義寫成 CONTAINTER 是筆誤,這正是最常見的錯字坑);05 BACK——功能碼;06 FIELD_CAT——欄位目錄;07 GUI_STATUS——SE41 狀態列存在且已啟動;08 LEAVE PROGRAM——離開指令;09 CALL SCREEN——主程式要呼叫螢幕;10 SCREEN DESIGN——版面設計完成。逐項打勾。
  6. OOP 基本功:寫一個本地類別 LCL_MATERIAL,屬性 MATNR 放 PRIVATE,提供 CONSTRUCTORGET_DESCRIPTION( )(讀 MAKT)、靜態方法 GET_COUNT( )。再加一個子類別 LCL_MATERIAL_FERT 覆寫 GET_DESCRIPTION,在描述前加上「【成品】」。用一個 LOOP 展示多型。
  7. 介面練習:定義 LIF_EXPORTABLE(方法 TO_CSV),讓 LCL_MATERIAL 與 LCL_ORDER 都實作它;寫一個接收 TYPE REF TO lif_exportable 的方法,證明「呼叫端不必知道實際類別」。
  8. 事件練習:把第 5 節的 LCL_STOCK 範例改成——出貨時若庫存不足直接 RAISE EXCEPTION TYPE zcx_no_stock,低於安全庫存則 RAISE EVENT。說明「例外」與「事件」在語意上的差別(提示:例外是「我做不下去了」,事件是「我做完了,順便通知你」)。
  9. ALV 事件實作:在第 9 節的完整範例上加三件事——(a) 雙擊 VBELN 跳 VA03;(b) 工具列加一顆「全部展開」按鈕;(c) NETWR 大於 100,000 的列整列變紅色(提示:fieldcat 加 stylefname 或 layout 的 info_fname + 內表加一個 char 3 的顏色欄位 'C610')。
  10. 可編輯 ALV:把 KWMENG 設成可編輯,在 DATA_CHANGED 事件檢核「不可為 0、不可小於已交貨量」,錯誤用 add_protocol_entry 回報;PAI 記得 check_changed_data( )
  11. SALV 內嵌:把同一份資料改用 CL_SALV_TABLE 顯示在 SE51 的 Custom Control 裡,並用 CL_SALV_FORM_LAYOUT_GRID 做出四行抬頭(報表名稱、執行者、日期時間、筆數)。說明為什麼一定要 set_screen_status( )
  12. 雙視窗連動:完成講義第 17 節的作業——上方 MARA,下方改成 VBAP-MATNR / VBAP-VBELN / VBAP-POSNR / VBAP-PSTYV。再進階:改成三個視窗(MARA → VBAP → VBEP 交期),用 rows = 3 或巢狀 SPLITTER。
  13. 容器比一比:同一支報表分別用 CUSTOM_CONTAINER、DOCKING_CONTAINER、DEFAULT_SCREEN 顯示,記錄三者在「要不要建螢幕、要不要畫元素、能不能和選擇畫面共存」上的差異。
  14. 記憶體檢核:故意把 SET HANDLER 寫在 PBO 的 IF 判斷之外(每次 PBO 都掛一次),觀察雙擊一次會跳出幾個訊息;再修正回來。這是理解「事件登記會累加」最直接的實驗。
  15. 例外處理:重現講義第 15 節的除以零範例,比較「不加 TRY 會 dump 到 ST22」與「加了 TRY 只印訊息」的差別;再加一個 CLEANUP 區塊,觀察它什麼時候才會被執行。
  16. 三代對照:把同一份 EKKO 資料,分別用 REUSE_ALV_LIST_DISPLAY、CL_GUI_ALV_GRID、CL_SALV_TABLE 寫三支程式,比較程式行數、可用功能、是否需要螢幕。最後回答:如果這支程式明年要搬到 ABAP Cloud,哪一支活得下來?
  17. 觀念總結題:用自己的話說明——為什麼老師說「OOALV 的程式放在各個物件中,不像 Function ALV 一目了然,但更適合規畫複雜系統」?舉一個你自己專案裡「因為用了物件所以好改」或「因為沒用物件所以難改」的例子。

第 3-3 章 增強技術(一):使用者出口 User Exit / 客戶出口 Customer Exit(VMOD / CMOD / SMOD)

SAP 標準功能不可能滿足每一家公司,所以 SAP 在標準程式裡「預留了插座」,讓我們把自己的邏輯插進去,而不必修改標準碼——這就是增強(Enhancement),又叫出口(Exit)。這一章把第一代(基於原始碼的 User Exit:MV45AFZZ、MV50AFZ1、RV60AFZZ、VOFM 常式)與第二代(基於函數模組的 Customer Exit:SMOD/CMOD、EXIT_SAPxxx、螢幕出口、菜單出口、欄位出口)講到可以直接上機。最重要的是:你要學會「怎麼在一片茫茫標準碼裡,找到那個該用的出口」——本章把它整理成可照著做的步驟清單。第三、四代(BAdI/Enhancement Spot)在第 3-4 章。

核心 TCODECMODSMODSE38SE37SE80SE84SE11SE16NSE93SE51SPROVOFMV/06SATSE30ST05SE18SE19SE09SPAUSPDDSPAU_ENHSNOTEVA01ME21N
關鍵物件MV45AFZZ、MV45AFZB、MV45AFZA、MV45AFZD、MV45ATZZ、MV45AOZZ、MV45AIZZ、MV50AFZ1、RV60AFZZ、FV45EFZ1、RV45PFZA、RV64A9xx、LV61A9xx、SAPMV45A、SAPMM06E、SAPLXM06、MM06E005、V45A0003、EXIT_SAPMV45A_003、EXIT_SAPMM06E_006/007/008、ZXVVAU05、ZXM06U38、MODSAP、MODSAPT、TFDIR、ENLFDIR、TSDIR、CUATEXTS、TADIR、TSTC、TRDIR、CI_EKKODB、CI_EKPODB、MODX_FUNCTION_ACTIVE_CHECK、MODX_SUBSCREEN_ACTIVE_CHECK、MODX_ALL_ACTIVE_MENUENTRIES、DYNP_VALUES_READ、CL_SALV_TABLE

1. 增強的概念:什麼時候該用增強

增強(Enhancement)的概念其實非常廣:建在 SAP 標準系統之上的所有二次開發,都可以理解為增強。但 ABAP 開發講的「增強」,特指標準系統事先預留好的介面——SAP 在自己的程式裡寫了一個空的鉤子,我們把程式碼掛上去,執行時就會被呼叫。這種開發稱為增強,也叫出口(Exit)。

老師的一句話定調:「如果增強滿足不了,就只能修正(Modification)。」 修正=改 SAP 標準碼,要申請 Access Key、升級時會在 SPAU 跟你要帳,是最後手段。順序永遠是:Customizing(設定)→ 增強(Exit / BAdI)→ 自建程式(Z*)→ 最後才是修正

1-1.典型的增強需求

  • 業務流程檢查:在某個工廠發貨,設定「在某個庫位的出貨只能使用某種移動類型」;訂單存檔前檢查信用額度、檢查客戶是否黑名單。
  • 介面(欄位)增強:使用者對某個欄位要求大寫,但最終使用者不照規範打,可以在出口中自動轉換;有些模組能自訂資料庫欄位,並在出口中增加欄位輸入;也能對輸入資料做檢查,甚至自動替代(Substitution)。
  • 不規則業務的處理:按某種特殊條件定價——從自己定義的 Z 表中按條件取值再回填價格。
  • 搜尋說明(Search Help)的出口:對 SAP 標準搜尋說明做權限控制,例如業務員只能查到自己負責的客戶。

1-2.出口的四種形態

形態說明典型例子
Function Module Exits(函數出口)最常見。SAP 在標準程式中呼叫一個空的函數,你在函數裡寫邏輯EXIT_SAPMM06E_007、EXIT_SAPMV45A_003
Screen Exits(螢幕出口)SAP 在標準畫面上留一塊子螢幕區域,你自己畫欄位SAPLXM06 螢幕 0101(採購單抬頭客制欄位)
Menu Exits(菜單出口)SAP 在 GUI Status 中預留功能碼(以 + 開頭),你補上文字與程式+CUS 之類的自訂功能碼
Structure Exits(結構出口)用 Append Structure/CI_ 結構把自訂欄位加進標準表或標準結構CI_EKKODB(加到 EKKO)、CI_EKPODB(加到 EKPO)

2. SAP 四代增強技術總覽與選用決策樹

世代技術本質找法/實作 TCODE
第一代User Exit(原始碼增強)SAP 交付一個空的 FORM 子常式,放在名稱倒數第二個字元多半是 Z 的 INCLUDE 裡;你直接在裡面寫碼。可以存取呼叫程式的所有全域資料SPRO 說明文件、SE80 看 Package VMOD、SE38 開主程式找 FORM USEREXIT_*;VOFM 常式
第二代Customer Exit(函數模組出口)SAP 用 CALL CUSTOMER-FUNCTION 'nnn' 呼叫一個空函數 EXIT_<程式名>_<nnn>,函數裡只有一行 INCLUDE ZX…只能用介面參數,不能碰全域變數SMOD 查增強組件、CMOD 建專案並啟用;SE37 看 EXIT_*
第三代Classic BAdI(old BAdI)執行時期才產生實體(run time instantiation),技術本質是一個 Interface。用 CL_EXITHANDLER=>GET_INSTANCE 取得實例,再呼叫介面方法。支援多重實作與篩選器SE18SE19(第 3-4 章)
第四代Kernel BAdI(new BAdI)編譯時期就實體化(compile time),用 GET BADI / CALL BADI,效能較佳SE18SE19(第 3-4 章)
第四代Enhancement-Point(顯式)顯式增強=SAP 手動加入到程式中的 Enhancement Options(ENHANCEMENT-POINT … SPOTS …SE38 原始碼上按「增強」眼鏡圖示(第 3-4 章)
第四代Enhancement-Point(隱式)隱式增強=系統內建的 Enhancement Options(每個 FORM/FUNCTION/METHOD 的頭尾、程式結尾等),不用 SAP 事先安排SE38 →「編輯 → 增強操作 → 顯示隱含增強點」(第 3-4 章)

2-1.選用決策樹(實務順序)

  1. 先問:能不能用 Customizing 解決?(SPRO、條件技術、輸出控制、複製控制)→ 能就不要寫程式。
  2. 有沒有 new/classic BAdI?(SE18 / SE84 找 Enhancement Spot)→ 有就用 BAdI。BAdI 支援多重實作,最不容易和其他顧問打架,也是 SAP 在 S/4HANA 唯一持續增修的介面。
  3. 有沒有第二代 Customer Exit?(SMOD/CMOD)→ 有就用。缺點:一個增強組件在整個 client 只能被一個 CMOD 專案啟用,多顧問搶同一個出口會衝突。
  4. 有沒有第一代 User Exit?(MV45AFZZ 之類)→ SD 很多情境只有第一代做得到(例如 USEREXIT_SAVE_DOCUMENT_PREPARE、USEREXIT_MOVE_FIELD_TO_VBAP),該用還是要用。
  5. 有沒有隱式/顯式 Enhancement Point?→ 都沒有出口時的最後救援(比修改標準碼安全)。
  6. 都沒有 → 才考慮修正(Modification),要 Access Key,升級走 SPAU/SPDD。
老師特別提醒第一代的兩個致命傷:(1)系統升級時可能被新版本覆蓋而失效;(2)因為它看得到全部全域變數,如果你在裡面改了全域變數,可能破壞系統原有的邏輯。所以「光是這一點,就不要輕易碰觸了,瞭解就行」——但既有專案裡到處都是它,你一定要看得懂。

3. 第一代 User Exit:原理與命名規則

第一代的做法是:SAP 提供一個空程式碼的子常式(FORM),在這個子常式中使用者可以加入自己的代碼。它的規律是:

  • 需要修改的 SAP 標準碼,集中在一些名稱倒數第二個字元為 Z 的 INCLUDE 程式中(MV45AFZZ、MV45AOZZ、MV45AIZZ、MV45ATZZ…)。
  • 子常式一般以 USEREXIT_ 起頭。
  • 在出口裡,呼叫程式的所有全域資料都可以使用(XVBAP、XVBEP、VBAK、KOMK…直接拿來用),這是第一代最強、也最危險的地方。
  • 螢幕增強以客戶螢幕(Include Screen)形式發佈,包含在標準程式中,沒有什麼特別規律,要看該模組的 IMG 說明。
  • 表/結構的增強Append Structure
  • 原始碼增強和螢幕增強的說明,可以從 SPRO 後臺設定中相關模組的路徑裡面找到。
第一代出口不需要 Access Key(那些 include 是 SAP 交付給客戶修改的),但它們是 SAP 物件,儲存時會被要求指定 Request,並可能提示是「修改」。這也是為什麼它們在升級時要進 SPAU 對帳。

3-1.VA01 主程式 SAPMV45A 的 INCLUDE 結構(第一代的活教材)

*-----------------------------------------------------------------------
*        Modulpool zur Kundenauftragsbearbeitung   ← VA01/VA02/VA03 主程式
*-----------------------------------------------------------------------
         INCLUDE mv45atop.
ENHANCEMENT-POINT sapmv45a_03 SPOTS es_sapmv45a STATIC.   " ← 第四代顯式增強點
*-----------------------------------------------------------------------
*        U S E R - E X I T S
*-----------------------------------------------------------------------
*        INCLUDE mv45atzz.   " 資料宣告(Data definitions,實際寫在 MV45ATOP 中)
         INCLUDE mv45aozz.   " 使用者模組 PBO
         INCLUDE mv45aizz.   " 使用者模組 PAI
         INCLUDE mv45afza.   " User-forms < 3.0
         INCLUDE mv45afzb.   " User-forms
         INCLUDE mv45afzc.   " User-forms < 3.0D
         INCLUDE mv45afzd.   " User-forms   3.0E
         INCLUDE mv45afzf.   " User-forms   3.0F
*        INCLUDE mv45afzg.   " User-forms   3.1G
         INCLUDE mv45afzh.   " User-forms   4.6B
         INCLUDE mv45afzz.   " User-forms  ← 最常用的一支
口訣:「ZZ 起頭(或倒數第二碼是 Z)的空程式,就是要增強的出口;USEREXIT_ 起頭的 FORM 也是增強的切入點。」 看到 SAPMV45A 這段註解 U S E R - E X I T S,就等於 SAP 直接告訴你插座在哪。

4. 怎麼找第一代出口:三種可操作的方法

先記住這句話第一代增強無法用 Z_FIND_USEREXITS 找出來。 那支程式(第 8 節)掃的是 TADIR 裡的 SMOD/SXSD 物件,也就是第二、三代。第一代是 FORM 子常式,不在 TADIR 裡,只能靠下面三招。

4-1.方法一:利用 SPRO(最正統,附官方說明文件)

  1. 執行 SPRO → 按「SAP 參考 IMG(SAP Reference IMG)」。
  2. 走到目標模組的節點,找名稱含 System Modifications系統修改User Exits 的子節點。SD 的路徑是:Sales and Distribution → System Modifications → User Exits
  3. 點該節點左邊的文件圖示(IMG 說明),SAP 會列出這個領域所有可用的 INCLUDE 與 USEREXIT_* 子常式,以及每一個出口的呼叫時機(例如「呼叫於 FORM BELEG_SICHERN 開頭」)。
  4. 把要用的 INCLUDE 名稱抄下來,到 SE38 開啟它,找到對應的 FORM 寫碼。

(依講義步驟整理)

4-2.方法二:利用 SE80 + Package VMOD

  1. 執行 SE80,物件類型下拉選 Package(套件/開發類別)
  2. 輸入 VMOD(SD 使用者出口的專屬套件;講義寫成「vomd」是筆誤)→ Enter。
  3. 展開 Programs/Includes,就會看到 MV45AFZZ、MV45AFZA、MV45AFZB、MV45AOZZ、MV45AIZZ、MV50AFZ1、RV60AFZZ… 這一整批第一代出口 include。
  4. 雙擊任一支,用「其他功能 → 尋找(Ctrl+F)」搜 USEREXIT_,就能列出這支 include 內所有可用出口。
其他模組也有類似的套件;找不到套件時,改用方法三(從主程式反推)最快,因為那不需要背套件名稱。

4-3.方法三:由 TCODE 找主程式,再從主程式找出口(最通用)

  1. 在目標交易畫面按 系統 → 狀態(System → Status),看「程式(畫面)」欄位,或直接跑 SE93 輸入交易碼看它掛的程式。例如 VA01 → SAPMV45AME21 → SAPMM06EVL01N → SAPMV50AVF01 → SAPMV60A
  2. SE38 開啟該主程式 → 看它的 INCLUDE 清單,找倒數第二個字元是 Z 的 include(多半集中在註解 U S E R - E X I T S 底下)。
  3. 雙擊進入 include,搜尋 FORM USEREXIT_,挑選符合時機的那一支。
  4. 不確定會不會被呼叫?在 FORM 第一行設中斷點,跑一次交易,停下來就是對的;停不下來就換一支。

典型路徑:SAPMV45A ⇒ MV45AFZZ ⇒ FORM USEREXIT_SAVE_DOCUMENT

老師的驗證法:「是否成功,可以靠 DEBUG 來確認;怕影響所有人,可以用 IF 做小範圍的測試。」——所有教學/測試用的出口碼,一律先包一層 IF sy-uname = '你的帳號' AND sy-mandt = '你的 client',這是第一代出口的鐵律,因為它對整個 client 的所有使用者生效

5. 第一代增強實作:訂單存檔前的檢查

最常用的出口是 MV45AFZZ / USEREXIT_SAVE_DOCUMENT_PREPARE——它是存檔前最後一個可以改資料、可以擋下單據的地方(呼叫於 FORM BELEG_SICHERN 的開頭)。

5-1.最小可驗證版本(老師的第一個案例)

*&---------------------------------------------------------------------*
*&      Form  USEREXIT_SAVE_DOCUMENT_PREPARE   (在 INCLUDE MV45AFZZ 中)
*&---------------------------------------------------------------------*
FORM userexit_save_document_prepare.

  " 只在自己的帳號+自己的 client 生效,避免影響其他人
  IF sy-uname = 'ERP000' AND sy-mandt = '540'.
    MESSAGE '第一代增強的方法 MV45AFZZ USEREXIT_SAVE_DOCUMENT_PREPARE' TYPE 'I'.
  ENDIF.

ENDFORM.

5-2.加上日期條件並「真的擋下存檔」

FORM userexit_save_document_prepare.

  IF sy-uname = 'ERP000'
 AND sy-mandt = '540'
 AND sy-datum = '20250501'.

    " 訊息類型 E:直接中止儲存流程,畫面停在原處讓使用者修正
    MESSAGE '第一代增強:MV45AFZZ USEREXIT_SAVE_DOCUMENT_PREPARE 擋單測試' TYPE 'E'.

    " 若只想警示不擋單,改用 TYPE 'I';
    " 若要用旗標控制後續流程,可自行設旗標(此變數需宣告在 MV45ATZZ/MV45ATOP)
    " gf_exit_save_document_prepare = abap_true.
    " EXIT.   " 訊息 E 已會終止,EXIT 可省略
  ENDIF.

ENDFORM.
MESSAGE 型別的坑(S/4HANA 尤其重要):這支出口不只被 VA01/VA02 呼叫,BAPI_SALESORDER_CREATEFROMDAT2、IDOC、Fiori/OData、批次背景作業也都會走到。在那些情境下:
TYPE 'I' / 'W' 會試圖跳出對話框 → 背景或 RFC 會 DUMP(MESSAGE_TYPE_X / 無畫面可顯示)。
TYPE 'E' 在 BAPI 中不會回到 RETURN 表,而是直接讓程式中斷。
正確做法:先判斷 sy-batchsy-binpt/呼叫來源,或把錯誤寫進不完整性紀錄(Incompletion Log);教學測試完務必把程式碼註解掉

5-3.實務範例:搬客戶主檔欄位進 VBAP 的自訂欄位

這是 USEREXIT_MOVE_FIELD_TO_VBAP 的標準用法(呼叫於 FORM VBAP_FUELLEN 結尾)。前提是先用 Append Structure 在 VBAP 加了 ZZ 欄位。

FORM userexit_move_field_to_vbap.

  " vbap / vbak 為呼叫程式的全域工作區,可直接使用
  IF vbap-zzcustgrp IS INITIAL AND vbak-kunnr IS NOT INITIAL.

    SELECT SINGLE kdgrp                     " 客戶群組
      FROM knvv
      WHERE kunnr = @vbak-kunnr
        AND vkorg = @vbak-vkorg
        AND vtweg = @vbak-vtweg
        AND spart = @vbak-spart
      INTO @vbap-zzcustgrp.

    IF sy-subrc <> 0.
      CLEAR vbap-zzcustgrp.
    ENDIF.
  ENDIF.

ENDFORM.
出口是逐筆、逐畫面被呼叫的(每個明細都會進來一次),千萬不要在裡面做整表 SELECT 或迴圈大量 DB 存取,否則 VA01 會慢到不能用。要查的資料請用緩衝(在程式全域內表暫存)或改到 USEREXIT_SAVE_DOCUMENT 一次處理。

5-4.USEREXIT_NUMBER_RANGE:依銷售組織決定號碼範圍

FORM userexit_number_range USING us_range_intern.

  " 依銷售組織(VKORG)/銷售辦公室(VKBUR)決定內部號碼範圍
  us_range_intern = SWITCH #( vbak-vkorg
                              WHEN '1000' THEN '02'
                              WHEN '2000' THEN '03'
                              ELSE us_range_intern ).   " 其餘沿用標準

ENDFORM.

5-5.USEREXIT_SAVE_DOCUMENT:寫自建統計表

FORM userexit_save_document.

  " 此出口在 FORM BELEG_SICHERN 的 COMMIT 之前被呼叫
  " → 這裡的 DB 動作會和單據在同一個 LUW,不需要(也絕對不可以)自己 COMMIT WORK
  DATA lt_log TYPE STANDARD TABLE OF zsd_order_log.

  LOOP AT xvbap INTO DATA(ls_vbap) WHERE updkz <> 'D'.
    APPEND VALUE #( mandt = sy-mandt
                    vbeln = ls_vbap-vbeln
                    posnr = ls_vbap-posnr
                    matnr = ls_vbap-matnr
                    kwmeng = ls_vbap-kwmeng
                    ernam = sy-uname
                    erdat = sy-datum
                    erzet = sy-uzeit ) TO lt_log.
  ENDLOOP.

  IF lt_log IS NOT INITIAL.
    MODIFY zsd_order_log FROM TABLE lt_log.
  ENDIF.

  " 若變更了標準欄位,必須設定 r185d-dataloss = 'X',
  " 系統在安全常式開頭會檢查此旗標,維護自建表時也要一併設定
  " r185d-dataloss = 'X'.

ENDFORM.
絕對不要在任何 User Exit / Customer Exit 裡下 COMMIT WORKROLLBACK WORK。出口是在標準交易的 LUW 中間被呼叫的,你一 COMMIT,SAP 標準的更新流程就被切成兩段,輕則單據不完整、重則資料不一致。要非同步處理請用 CALL FUNCTION … IN UPDATE TASKIN BACKGROUND UNIT

6. SD 第一代出口大全(依 INCLUDE 分類)

下表整理自 IMG 說明「User Exits In Sales Document Processing」。這是查閱型的表,實作時對照時機挑一支就好。

6-1.涉及的程式元件

INCLUDE用途命名規定
MV45ATZZ輸入銷售單據處理用的 metadata(全域資料宣告)使用者自訂 metadata 必須以 ZZ 開頭
MV45AOZZ額外的 PBO 模組(畫面輸出前執行)模組名必須以 ZZ 開頭
MV45AIZZ額外的 PAI 模組(資料輸入後執行,例如資料檢核)模組名必須以 ZZ 開頭
MV45AFZZMV45EFZ1輸入安裝特定的 FORM 常式與使用 user exit;這些元件由 MV45AOZZ/MV45AIZZ 的模組呼叫

6-2.MV45AFZZ 的出口(最常用)

USEREXIT_*用途呼叫時機
USEREXIT_DELETE_DOCUMENT刪除單據時,一併刪掉建單時寫進自建表的資料(例如記錄 ERNAM 的表)FORM BELEG_LOESCHEN 結尾,BELEG_SICHERN 之前
USEREXIT_FIELD_MODIFICATION修改螢幕欄位屬性(可輸入/隱藏/必填)。欄位被分配到修改群組 1–4:群組 1=交易 MFAW 自動修改;群組 2=step loop 欄位含 'LOO';群組 3=依檢查表或固定資訊修改;群組 4=未使用每個螢幕欄位都會呼叫一次;由模組 FELDAUSWAHL 呼叫
USEREXIT_MOVE_FIELD_TO_VBAK給抬頭層新欄位賦值(客戶主檔欄位帶入銷售單據)FORM VBAK_FUELLEN 結尾
USEREXIT_MOVE_FIELD_TO_VBAP給明細層新欄位賦值FORM VBAP_FUELLEN 結尾
USEREXIT_MOVE_FIELD_TO_VBEP給排程行(Schedule Line)新欄位賦值FORM VBEP_FUELLEN 結尾
USEREXIT_MOVE_FIELD_TO_VBKD給商業資料(VBKD)新欄位賦值FORM VBKD_FUELLEN 結尾
USEREXIT_NUMBER_RANGE依欄位(如 VKORG、VKBUR)決定內部單據號碼範圍FORM BELEG_SICHERN 中
USEREXIT_PRICING_PREPARE_TKOMK在定價用的通訊結構 KOMK(抬頭)加欄位/賦值定價前準備
USEREXIT_PRICING_PREPARE_TKOMP在定價用的通訊結構 KOMP(明細)加欄位/賦值定價前準備
USEREXIT_READ_DOCUMENT讀取(VA02/VA03)時要多讀自建表FORM BELEG_LESEN 結尾
USEREXIT_SAVE_DOCUMENT寫使用者自訂的統計更新表。若改動標準欄位,必須設 r185d-dataloss = 'X'FORM BELEG_SICHERN 中、COMMIT 之前
USEREXIT_SAVE_DOCUMENT_PREPARE存檔前做最後的變更或檢查——這是存檔前最後一次改/檢查單據的機會FORM BELEG_SICHERN 開頭

6-3.MV45AFZA 的出口

USEREXIT_*用途
USEREXIT_MOVE_FIELD_TO_KOMKD / _KOMPD物料判定(Material Determination)用的抬頭/明細通訊結構加欄位
USEREXIT_MOVE_FIELD_TO_KOMKG / _KOMPG物料判定與物料列表(Listing/Exclusion)用的通訊結構加欄位
USEREXIT_REFRESH_DOCUMENT單據處理結束、進入下一張單據前,重設客戶自訂欄位(例如上一位售達方的信用額度不可沿用到下一張)。以 F3/F15 離開時執行;呼叫於 FORM BELEG_INITIALISIEREN 結尾

6-4.MV45AFZB 的出口(檢查與判定類)

USEREXIT_*用途
USEREXIT_CHECK_XVBAP_FOR_DELET額外條件成立時「不刪除」明細(與標準相反)
USEREXIT_CHECK_XVBEP_FOR_DELET額外條件成立時「不刪除」排程行
USEREXIT_CHECK_VBAK抬頭層額外檢查(例如某客戶群組只允許某些出貨條件)
USEREXIT_CHECK_VBAP明細層額外檢查
USEREXIT_CHECK_VBKD商業資料額外檢查
USEREXIT_CHECK_VBEP排程行額外檢查(BOM 展開時,把主項目欄位複製到子項目,如開票凍結)
USEREXIT_CHECK_VBSN / USEREXIT_CHECK_XVBSN_FOR_DELET序號的額外檢查/不刪除條件
USEREXIT_FILL_VBAP_FROM_HVBAP用主項目資料填子項目的額外欄位
USEREXIT_MOVE_FIELD_TO_TVCOM_H / _I影響抬頭/明細文字判定(新增判定欄位或改值)
USEREXIT_MOVE_FIELD_TO_COBL在 COBL(會計科目判定用)結構加新欄位
USEREXIT_COBL_RECEIVE_VBAK / _VBAP把抬頭/明細的值指派給 COBL 新欄位
USEREXIT_COBL_SEND_ITEM / _HEADER把 COBL 改過的欄位回寫到明細/抬頭(例如在帳戶指派區塊顯示某欄位)
USEREXIT_SOURCE_DETERMINATION決定出貨工廠。標準是從客戶主檔或客戶物料資訊記錄取,要改規則就寫這裡
USEREXIT_MOVE_FIELD_TO_ME_REQEBAN(採購申請)/EBKN(採購申請帳戶指派)加欄位
USEREXIT_GET_FIELD_FROM_SDCOM變式配置(Variant Configuration):處理 SDCOM 結構中的新欄位再回傳訂單
USEREXIT_MOVE_WORKAREA_TO_SDWA變式配置:格式化額外的工作區
USEREXIT_NEW_PRICING_VBAP / _VBKD設定「什麼情況要重新定價」的前提條件(例如改了某明細欄位、改了客戶群組/價格群組就重跑定價)

6-5.首次資料轉檔(First Data Transfer)專用出口

下列出口只能用於首次資料轉檔,而且只在名稱/欄位不同名時才需要用(同名會自動搬)。
  • USEREXIT_MOVE_FIELD_TO_VBAKKOM/VBAPKOM/VBEPKOM:在抬頭/明細/排程行的通訊結構加欄位。
  • USEREXIT_MOVE_FIELD_TO_VBSN:VBSN(排程協議相關變更狀態)加欄位。
  • USEREXIT_MOVE_FIELD_TO_KOMKH/KOMPH:批次判定(Batch Determination)抬頭/明細加欄位。
  • USEREXIT_CUST_MATERIAL_READ:在客戶物料資訊記錄中改用另一個客戶號(例如客戶階層情境)。

6-6.其他 SD 程式的出口

程式出口用途
MV45AFZDUSEREXIT_CONFIG_DATE_EXPLOSION決定 BOM 用哪個日期展開(標準用輸入日期,可改成需求交貨日)
FV45EFZ1USEREXIT_CHANGE_SALES_ORDER採購申請被指派後,標準會自動改排程行的數量與確認日期;用這支出口設定條件,保護訂單不被自動改
RV45PFZAUSEREXIT_SET_STATUS_VBUK設定 VBUK(抬頭狀態)保留欄位(如 UVK01 當額外訂單狀態)。可用工作區:VBUK、FXVBUP、FXVBUV
RV45PFZAUSEREXIT_SET_STATUS_VBUP設定 VBUP(明細狀態)保留欄位。可用工作區:FXVBAP、FXVBAPF、FXVBUV
RV45PFZAUSEREXIT_STATUS_VBUK_INVOICE影響抬頭層的開票狀態
MV45AFZ4USEREXIT_MOVE_FIELD_TO_KOMK / _KOMP免費商品判定(Free Goods)用的抬頭/明細通訊結構加欄位
SAPFV45PF0E / SAPFV45PF0CEXIT_SAPFV45P_001跨公司代碼銷售時,決定獲利分析段(Profitability Segment)用公司間開票資料,還是用外部開票資料
MV50AFZ1USEREXIT_SAVE_DOCUMENT_PREPARE、USEREXIT_SAVE_DOCUMENT、USEREXIT_MOVE_FIELD_TO_LIKP/LIPS…交貨單(VL01N/VL02N)的第一代出口。還有 MV50AFZ3(挑撿/過帳)、MV50AFZL(LIPS 相關)
RV60AFZZUSEREXIT_NUMBER_RANGE、USEREXIT_FILL_VBRK_VBRP…開立發票(VF01/VF04)的第一代出口。搭配 RV60AFZC(會計文件)、RV60AFZD
MV45AFZH / RV61AFZA / RV61AFZBUSEREXIT_PRICING_PREPARE_TKOMK/TKOMP 等定價通訊結構(開票端)

6-7.螢幕上的第一代出口

SD 的抬頭附加資料在螢幕 SAPMV45A 0309,明細附加資料在 SAPMV45A 0459。這兩個螢幕分別內含 Include Screen SAPMV45A 8309SAPMV45A 8459,就是給客戶用的螢幕出口。

  1. 要在銷售單據中新增欄位(例如 VBAK-ZZKUN),先用 SE11 Append Structure 把欄位加進 VBAK。
  2. 如果檢核是用資料字典定義的(固定值或檢查表),只要用 Screen Painter 全螢幕編輯器把欄位放進 SAPMV45A 8309處理邏輯完全不用改
  3. 如果需要應用專屬的檢核模組,把模組寫在 MV45AIZZ(模組名以 ZZ 開頭),並在 Include Screen 的處理邏輯中呼叫。
  4. 欄位傳輸(Field Transport)不需要做任何調整。

(依講義步驟整理)

7. VOFM 常式:SD 的另一種第一代增強

VOFM 是 SD/定價/輸出控制專用的「常式維護器」。它產生的也是 FORM 子常式(第一代的親戚),但是由 VOFM 統一產生、統一啟用,並在 Customizing 中被掛號使用。

7-1.VOFM 的四大類

選單中文用途產生的 FORM / 程式
Requirements → Pricing需求常式(定價)決定某個條件類型要不要被存取/計算。掛在 V/08 定價程序的 RequirementFORM KOBED_9xx(含明細檢查)與 KOBEV_9xx(前置檢查,不可用明細欄位);程式 LV61A9xx
Formulas → Condition value條件值公式自行計算條件的金額,結果放進 XKWERT。掛在 V/08 的 CalType(計算公式)欄FORM FRM_KONDI_WERT_9xx;程式 RV64A9xx
Formulas → Condition base value條件基值公式自行決定計算基礎(基數)。掛在 V/08 的 BasTypeFORM FRM_KOND_BASIS_9xx;程式 RV64A9xx
Formulas → Scale base / Group key級距基值/群組鍵決定級距用什麼數量、群組條件如何彙總FORM FRM_STAFFELBAS_9xxFRM_GRP_SCHL_9xx
Data Transfer資料轉移複製控制(VTAA/VTLA/VTFA)中,決定來源單據的哪些欄位怎麼搬到目標單據FORM DATEN_KOPIEREN_9xx(程式名依子選單而異,雙擊 VOFM 該列即可看到)
Copying requirements複製需求決定「這張單據可不可以被複製成下一張」(訂單→交貨→發票)FORM KOBED_9xx 系列(複製控制專用編號段)
客戶命名空間:VOFM 常式編號 600–999 保留給客戶(定價相關實務上習慣用 900–999);001–599 是 SAP 標準,不要動。條件類型的定價程序設定在 V/08,條件類型本身在 V/06——講義所說的「V/06 定價常式 RV64A9xx」就是指這條線:V/06 定條件類型 → V/08 掛上 RV64A9xx 產生的公式編號。

7-2.需求常式範例(KOBED / KOBEV)

*&--- VOFM → Requirements → Pricing → 901(程式 LV61A901)
*     邏輯:只有銷售組織 1000 且料號以 DXTR 開頭時,這個條件類型才生效
FORM kobed_901.

  sy-subrc = 4.                     " 預設「不符合需求」→ 條件不被存取

  CHECK komk-vkorg = '1000'.
  CHECK komp-matnr(4) = 'DXTR'.

  sy-subrc = 0.                     " 符合 → 條件生效

ENDFORM.

*     KOBEV 是「前置檢查」:在還沒有明細資料時被呼叫,
*     因此 **絕對不可以** 存取 KOMP(明細)欄位,只能用 KOMK(抬頭)
FORM kobev_901.

  sy-subrc = 4.
  CHECK komk-vkorg = '1000'.
  sy-subrc = 0.

ENDFORM.
KOBED 與 KOBEV 一定要成對維護且邏輯相容。最經典的錯誤是:KOBEV 寫了 KOMP 的條件,結果前置檢查時 KOMP 還是空的,導致條件時有時無——症狀就是「同一張單,重跑定價結果不一樣」。這個「不穩定」的症頭和第 1 章講的 SELECT 無排序保證是同一類問題:只要結果不穩定,先懷疑「輸入資料的順序/完整性」

7-3.定價公式範例(FRM_KONDI_WERT_9xx)

*&--- VOFM → Formulas → Condition value → 901(程式 RV64A901)
*     邏輯:從自建表 ZSD_SPECIAL_PRICE 取特殊單價,找不到就沿用標準計算結果
FORM frm_kondi_wert_901.

  " XKWERT 是這類公式的回傳欄位;XKOMV 是目前處理中的條件行
  SELECT SINGLE price
    FROM zsd_special_price
    WHERE kunnr = @komk-kunnr
      AND matnr = @komp-matnr
      AND datab <= @komk-prsdt
      AND datbi >= @komk-prsdt
    INTO @DATA(lv_price).

  IF sy-subrc = 0.
    " 單價 × 數量;注意幣別小數位,必要時用 CURRENCY_CONVERSION 類函數處理
    xkwert = lv_price * komp-mgame / 1000.
  ENDIF.

ENDFORM.
VOFM 常式裡只能使用定價通訊結構(KOMK/KOMP/KOMT1/XKOMV/TKOMV…)。要多帶欄位進來,就得回頭用 USEREXIT_PRICING_PREPARE_TKOMK / TKOMP 把欄位塞進 KOMK/KOMP(先在 KOMKAZ/KOMPAZ 這兩個 include 結構裡 Append 欄位)。這就是第一代出口與 VOFM 的標準搭檔打法。

7-4.VOFM 的存取碼(Access Key)問題

  • VOFM 產生的程式(LV61A9xx、RV64A9xx…)本質上是 SAP 物件,因此在許多系統上「新建/修改常式」時,會跳出要求輸入 物件存取碼(SSCR Object Key / Access Key) 的視窗。
  • 正規解法:到 SAP Support Portal(原 OSS)用該系統的安裝號碼申請該物件的 Registration Key;有些版本改為只要開發者鑰匙即可。相關說明可查 SAP Note 關鍵字 VOFM namespace / VOFM modification(例如 Note 356737 一系列,實際 Note 號請以你系統上 SNOTE 查到的為準)。
  • 啟用旗標:VOFM 畫面上每一列前面有「Active(啟用)」欄,新建後必須把它打勾(選單「編輯 → 啟用」),否則常式不會被呼叫——這是最常見的「我明明寫了怎麼沒進來」的原因。
  • 升級陷阱:升級或 Support Package 之後,VOFM 常式的啟用旗標可能遺失,或產生程式 T-code 表(TFRM/TFRMT)與程式不同步,需重新啟用並重新產生。上線前的升級測試務必把所有客戶編號常式跑過一遍。
  • 傳輸:VOFM 常式和它的啟用登錄(TFRM/TFRMT)是分開的物件,傳輸時容易漏——傳完到目標系統再開一次 VOFM 確認狀態。

8. 第二代 Customer Exit:概念與四種類型

第二代的原始碼增強以函數模組形式發佈:在 SAP 的發行版本中,用 CALL CUSTOMER-FUNCTION 'nnn' 呼叫這些函數模組,它們在發佈時只有一句程式碼 INCLUDE ZXxxxxxx.。使用者增強時無需申請物件鍵(Access Key),直接建立這個 INCLUDE、編寫相應的程式碼即可。

第二代與第一代最大的差別:這些函數模組中只能使用介面(EXPORTING/IMPORTING/TABLES)中傳遞的參數,不能使用呼叫程式的全域變數。這是優點(安全、不會破壞標準邏輯),也是限制(想要的資料沒傳進來就是拿不到,只能改用第一代或 BAdI)。

8-1.標準程式裡長什麼樣

*--- SAP 標準程式 SAPMM06E 裡的一段(示意)
  CALL CUSTOMER-FUNCTION '007'
    EXPORTING
      i_ekko = ekko                  " 只有這裡列出的東西,出口才拿得到
    IMPORTING
      e_ci_ekko = ci_ekko
    EXCEPTIONS
      OTHERS = 1.

*--- 命名規則(背起來):
*    CALL CUSTOMER-FUNCTION 'nnn'  ⇒  函數名 = EXIT_<主程式名>_<nnn>
*    SAPMM06E + '007'              ⇒  EXIT_SAPMM06E_007
*    SAPMV45A + '003'              ⇒  EXIT_SAPMV45A_003

*--- 而 EXIT_SAPMM06E_007 的原始碼裡「只有一行」:
FUNCTION exit_sapmm06e_007.
  INCLUDE zxm06u38.                  " ← 雙擊它,系統會問你要不要建立,這才是你寫碼的地方
ENDFUNCTION.

8-2.第二代的四種類型(SMOD 元件的 TYP 欄位)

TYP類型說明
EFunction Exit(功能模組增強)EXIT_ 起頭的函數,可到 SE37 查看,也可在資料字典的 TFDIR(函數表)中查詢 EXIT_ 開頭的函數
SScreen Exit(子螢幕增強)第一代是「螢幕編號先預留好,但內容是空」;第二代要你自己建立螢幕。採購訂單 ME2*、銷售訂單 VA0*、製令單 CO0* 都允許使用者自訂輸入介面並編寫相應的輸入輸出處理常式
CMenu Exit(GUI 增強/功能碼)GUI code:在標準的功能表/按鈕列加上自訂功能碼(以 + 開頭)與文字
TStructure Exit(表結構增強)Include structure。例如 MM06E005 中允許使用者維護的兩個結構 CI_EKKODBCI_EKPODB(講義寫成 CL_ 是筆誤,實際是 CI_ 開頭的 Customer Include)

8-3.第二代背後的系統函數與資料表

函數用途
MODX_FUNCTION_ACTIVE_CHECK出口函數增強:檢查這個 EXIT_ 函數有沒有被啟用(本章第 12 節的「超級函數」)
MODX_SUBSCREEN_ACTIVE_CHECK螢幕增強:檢查子螢幕出口是否啟用
MODX_ALL_ACTIVE_MENUENTRIES菜單增強:取出所有啟用的菜單項目
MODX_MENUENTRY_ACTIVE_CHECK菜單增強:檢查單一菜單項目是否啟用
DYNP_VALUES_READ讀取目前畫面上「還沒被 PAI 傳進來」的欄位值(做連動 F4、子螢幕互動時必備)
資料表內容重要欄位
MODSAPSAP 增強表:記錄了所有 enhancement 的增強內容,也就是增強點與函數的對應關係表NAME(增強名,如 MM06E005)、TYP(元件類型 E/C/S/T)、MEMBER(元件函數模組名,如 EXIT_SAPMM06E_007)
MODSAPTMODSAP 的文字表(增強說明)SPRSL、NAME、MODTEXT
TFDIR所有的函數表FUNCNAME(函數名)、MAND:值為 'C' 時才表示此出口函數已被啟動
ENLFDIR函數模組的額外屬性(可由函數反查所屬函數群組 AREA)FUNCNAME、AREA
TSDIRDynpro Areas:CALL CUSTOMER-SUBSCREEN 的登錄(螢幕增強)PROG、DYNNR、AREA
CUATEXTSGUI Interface:Menu Texts Changed(GUI 功能表文字增強)PROGRAM、FCODE、TEXT
MODACTCMOD 專案與增強組件的指派關係(哪個專案用了哪個 SMOD 增強)NAME(專案)、MEMBER(增強)
TADIRRepository 物件目錄;找出口靠它(OBJECT = SMOD 是第二代增強、SXSD 是 BAdI)PGMID、OBJECT、OBJ_NAME、DEVCLASS

9. 怎麼找第二代出口:六種可操作的方法

這一節是本章的核心。照順序試,一定找得到。

方法 A(首選):用 Z_FIND_USEREXIT 依 TCODE 一次列出

  1. SE38 執行 Z_FIND_USEREXIT(自建工具程式,見第 10 節)。
  2. 輸入交易碼(如 ME21NVA01)或直接輸入程式名。
  3. 報表會分兩區列出:Enhancement(SMOD,第二代)Business Add-in(SXSD,第三/四代),並顯示筆數。
  4. 雙擊清單中的名稱可直接跳到 SMOD(第二代)或 SE18(BAdI)。
原理:由 TCODE → TSTC 找主程式 → TADIR 找該程式的開發類別(Package / DEVCLASS) → 再列出同一個 Package 底下所有 SMOD 與 SXSD 物件。所以它列出來的是「這個模組領域的所有出口」,不是「這個交易一定會呼叫的出口」——還是要靠 DEBUG 確認。

方法 B:SE84 Repository Information System

  1. SE84 → 展開 增強(Enhancements)
  2. Customer Exits → Enhancements:可用增強名稱(如 MM06E*)、Package元件名(如 EXIT_SAPMM06E*)反查增強組件。
  3. Customer Exits → Function Exits:直接用 EXIT_* 樣式搜函數出口。
  4. Enhancement Spots / BAdIs:找第三、四代(第 3-4 章)。

方法 C:SE16N 查 MODSAP / MODSAPT

  1. SE16N → 表名 MODSAP
  2. MEMBER 欄位輸入 EXIT_SAPMM06E*(用你猜到的主程式名組出來的樣式)→ 執行,即可看到所有相關的增強名與類型。
  3. 反過來也行:知道增強名(如 MM06E005)填 NAME,就能看到它底下所有元件(E/S/C/T 各是什麼)。
  4. 要中文/英文說明就查 MODSAPT(SPRSL = 語言)。

方法 D:在標準程式中直接搜尋 CALL CUSTOMER-FUNCTION(最可靠)

  1. SE93 由交易碼查出主程式(例:ME21N → SAPLMEGUI;ME21 → SAPMM06E;VA01 → SAPMV45A)。
  2. SE38 開該程式 → 功能表「其他功能 → 搜尋/取代」(或 Ctrl+F)。
  3. 搜尋字串輸入 CUSTOMER-FUNCTION,範圍選 「在所有 Include 中(In main program)」、勾選「所有出現位置」。
  4. 結果每一行像 CALL CUSTOMER-FUNCTION '007',把 主程式名 + 編號 組起來就是函數名:EXIT_SAPMM06E_007
  5. 螢幕出口就搜 CALL CUSTOMER-SUBSCREEN;菜單出口搜 GUI Status 中以 + 開頭的功能碼。
  6. 拿函數名回到 MODSAP(方法 C)反查它屬於哪個增強組件(NAME),才知道 CMOD 要掛哪一個。

方法 E:SE37 直接猜/查 EXIT_ 函數

  1. SE37 函數名輸入 EXIT_SAPMM06E* → 按 F4,列出所有符合的出口函數。
  2. 雙擊進去看它的介面參數——這一步很關鍵:介面裡沒有的資料,你在出口裡就拿不到,決定了這支出口能不能用。
  3. 看它的原始碼(只有一行 INCLUDE ZX…),記下 INCLUDE 名稱。

方法 F:追不到時的 DEBUG/Trace 大絕

  1. 敘述中斷點:進入交易 → /h 開 Debug → 功能表「中斷點 → 中斷點於 → 敘述(Breakpoint at Statement)」→ 輸入 CALL CUSTOMER-FUNCTION。之後每次標準碼呼叫任何出口都會停下來,sy-repid 就是主程式、參數就是編號。同理可設 CALL CUSTOMER-SUBSCREENCALL BADICALL METHOD
  2. 函數中斷點:「中斷點 → 中斷點於 → 函數模組」→ 輸入 MODX_FUNCTION_ACTIVE_CHECK。每次系統檢查「這個出口有沒有啟用」都會停,在 Local 頁籤看參數就知道正在檢查哪一支出口(第 12 節詳述)。
  3. 執行時期分析SAT(舊版 SE30)錄一次交易,在結果的「呼叫階層/Hit List」中用 EXIT_ZX 過濾,可以看到實際被執行過的出口與先後順序——這是判斷「哪一支出口真的會跑」最省時間的方法。
  4. SQL TraceST05 看標準程式讀了哪些表,反推該從哪個時點切入。
老師的實戰忠告:「畫面上的程式應是 EXIT_SAPMM06E_006,而且有可能會有多支增強,要耐心的 DEBUG。」 找出口沒有一步到位的魔法,方法 A/C/E 先縮小範圍,方法 F 定生死。

10. 工具程式 Z_FIND_USEREXIT(S/4HANA 現代化改寫)

老師的教材附了經典版 Z_FIND_USEREXIT(TABLES + WRITE 清單)。下面是同樣邏輯的現代化重寫版:內嵌宣告、逸出主機變數、CL_SALV_TABLE 顯示、雙擊跳轉。上半段找第二代(SMOD),下半段找第三/四代(SXSD/BAdI)。

REPORT z_find_userexit.

TYPES: BEGIN OF ty_out,
         kind     TYPE string,          " Enhancement / Business Add-in
         obj_name TYPE tadir-obj_name,  " MM06E005 / ME_PROCESS_PO_CUST
         object   TYPE tadir-object,    " SMOD / SXSD
         descr    TYPE string,
       END OF ty_out.

PARAMETERS: p_tcode TYPE tstc-tcode,
            p_pgmna TYPE tstc-pgmna.

START-OF-SELECTION.

  DATA(lv_pgmna) = p_pgmna.

*--- 1) 由交易碼反查主程式
  IF p_tcode IS NOT INITIAL.
    SELECT SINGLE pgmna
      FROM tstc
      WHERE tcode = @p_tcode
      INTO @lv_pgmna.
    IF sy-subrc <> 0.
      MESSAGE |交易碼 { p_tcode } 不存在| TYPE 'S' DISPLAY LIKE 'E'.
      RETURN.
    ENDIF.
  ENDIF.

  IF lv_pgmna IS INITIAL.
    MESSAGE '請輸入交易碼或程式名' TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.

*--- 2) 由主程式找出它的開發類別(Package)
  SELECT SINGLE devclass
    FROM tadir
    WHERE pgmid    = 'R3TR'
      AND object   = 'PROG'
      AND obj_name = @lv_pgmna
    INTO @DATA(lv_devclass).

  IF sy-subrc <> 0.
    " 找不到 → 可能是函數群組的主程式(SAPLxxxx),改由 TFDIR → ENLFDIR → FUGR 反查
    SELECT SINGLE subc FROM trdir WHERE name = @lv_pgmna INTO @DATA(lv_subc).
    IF lv_subc = 'F'.                      " F = 函數群組
      SELECT SINGLE t~funcname, e~area
        FROM tfdir AS t
        INNER JOIN enlfdir AS e ON e~funcname = t~funcname
        WHERE t~pname = @lv_pgmna
        INTO @DATA(ls_fu).
      IF sy-subrc = 0.
        SELECT SINGLE devclass
          FROM tadir
          WHERE pgmid = 'R3TR' AND object = 'FUGR' AND obj_name = @ls_fu-area
          INTO @lv_devclass.
      ENDIF.
    ENDIF.
  ENDIF.

  IF lv_devclass IS INITIAL.
    MESSAGE |找不到 { lv_pgmna } 的開發類別| TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.

*--- 3) 列出同一個 Package 底下的 SMOD(第二代增強)與 SXSD(BAdI)
  SELECT object, obj_name
    FROM tadir
    WHERE pgmid  = 'R3TR'
      AND object IN ( 'SMOD', 'SXSD' )
      AND devclass = @lv_devclass
    ORDER BY object, obj_name
    INTO TABLE @DATA(lt_tadir).

  IF lt_tadir IS INITIAL.
    MESSAGE 'No userexits or BAdIs exist' TYPE 'S' DISPLAY LIKE 'W'.
    RETURN.
  ENDIF.

*--- 4) 補說明文字
  DATA lt_out TYPE STANDARD TABLE OF ty_out WITH EMPTY KEY.

  LOOP AT lt_tadir INTO DATA(ls_tadir).
    DATA(lv_txt) = VALUE string( ).

    CASE ls_tadir-object.
      WHEN 'SMOD'.
        SELECT SINGLE modtext FROM modsapt
          WHERE sprsl = @sy-langu AND name = @ls_tadir-obj_name
          INTO @lv_txt.
      WHEN 'SXSD'.
        SELECT SINGLE text FROM sxs_attrt
          WHERE sprsl = @sy-langu AND exit_name = @ls_tadir-obj_name
          INTO @lv_txt.
    ENDCASE.

    APPEND VALUE #( kind     = COND #( WHEN ls_tadir-object = 'SMOD'
                                       THEN 'Enhancement(第二代)'
                                       ELSE 'Business Add-in(BAdI)' )
                    object   = ls_tadir-object
                    obj_name = ls_tadir-obj_name
                    descr    = lv_txt ) TO lt_out.
  ENDLOOP.

  DATA(lv_smod) = REDUCE i( INIT n = 0 FOR <o> IN lt_out
                            NEXT n = COND #( WHEN <o>-object = 'SMOD' THEN n + 1 ELSE n ) ).
  DATA(lv_badi) = lines( lt_out ) - lv_smod.

*--- 5) 用 CL_SALV_TABLE 顯示
  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_out ).

      lo_alv->get_functions( )->set_all( abap_true ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->get_display_settings( )->set_list_header(
        |{ lv_pgmna } / Package { lv_devclass }:Exits { lv_smod } 個,BAdIs { lv_badi } 個| ).

      lo_alv->display( ).

    CATCH cx_salv_msg INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'S' DISPLAY LIKE 'E'.
  ENDTRY.
要做出原版「雙擊跳 SMOD/SE18」的效果,可以註冊 lo_alv->get_event( )double_click 事件,然後 SET PARAMETER ID 'MON' FIELD lv_name.CALL TRANSACTION 'SMOD' AND SKIP FIRST SCREEN.(BAdI 則用 Parameter ID 'EXN' + SE18)。

10-1.直接用 SQL 查 MODSAP / TFDIR 的兩支小工具

*--- (1) 由函數名樣式反查「這支出口屬於哪個增強組件」
SELECT m~name,                       " 增強名,例如 MM06E005
       m~typ,                        " E=函數 S=螢幕 C=菜單 T=結構
       m~member,                     " 元件名,例如 EXIT_SAPMM06E_007
       t~modtext AS descr
  FROM modsap AS m
  LEFT OUTER JOIN modsapt AS t
    ON  t~name  = m~name
    AND t~sprsl = @sy-langu
  WHERE m~member LIKE 'EXIT_SAPMM06E%'
  ORDER BY m~name, m~member
  INTO TABLE @DATA(lt_modsap).

cl_demo_output=>display( lt_modsap ).

*--- (2) 檢查出口函數「有沒有被啟用」:TFDIR-MAND = 'C' 才是啟用
SELECT funcname, mand
  FROM tfdir
  WHERE funcname LIKE 'EXIT_SAPMM06E%'
  ORDER BY funcname
  INTO TABLE @DATA(lt_tfdir).

LOOP AT lt_tfdir INTO DATA(ls_tfdir).
  WRITE: / ls_tfdir-funcname,
           COND string( WHEN ls_tfdir-mand = 'C' THEN '已啟用' ELSE '未啟用' ).
ENDLOOP.
絕對不要用 UPDATE 直接改 TFDIR。 講義附了一支 ZACTEXITFUN,內容是 UPDATE tfdir SET mand = 'C' WHERE funcname = 'EXIT_SAPMM06E_013'.,用途是「一時查不到相關 Enhancement、無法用 SMOD/CMOD 啟用時強制啟動出口函數」。這是反例,只用來理解 MAND 欄位的意義
・直接 UPDATE SAP 標準表繞過 CMOD,不會產生傳輸請求,DEV 改了 QAS/PRD 不會有,升級或重新產生時會被打回原形。
・沒有 CMOD 專案登錄,下一個顧問完全看不出這裡有增強,是稽核與交接的災難。
・正確做法永遠是:SMOD 查出增強組件 → CMOD 建專案 → 指派 → 寫碼 → 啟用專案

11. SMOD 與 CMOD:查看與實現增強的完整操作

兩個交易碼的分工,一句話記住:SMOD 是「看有什麼出口」(SAP 交付的增強組件,唯讀);CMOD 是「我要用哪些出口」(客戶專案,可寫可啟用)。

11-1.SMOD:查看增強組件

  1. SMOD → 增強名稱輸入(如 MM06E005V45A0003),或按 F4 用說明文字模糊搜尋。
  2. Components(元件) → 顯示。畫面分區列出:Function exits(EXIT_*)、Screen areas(螢幕出口的程式+螢幕編號)、Menu entriesInclude structures(CI_ 結構)。
  3. Documentation(文件) 讀 SAP 的官方說明:這個增強什麼時候被呼叫、能改什麼、不能改什麼。這一步千萬別跳過
  4. 雙擊某個 EXIT_ 函數即可進入 SE37 看介面與 INCLUDE 名稱。

(依講義步驟整理)

講義另外提到一個很實用的練習題:「SAP 記錄 CO02 生產訂單修改日誌的增強」——用 Z_FIND_USEREXIT 輸入 CO02,會列出 PP 模組的一整批 SMOD/BAdI,再用 SMOD 的文件逐一看說明,挑出能攔截「訂單修改」的那一支。這是練習「找出口」最好的自我訓練。

11-2.CMOD:建立專案並實現增強(標準八步驟)

  1. CMOD → 專案(Project)輸入自訂名稱(Z 開頭,例如 ZPOZSD01)→ 按建立
    命名策略:老師的做法是「ZPO 是指將所有的 PO 相關增強都放在此 PROJECT」——依模組/主題分專案,不要一個出口開一個專案,否則以後沒人管得動。
  2. 輸入簡短說明 → 存檔 → 指定套件與傳輸請求
  3. Enhancement assignments(增強指派) → 在清單中列舉由 SMOD 所找到的、想要增強的增強組件名稱(可一次填多個)→ 存檔。
  4. Components(元件) → 可看到已經預設好的增強函數名稱(例如 EXIT_SAPMM06E_007)。
  5. 雙擊該函數名 → 進入函數群組(例如 XM06)→ 函數原始碼中只有一行 INCLUDE ZXM06U38.
  6. 雙擊 INCLUDE 名稱 → 系統詢問「此 Include 尚不存在,要建立嗎?」→ 按,指定套件與請求 → 建立 ZXM06U38,裡頭就可以自行新增程式碼了。
  7. 寫完程式碼 → 啟用(Ctrl+F3) → 退出回到 CMOD。
  8. 在 CMOD 專案畫面按 啟用(Activate,火柴人/閃電圖示)。啟用成功後,回到 Components 會發現增強函數名稱前面有一個綠色 ✔

(依講義步驟整理)

老師的鐵律:「增強要生效必須啟動;增強要刪除必須取消啟動。」
・只寫程式碼、沒按 CMOD 的「啟用」=完全不會被呼叫(TFDIR-MAND 不會變成 'C')。這是初學者最常卡住的地方。
・要移除增強,順序是:先 Deactivate 專案 → 再刪除指派/程式碼。直接刪 Include 會讓函數變成語法錯誤,整個交易碼掛掉。
一個增強組件(SMOD name)只能被一個 CMOD 專案指派。若已被別的專案佔用,系統會擋下來——這時要去談,把邏輯合併進既有專案,不要另開專案硬幹。

11-3.第一代與第二代在「建程式」上的差別

口訣:「第一代程式會先建好,第二代則由自己新增。」 第一代的 MV45AFZZ、USEREXIT_SAVE_DOCUMENT_PREPARE 已經存在,你打開就能寫;第二代的 ZXM06U38 一開始不存在,要你雙擊建立;螢幕出口的螢幕 0101 也不存在,要你自己到 SE38/SE80 建。

12. 第二代實作(一):採購單 MM06E005

練習題:在採購單上增加客制的欄位,並處理它。 這個練習一次用到第二代的三種類型:T(結構)→ S(螢幕)→ E(函數)

12-1.步驟總覽

  1. SMODMM06E005(Customer fields in purchasing document)→ 看 Components。
  2. CMOD 建專案 ZPO → Enhancement assignment 填 MM06E005
  3. 結構增強(T):SE11 維護 CI_EKKODB(抬頭)與 CI_EKPODB(明細),加上 Z 開頭的欄位並啟用。
  4. 螢幕增強(S):SE38/SE80 進 SAPLXM06,建立子螢幕 0101(抬頭)/0111(明細),畫上剛才的欄位。
  5. 函數增強(E):實作 EXIT_SAPMM06E_006/007/008 等,把資料在標準結構與子螢幕之間搬進搬出。
  6. CMOD 啟用專案 → ME21N/ME21 測試。

(依講義步驟整理)

12-2.欄位結構增強(EKKO)

  1. SE11 → 結構 CI_EKKODB → 若不存在則建立(它是 SAP 預留給客戶的 Customer Include,名稱固定,不要自己另取)。
  2. 加入欄位,欄位名稱以 Z 起頭(例如 ZZCONTRACT、ZZREMARK),指定資料元素。
  3. 啟用——講義特別提醒:「啟動要等待比較久」(因為 EKKO 是大表,要做結構調整/轉換)。
  4. 啟用完成,回頭用 SE11 看 EKKO,就會發現表中已加入增強的欄位了。
在正式系統上 Append 大表(EKKO/EKPO/VBAK/MARA)會觸發資料庫表格轉換,資料量大時可能鎖表數十分鐘。一定要安排在停機視窗,並先在 QAS 量測時間。S/4HANA on HANA 多數情況是「線上新增欄位」很快,但仍要以 SE14 的調整記錄為準。

12-3.螢幕增強實作

  1. SE38 進入 SAPMM06E,搜尋 CALL CUSTOMER-SUBSCREEN,確認 SAP 預留的子螢幕區域名稱與呼叫的程式(SAPLXM06)。
  2. 第二代增強不會預先新增程式或螢幕,因此要進入 SE38SE80 開啟 SAPLXM06自行新增螢幕編號 0101
  3. 建立螢幕 0101 時,屬性中的螢幕類型要設定成 Subscreen(子螢幕)——設錯類型畫面不會出現。
  4. 進入 Layout 設計自己的子螢幕:用 Dict/Program fields 把 EKKO-ZZxxx 拉進來。
  5. 在該螢幕的 Flow Logic 寫 PBO/PAI 模組(模組要寫在函數群組 XM06 的客戶 include,例如 ZXM06O01/ZXM06I01)。
  6. 回到 CMOD 專案的 Components,在 Screen areas 那一列填上 SAPLXM06 / 0101 → 存檔 → 啟用。

(依講義步驟整理)

*--- SAPLXM06 子螢幕 0101 的 Flow Logic(在 SE51 的 Flow logic 頁籤)
PROCESS BEFORE OUTPUT.
  MODULE zzstatus_0101.          " 依採購單狀態決定欄位可否輸入

PROCESS AFTER INPUT.
  MODULE zzcheck_0101.           " 客制欄位的檢核

*--- 對應的模組(寫在函數群組 XM06 的客戶 include,例如 ZXM06O01 / ZXM06I01)
MODULE zzstatus_0101 OUTPUT.
  " gs_ekko 是本函數群組的全域工作區,由 EXIT_SAPMM06E_006 填入
  LOOP AT SCREEN.
    IF screen-group1 = 'ZZ1' AND gs_ekko-loekz IS NOT INITIAL.
      screen-input = 0.          " 已刪除的採購單 → 客制欄位變唯讀
      MODIFY SCREEN.
    ENDIF.
  ENDLOOP.
ENDMODULE.

MODULE zzcheck_0101 INPUT.
  IF ekko-zzcontract IS NOT INITIAL.
    SELECT SINGLE @abap_true
      FROM zmm_contract
      WHERE contract_no = @ekko-zzcontract
      INTO @DATA(lv_exists).
    IF lv_exists = abap_false.
      MESSAGE |合約號碼 { ekko-zzcontract } 不存在| TYPE 'E'.
    ENDIF.
  ENDIF.
ENDMODULE.
子螢幕上的 MESSAGE TYPE 'E' 是安全的(有畫面、屬於 PAI),和第 5 節警告的「背景/BAPI 情境」不同。這也是為什麼使用者互動的檢核放在螢幕出口,資料一致性的檢核放在存檔出口

12-4.函數增強(資料搬進搬出)

MM06E005 的函數出口是成對的:一支把標準資料送到子螢幕,一支把子螢幕的輸入收回標準結構。講義的實作以 EXIT_SAPMM06E_007 為例,雙擊後往下新增 ZXM06U38 程式。

*--- INCLUDE ZXM06U38(由 EXIT_SAPMM06E_007 呼叫)
*    只能使用介面參數!這裡的 i_ekko / e_ci_ekko 都是函數介面帶進來的

" 把標準採購單抬頭資料送進子螢幕用的全域工作區
gs_ekko = i_ekko.

" 客制欄位的預設值:新建單據且合約號碼為空時,依供應商帶出預設合約
IF gs_ekko-zzcontract IS INITIAL AND gs_ekko-lifnr IS NOT INITIAL.

  SELECT SINGLE contract_no
    FROM zmm_contract
    WHERE lifnr  = @gs_ekko-lifnr
      AND datbi >= @sy-datum
    INTO @gs_ekko-zzcontract.

ENDIF.

" 回傳客制欄位結構給標準程式
e_ci_ekko = CORRESPONDING #( gs_ekko ).
講義的重要提醒:「EXIT_SAPMM06E_008 才可以更動 EKKO。」 出口是有分工的——有的只能(送資料給畫面),有的才可以(把值回寫進標準結構)。寫在錯誤的出口,值會在下一個 PBO 被沖掉,症狀是「我明明有填,存檔後就不見了」。決定寫在哪一支的唯一可靠方法:SMOD 的 Documentation + 看函數的 IMPORTING/CHANGING 參數

13. 超級函數 MODX_FUNCTION_ACTIVE_CHECK 與出口觸發順序

MODX_FUNCTION_ACTIVE_CHECK 的參數是由功能模組名稱而來——標準程式在真正呼叫 EXIT_ 之前,會先用這支函數問「這個出口啟用了沒」。所以只要在它上面設一個中斷點,整個交易會呼叫哪些出口、以什麼順序呼叫,就全部無所遁形。這就是老師說的「超級函數」。

13-1.用它來反查出口(操作步驟)

  1. SE37 開啟 MODX_FUNCTION_ACTIVE_CHECK → 在第一行可執行敘述設外部中斷點(Session Breakpoint)DEBUG 技巧:也可以直接由函數設定 DEBUG。
  2. 執行目標交易(例如 ME21N)。
  3. 每次停下來時,切到 Debugger 的 Locals(本地)頁籤,看傳進來的函數名參數——那就是系統正在檢查的出口。
  4. 把每次停下來的函數名依序抄下來,就得到「這個交易的出口觸發時序表」。
  5. 找到目標出口後,把中斷點移到該出口的 ZX… include 上,取消 MODX 的中斷點。
同理,螢幕出口用 MODX_SUBSCREEN_ACTIVE_CHECK、菜單出口用 MODX_MENUENTRY_ACTIVE_CHECKMODX_ALL_ACTIVE_MENUENTRIES。這三支加上 MODX_FUNCTION_ACTIVE_CHECK,就是第二代增強的「四把萬能鑰匙」。
*--- 在自己的程式裡也能判斷某個出口是否已啟用(少用,但除錯很好用)
DATA(lv_active) = abap_true.

CALL FUNCTION 'MODX_FUNCTION_ACTIVE_CHECK'
  EXPORTING
    funcname   = 'EXIT_SAPMM06E_007'
  EXCEPTIONS
    not_active = 1
    OTHERS     = 2.

IF sy-subrc <> 0.
  lv_active = abap_false.
ENDIF.

" 介面參數名稱會因版本略有差異,請以你系統上 SE37 顯示的為準
WRITE: / |EXIT_SAPMM06E_007 啟用狀態:{ COND string( WHEN lv_active = abap_true
                                                     THEN '已啟用' ELSE '未啟用' ) }|.

13-2.ME21N 的出口觸發時序(老師實測結果)

時機被呼叫的出口
一進入 ME21N 之後,立刻執行EXIT_SAPMM06E_006
打完抬頭(檔頭)之後EXIT_SAPMM06E_007EXIT_SAPMM06E_008
點入 Material(物料)時EXIT_SAPLOMCV 系列
Material 輸入完成按下 ENTER 時EXIT_SAPMM06E_016
整筆明細輸入完成之後EXIT_SAPMM06E_018
按下存檔之後EXIT_SAPLEBND_002
除錯螢幕增強的訣竅:增強的 DEBUG 若是螢幕,就找 PBO / PAI 裡頭的 MODULE 設中斷點。講義的實作是在 CUSTSCR1_HEAD_SET_DATA 設中斷點,然後建立一張 ME21N;輸入抬頭時就會停在斷點。
老師的老實話:「這種增強的實作若沒有 BASIS 支援,很難有環境讓您試。」「然後啟動全部,有些練習環境有可能無法啟動。」——結構增強要改大表、CMOD 專案是 client 層級全域生效,教學環境通常鎖住。沒有環境時,把重點放在「看得懂別人寫的出口」與「找得到出口」,這兩件事才是專案上真正天天在用的。

14. 第二代實作(二):客戶訂單 V45A0003

這是本章最容易在教學環境完成的實作,強烈建議親手做一次。

14-1.完整步驟

  1. SMOD 輸入 V45A0003 → 按 Components,看到元件 EXIT_SAPMV45A_003
  2. CMOD 建立專案 ZSD01 → 輸入說明 → 存檔(指定套件與請求)。
  3. Enhancement assignment(增強指派) → 填入 V45A0003 → 存檔。
  4. Components → 雙擊 EXIT_SAPMV45A_003 → 進入 SE37。
  5. 函數原始碼只有一行 INCLUDE ZXVVAU05.雙擊 ZXVVAU05 → 建立它。
  6. 在 ZXVVAU05 中寫程式碼 → 啟用(要激活)
  7. 回到 CMOD專案也要激活(這步最常被忘記)。
  8. 執行 VA01 測試。

(依講義步驟整理)

14-2.案例程式碼

*--- INCLUDE ZXVVAU05(由 EXIT_SAPMV45A_003 呼叫,增強組件 V45A0003)
*    案例一:確認出口真的被呼叫
IF sy-uname = 'ERP000' AND sy-mandt = '540'.
  MESSAGE '第二代增強的方法 EXIT_SAPMV45A_003 / ZXVVAU05' TYPE 'I'.
ENDIF.
*--- INCLUDE ZXVVAU05
*    案例二:加上日期條件,並把三個關鍵資訊都寫進訊息,方便日後辨識
IF sy-uname = 'ERP000' AND sy-datum = '20250501'.

  MESSAGE |這是第二代增強:函數 EXIT_SAPMV45A_003,| &&
          |組件 V45A0003,INCLUDE ZXVVAU05| TYPE 'I'.

ENDIF.
老師三令五申:「測試完之後,請記得要註釋掉,以免影響他人。」 CMOD 專案是整個 client 生效的,你留一個 MESSAGE 'I',全公司開 VA01 都會跳視窗;如果那個 client 上有跑背景批次或介面程式,還會直接 DUMP。教學環境尤其容易翻車。

15. 菜單出口與欄位出口

15-1.Menu Exit(菜單出口)

  1. SAP 在標準的 GUI Status 中預留了功能碼,+ 開頭(例如 +CUS、+C01)。
  2. SMOD 該增強的 Components 中,Menu entries 區塊會列出程式名、Status、功能碼。
  3. CMOD 的 Components 中雙擊該功能碼,填入功能表文字(多語系可分別維護,文字存在 CUATEXTS)。
  4. 功能碼被按下時,會經由對應的函數出口(通常是同一個增強裡的某支 EXIT_,介面帶有 SY-UCOMM/FCODE)交給你處理。
  5. CMOD 專案啟用後,該功能表項目才會出現在交易畫面上。

(依講義步驟整理)

系統背後是用 MODX_ALL_ACTIVE_MENUENTRIES / MODX_MENUENTRY_ACTIVE_CHECK 判斷菜單項目是否啟用,資料放在 CUATEXTS。菜單項目沒出現,先查這兩個地方。

15-2.Field Exit(欄位出口)

欄位出口是掛在資料元素(Data Element)上的檢核/轉換:只要畫面上任何欄位用了那個資料元素,離開欄位時就會呼叫你的函數。

  1. CMOD → 功能表 Goto → Text enhancements → Field exits(或直接執行程式 RSMODPRF)。
  2. 建立 → 輸入資料元素名稱 → 系統自動產生函數 FIELD_EXIT_<資料元素>(可加尾碼 _A、_B 產生多個版本)。
  3. 在函數中處理:介面固定為 INPUT(使用者輸入值)與 OUTPUT(要寫回畫面的值)。
  4. 可以指定只對某一個程式/螢幕編號生效(全域 vs 區域欄位出口)。
  5. 啟用欄位出口,而且系統參數 abap/fieldexit = YES 必須設定(要 BASIS 改 profile 並重啟,這也是它很少被用的原因)。
FUNCTION field_exit_zzcontract.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  IMPORTING
*"     REFERENCE(INPUT)
*"  EXPORTING
*"     REFERENCE(OUTPUT)
*"----------------------------------------------------------------------

  " 典型用途:使用者不照規範打小寫 → 自動轉大寫並補零
  output = to_upper( condense( CONV string( input ) ) ).

ENDFUNCTION.
Field Exit 在新版本(ECC 6.0 之後、S/4HANA)已被 SAP 視為過時(obsolete)技術,不再建議使用,且對 Web Dynpro/Fiori 畫面完全無效。要做欄位層級的轉換/檢核,改用:資料元素的 Conversion Exit(CONVERSION_EXIT_xxxxx_INPUT/OUTPUT)、畫面 PAI 檢核、或 BAdI。這裡列出來只是為了讓你看得懂舊系統。

16. 傳輸、相依與版本管理

物件傳輸方式常見漏傳的地雷
第一代 include(MV45AFZZ…)PROG/INCLUDE 物件進 Request(會被標為對 SAP 物件的修改)宣告寫在 MV45ATZZ/MV45ATOP,但只傳了 MV45AFZZ → 目標系統語法錯誤
CMOD 專案物件類型 R3TR CMOD <專案名>只傳了 ZX… include 卻沒傳 CMOD 專案 → 目標系統程式在、但沒啟用,完全沒作用
ZX… includeR3TR PROG(或函數群組 R3TR FUGR只傳專案沒傳程式 → 目標系統啟用時語法錯誤
螢幕出口(SAPLXM06 0101)隨函數群組 FUGR XM06 一起傳螢幕(Dynpro)沒被加進 Request;PBO/PAI 模組寫在別的 include 卻沒傳
結構增強(CI_EKKODB/Append)R3TR TABLR3TR TTYP 等字典物件字典物件與程式在同一個 Request 但匯入順序造成短暫語法錯誤;建議字典先行、程式隨後
VOFM 常式產生的程式(LV61A9xx/RV64A9xx)+啟用登錄(TFRM/TFRMT)啟用旗標沒跟著過去 → 目標系統常式在但不執行
Field Exit函數群組+登錄表目標系統的 abap/fieldexit 參數沒開
傳輸前自檢清單SE09SE10 打開 Request → 展開物件清單,確認四樣東西都在:①CMOD 專案 ②ZX… include(或函數群組)③字典物件(CI_ 結構/Append)④螢幕。四缺一,QAS 一定出事。

17. S/4HANA 注意事項

17-1.第一、二代仍然存在且可用,但已停止擴充。 S/4HANA 1809 以後,SD/MM 的傳統交易(VA01、ME21、VF01)大多還在,MV45AFZZ、MM06E005 也都還在被呼叫。但 SAP 不再新增第一、二代出口,所有新功能只提供 BAdI / Enhancement Spot。新開發一律優先找 BAdI(第 3-4 章)。
17-2.走 Fiori / OData / BAPI / RAP 的路徑,出口不一定會被呼叫。 例如:
ME21N(Enjoy 交易)不使用 MM06E005 的子螢幕出口,客制欄位的畫面要改用 BAdI ME_GUI_PO_CUST,邏輯用 ME_PROCESS_PO_CUST。舊的 SAPLXM06 0101 只在舊式 ME21/ME22/ME23 有效。這正是講義所說「有些練習環境有可能無法啟動/很難有環境讓您試」的技術原因之一。
・銷售訂單若由 Fiori App 或 API_SALES_ORDER_SRV(OData)建立,走的是不同的服務層,第一代 SD user exit 不保證被觸發。
做法:導入前一定要把「所有建單通路」列出來(GUI/BAPI/IDoc/OData/CPI),逐一 DEBUG 驗證出口有沒有跑到。
17-3.標準程式被改寫後,出口可能失效。 S/4HANA 的簡化清單(Simplification List)改寫了大量標準程式:MB5B/MSEG 相關邏輯改走 MATDOC、BSEG/BKPF 相關改走 ACDOCA、SD 的狀態表 VBUK/VBUP 已被併入 VBAK/VBAP。所以:
RV45PFZA 的 USEREXIT_SET_STATUS_VBUK / _VBUP 在 S/4HANA 上因 VBUK/VBUP 被移除(改為 CDS 相容視圖),行為與 ECC 不同,升級專案必須逐一重測。
・出口裡若有 SELECT … FROM vbukFROM bsegFROM mseg,升級時要改成新表或 CDS View。
17-4.升級與 Note 的衝突處理。
SPDD:升級時處理字典物件的修改對帳(Append Structure、資料元素、Domain)。在匯入階段就要做,錯過會遺失
SPAU:升級後處理 Repository 物件(程式、函數、螢幕)的修改對帳。第一代出口 include 若被 SAP 改版,會出現在這裡,要人工合併。
SPAU_ENH:專門處理增強實作(第三、四代)的調整。
SNOTE 套 Note 時,若 Note 改到你動過的 include,會要求 Access Key 或提示衝突——先用 SE95(修改瀏覽器)確認自己動過哪些標準物件
治本建議:把出口裡的邏輯全部搬進自建的 Z 類別/Z 函數,出口 include 裡只留一行呼叫。這樣升級時 SPAU 只要處理那一行,維護成本降到最低。
*--- 建議的出口寫法:出口 include 內只留「一行呼叫」,邏輯全在自建類別中
*    INCLUDE ZXVVAU05
zcl_sd_order_exit=>check_before_save( ).

*--- 自建類別(可單元測試、可版本管理、升級時不受 SPAU 影響)
CLASS zcl_sd_order_exit DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    CLASS-METHODS check_before_save
      RAISING cx_static_check.
ENDCLASS.

CLASS zcl_sd_order_exit IMPLEMENTATION.
  METHOD check_before_save.

    TRY.
        " 真正的商業邏輯寫在這裡:可以寫 ABAP Unit、可以被別的程式重用
        DATA(lo_checker) = NEW zcl_sd_credit_checker( ).
        lo_checker->validate( ).

      CATCH zcx_sd_check INTO DATA(lx).
        " 統一的錯誤處理:介面情境寫 log,對話情境才丟訊息
        IF sy-batch = abap_true OR sy-binpt = abap_true.
          zcl_app_log=>add( lx->get_text( ) ).
        ELSE.
          MESSAGE lx->get_text( ) TYPE 'E'.
        ENDIF.
    ENDTRY.

  ENDMETHOD.
ENDCLASS.
出口撰寫的七條軍規(把它貼在你的螢幕邊):
① 不下 COMMIT WORK / ROLLBACK WORK。
② 不改不屬於自己的全域變數。
③ 不在出口裡做大量 DB 存取或呼叫 RFC(會拖垮交易反應時間)。
④ 訊息型別要看情境:對話用 E/W,背景與 BAPI 用 log。
⑤ 一律先做 IF 條件縮小生效範圍(公司代碼/單據類型/工廠),不要對全公司生效。
⑥ 邏輯搬進 Z 類別,出口只留一行呼叫。
⑦ 每一段增強都要在程式開頭寫註解:誰、哪一天、哪張需求單、為什麼

練習與自我檢核

  1. 四代對照:不看筆記寫出四代增強的名稱、技術本質與對應的 TCODE。
    要點:第一代=原始碼 FORM(SPRO/SE80 VMOD/SE38);第二代=函數 CALL CUSTOMER-FUNCTION(SMOD/CMOD);第三代=Classic BAdI,執行時期產生實體,用 CL_EXITHANDLER=>GET_INSTANCE(SE18/SE19);第四代=Kernel BAdI(編譯時期實體化)+ Enhancement-Point(顯式/隱式)。
  2. 找出口(第一代):用三種方法各找一次「VA01 存檔前檢查」的出口,寫下你走的每一步。
    答案:SPRO → SD → System Modifications → User Exits 的說明文件/SE80 Package VMOD/SE93 查出 SAPMV45A → MV45AFZZ → USEREXIT_SAVE_DOCUMENT_PREPARE
  3. 第一代實作:在 MV45AFZZ 的 USEREXIT_SAVE_DOCUMENT_PREPARE 寫一段「只有自己帳號 + 自己 client 才生效」的訊息,用 VA01 驗證,再改成「訂單金額超過 100 萬且客戶信用凍結時擋單」。測完務必註解掉。
  4. 為什麼 Z_FIND_USEREXIT 找不到第一代?
    答案:它掃的是 TADIR 中 OBJECT IN ('SMOD','SXSD') 的物件;第一代是 FORM 子常式,不是 Repository 物件,不在 TADIR 裡。
  5. 出口挑選:需求是「訂單明細儲存時,把客戶主檔的客戶群組寫進 VBAP 的自訂欄位」。該用哪一支出口?為什麼不是 USEREXIT_SAVE_DOCUMENT?
    要點USEREXIT_MOVE_FIELD_TO_VBAP(VBAP_FUELLEN 結尾,每筆明細都會呼叫);SAVE_DOCUMENT 太晚且要自己 LOOP XVBAP。
  6. 第二代查表:用 SE16N 查 MODSAP,找出 MM06E005 底下所有元件,並說明 TYP 欄位 E/S/C/T 各是什麼。再查 TFDIR 確認 EXIT_SAPMM06E_007 的 MAND 是不是 'C'。
  7. CALL CUSTOMER-FUNCTION 命名規則:某標準程式 SAPMV45A 中有一行 CALL CUSTOMER-FUNCTION '003',請寫出函數名、要在 SMOD 查哪個增強、程式碼寫在哪個 INCLUDE。
    答案EXIT_SAPMV45A_003/增強組件 V45A0003/INCLUDE ZXVVAU05
  8. 第二代實作(必做):完成第 14 節的 CMOD ZSD01 + V45A0003 + ZXVVAU05,用 VA01 測到訊息跳出來,然後取消啟用並確認訊息消失。說出「程式啟用」與「CMOD 專案啟用」的差別。
  9. 採購單三合一:描述在採購單加一個客制欄位的完整步驟(結構 → 螢幕 → 函數 → 啟用),並回答:為什麼 EXIT_SAPMM06E_008 才可以更動 EKKO?
    要點:出口分「送資料給畫面」與「收資料回標準結構」,寫在只讀的出口會被下一個 PBO 覆蓋。
  10. 超級函數:在 MODX_FUNCTION_ACTIVE_CHECK 設中斷點,跑一次 ME21N,把觸發到的出口依序記錄下來,和第 13-2 節的表比對。你的系統多了或少了哪幾支?為什麼?
  11. CO02 練習:用 Z_FIND_USEREXIT 輸入 CO02,找出「SAP 記錄生產訂單修改日誌」的增強,讀它的 SMOD 文件並寫出它的呼叫時機。
  12. VOFM:說明 KOBEDKOBEV 的差別,並解釋為什麼在 KOBEV 裡存取 KOMP 欄位會導致「定價結果不穩定」。再說出定價值公式 FRM_KONDI_WERT_9xx 的回傳欄位是什麼。
    答案:KOBEV 是前置檢查、此時沒有明細資料;回傳欄位是 XKWERT
  13. VOFM 存取碼:新建一支 901 的定價公式,記錄系統是否要求 Access Key、常式編號的客戶命名空間是多少、以及「Active」旗標在哪裡打勾。
  14. 反例分析:說明為什麼 ZACTEXITFUN(直接 UPDATE tfdir SET mand = 'C')雖然「有效」卻絕對不能用在專案上,至少列出三個理由。
  15. S/4HANA 檢核:你接手一支 ECC 時代寫的 MV45AFZZ 出口,裡面有 SELECT … FROM vbukMESSAGE … TYPE 'I'。列出你在 S/4HANA 升級時要做的所有修改,並說明 SPDD/SPAU/SPAU_ENH 各在什麼時機用。
  16. 架構題:把第 14 節的 ZXVVAU05 重構成「出口只留一行、邏輯放 Z 類別」的寫法,並替 Z 類別寫一支 ABAP Unit 測試。說明這樣做對升級多顧問協作各有什麼好處。
  17. 銜接:把本章找到的每一個需求,回頭到 SE18 查查看有沒有對應的 BAdI 可以取代。這就是第 3-4 章的入口。

第 3-4 章 增強技術(二):Classic BAdI / New BAdI / Enhancement Framework(第三、四代)

這一章把「不改標準程式,卻能讓標準程式照我的規則跑」講到底。第三代 Classic BAdI 用 SE18 定義、SE19 實作、靠 CL_EXITHANDLER=>GET_INSTANCE 動態產生實例;第四代 Enhancement Framework 用 Enhancement Spot + GET BADI / CALL BADI,再加上「隱式增強點」讓你幾乎可以在標準程式的任何 FORM/METHOD 開頭結尾插自己的程式。學完之後,你會有一套固定的實戰流程:從畫面找到程式 → 從程式找到增強點 → 選對 BAdI → 寫對 method → 只擋自己的帳號,並且知道在 S/4HANA 與 Clean Core 時代,哪些寫法還能用、哪些已經是死路。

核心 TCODESE18SE19SE24SE38SE80SE84SE11SE16NSATSE30ST05SMODCMODSPAUSPAU_ENHSPDDSE09MM02ME21NME22NMIGOMD04SE93
關鍵物件CL_EXITHANDLER、IF_EX_*、CL_EX_*、SXS_ATTR、SXS_ATTRT、SXC_EXIT、SXC_CLASS、ENHSPOTHEADER、ENHHEADER、ENHOBJ、BADI_MATERIAL_CHECK、ME_PROCESS_PO_CUST、IF_PURCHASE_ORDER_ITEM_MM、MB_MIGO_BADI、MB_DOCUMENT_BADI、MB_GOODSMOVEMENT、CX_BADI_NOT_IMPLEMENTED、CX_BADI_MULTIPLY_IMPLEMENTED、EKPO、EKBE、MSEG、MATDOC

1. 四代增強技術總覽:先搞清楚你在第幾代

SAP 的「增強(Enhancement)」演進了四代。每一代都還活著、都還會在標準程式裡遇到,所以你必須一眼認出現在看到的是哪一代,才知道要去哪個交易碼做。

名稱在標準程式裡長什麼樣工具特性
第一代User Exit(客戶出口/原始碼出口)PERFORM userexit_xxx、SAPMV45A 的 MV45AFZZ 等 Z 開頭 IncludeSE38 直接寫 FORM其實是「被 SAP 預留的修改」,要 Access Key,會列入 SPAU
第二代Customer Exit(FUNCTION/SCREEN/MENU EXIT)CALL CUSTOMER-FUNCTION '001'SMOD 看定義、CMOD 建專案並實作 EXIT_* 的 ZX* Include一個 Exit 全系統只能有一個實作;不需 Access Key
第三代Classic BAdI(商業附加元件)CALL METHOD cl_exithandler=>get_instanceSE18 定義、SE19 實作物件導向;可多重使用(Multiple Use)、可過濾(Filter);一個定義可有多個實作
第四代New BAdI + Enhancement FrameworkGET BADI / CALL BADI;以及原始碼中的 ENHANCEMENT-POINTSE18/SE19(新式)、SE80/SE38 的 Enhance 模式、ADTEnhancement Spot 為容器;支援 Fallback Class、實作排序、Switch Framework;隱式增強點幾乎無所不在
老師的選用順序(S/4HANA 一樣適用)
① 先問「標準設定能不能做到?」(IMG/SPRO、容差、欄位選擇、訊息控制 OBA5)
② 做不到 → 找 BAdI(第三/四代)
③ 沒有 BAdI → 找 Enhancement Point(隱式/顯式)
④ 都沒有 → 才考慮 Modification(修改標準),需要 Access Key,且每次升級都要在 SPAU 對帳。
換句話說:能設定就不寫程式,能掛勾就不改標準。
第一條鐵律,老師在講義裡用最大的字寫著:一定要加上 IF sy-uname = 'ERPXXX' 防止干擾到全部的人。 教學機/開發機是共用的,你在 BAdI 裡寫的檢查會對所有人生效,沒有加帳號判斷,隔壁同學的 ME21N 全都會被你擋住。上正式環境時,這個條件要換成正式的判斷邏輯(工廠、公司代碼、料號群組、Z 表控制),而不是留著寫死的帳號。

2. 第三代 Classic BAdI 的運作原理

Classic BAdI 是一組互相搭配的物件,理解它們的關係,你才知道 SE18/SE19 每個畫面在做什麼:

物件命名慣例由誰產生作用
BAdI 定義(Definition)BADI_MATERIAL_CHECKZ_BADIDEF_BADITEST2SE18 建立宣告「這裡有一個掛勾點」,資料存在 SXS_ATTR
BAdI 介面(Interface)IF_EX_<定義名>SE18 自動產生(可在 SE24 看)規範方法名稱與參數;實作類別必須實作它所有方法
介面卡類別(Adapter Class)CL_EX_<定義名>SE18 自動產生執行期由它負責把呼叫轉發給所有作用中的實作
BAdI 實作(Implementation)Z_BADI_ERP000_CHECKSE19 建立對應一個實作類別 ZCL_IM_*,資料存在 SXC_EXITSXC_CLASS
Exit HandlerCL_EXITHANDLERSAP 標準靜態方法 GET_INSTANCE,執行期動態產生介面卡實例

標準程式呼叫 Classic BAdI 的固定樣式長這樣,認得這 10 行,你就認得所有第三代增強點

" 標準程式(例如 SAPLMIGO)裡的典型第三代 BAdI 呼叫
DATA lo_badi TYPE REF TO if_ex_mb_migo_badi.   "介面型別 = IF_EX_ + BAdI 名稱

CALL METHOD cl_exithandler=>get_instance
  EXPORTING
    exit_name              = 'MB_MIGO_BADI'    "BAdI 定義名稱
    null_instance_accepted = 'X'               "沒有任何實作時允許回傳空參照
  CHANGING
    instance               = lo_badi.

IF lo_badi IS BOUND.                            "沒有實作時就跳過
  CALL METHOD lo_badi->check_item
    EXPORTING
      i_line_id = lv_line_id
    CHANGING
      ct_msg    = lt_msg.
ENDIF.
為什麼 GET_INSTANCE 有時候沒有 EXPORTING? 因為 exit_name 是選擇性參數。當 CHANGING 的參照變數型別是 IF_EX_XXX 時,框架可以由型別反推 BAdI 名稱,所以講義中的簡寫 CALL METHOD cl_exithandler=>get_instance CHANGING instance = exit. 也能跑。實務上建議明寫 exit_name,除錯時比較好讀。
介面卡的魔法lo_badi 指向的不是你的 ZCL_IM_* 類別,而是 SAP 產生的 CL_EX_* 介面卡。你呼叫一次 check_item,介面卡會依序呼叫所有作用中的實作。這就是「Multiple Use(多重使用)」能成立的原因。

3. SE18:建立 Classic BAdI 定義(含多重使用與過濾器)

進入 SE18,輸入定義名稱(自建一律 Z/Y 開頭),按建立。畫面上有四個頁籤要填:(依講義步驟整理)

  1. Attributes(屬性)
    • Type:Multiple Use(多重使用)/Filter-Dependent(依過濾器)
    • Filter Type:勾了 Filter-Dependent 才要填,通常填一個 Data Element(例如 BUKRSWERKS_DLAND1
    • Instance Creation:預設每次呼叫產生新實例
  2. Interface(介面):填介面名稱,預設就是 ZIF_EX_<定義名>。存檔後按介面名稱會跳到 SE24,在那裡定義方法與參數。
  3. Subscreens(子畫面):需要在標準畫面上加自己的頁籤時才用。
  4. Imp. Example(範例實作):可選,給實作者參考用的樣板類別。
選項意義什麼時候勾陷阱
Multiple Use同一個定義可以同時有多個作用中的實作,全部都會被呼叫純檢查、純寫 Log、純通知執行順序不保證!不要讓兩個實作互相依賴。這正是「沒排序就抓錯資料」那類 bug 的根源
Filter-Dependent實作上掛過濾值,只有值相符時才被呼叫不同公司代碼/國家/工廠要不同邏輯介面的每個方法會自動多出一個 FLT_VAL importing 參數,呼叫端必須傳值
Instance Creation實例產生模式幾乎都用預設Context-dependent 只在少數 SAP 內部 BAdI 使用

介面方法在 SE24 定義時,參數方向的意義要抓對:

方向你在實作裡能做什麼
IMPORTING只能讀。標準程式把當下的資料交給你判斷
CHANGING可讀可寫,寫回去會影響標準流程(例如改欄位值、加旗標)
EXPORTING你回傳結果給標準程式(例如回傳訊息表、回傳「檢查失敗」)
EXCEPTIONS/RAISING丟例外中斷流程;Classic BAdI 多半用訊息表或 MESSAGE 而非例外
講義第一頁的紅字「同一個 interface 底下的所有 method 都要正確,否則會無效。」
Classic BAdI 的實作類別必須實作介面的每一個方法。你只想寫 PROCESS_ITEM,但介面還有 10 個方法,那 10 個方法也必須存在(可以是空的 METHOD ... ENDMETHOD.)。只要有一個方法語法錯誤,整個類別無法啟動,於是整個 BAdI 實作等於沒作用——而且不會有任何錯誤訊息告訴你,你會以為「BAdI 沒被呼叫」。這是初學者最常卡住的地方。

4. SE19:建立 Classic BAdI 實作

進入 SE19,在「Create Implementation」區選 Classic BAdI,輸入 BAdI 定義名稱,再給實作名稱(Z 開頭),系統會產生實作類別 ZCL_IM_<實作名>(依講義步驟整理)

  1. 輸入短文字說明。
  2. 切到 Interface 頁籤,會看到介面的所有方法列表。
  3. 雙擊要寫的方法 → 進入類別編輯器 → 寫程式 → 存檔。
  4. 回到 SE19 按 Activate(啟動)沒有啟動的實作等於不存在。
  5. 指定傳輸請求(Workbench Request)。

實作的骨架就是這樣,記得第一行永遠是帳號/範圍防護

METHOD if_ex_z_badidef_baditest2~method.

  "★★ 防護:只對自己的帳號生效,避免干擾同一台機器的其他人 ★★
  CHECK sy-uname = 'ERP000'.

  "把傳進來的字串轉大寫後寫回去(CHANGING 參數)
  parameter = to_upper( parameter ).

ENDMETHOD.
講義原文寫的是 TRANSLATE parameter TO UPPER CASE.——這在 S/4HANA 仍可執行,但現代寫法建議用內建函數 to_upper( ) / to_lower( ) / to_mixed( ),可以直接寫在運算式裡,不必先搬到變數。
SE19 的另一個用法:反查。 講義 02-19 的「省思過程」就是這招——在 SE19 輸入你自己的實作名稱(例如 Z_BADI_ERP000_CHECK)按 Change / Display,畫面上就會顯示它屬於哪一個 BAdI 定義與哪一個 Enhancement Spot。忘記自己當初掛在哪裡時,這是最快的路。

5. 自建第三代增強完整實例:Z_BADIDEF_BADITEST2

講義第 03 節的完整流程,把「定義 → 實作 → 呼叫」三件事一次走完,這是理解 Classic BAdI 最好的沙盤。

步驟 1SE18 建立定義 Z_BADIDEF_BADITEST2,介面 ZIF_EX__BADIDEF_BADITEST2,在 SE24 為介面新增一個方法 METHOD,參數 PARAMETER 型別 STRING,方向 CHANGING

步驟 2SE19 建立實作,方法內容如下。

METHOD zif_ex__badidef_baditest2~method.
  "示範:把呼叫端傳來的字串改成大寫再送回去
  parameter = to_upper( parameter ).
ENDMETHOD.

步驟 3:寫一支測試程式(講義的 Z_BUSINESSADDIN),親手做一次標準程式在做的事。以下是現代化改寫版

REPORT z_businessaddin.

"講義原文有一行 CLASS cl_exithandler DEFINITION LOAD.
"那是舊式的「先載入類別定義」宣告,S/4HANA 已不需要,可直接刪除。

PARAMETERS p_word TYPE string LOWER CASE
           DEFAULT 'change to upper case'.

START-OF-SELECTION.

  DATA lo_badi TYPE REF TO zif_ex__badidef_baditest2.

  "① 由 Exit Handler 動態取得介面卡實例
  CALL METHOD cl_exithandler=>get_instance
    EXPORTING
      exit_name              = 'Z_BADIDEF_BADITEST2'
      null_instance_accepted = 'X'
    CHANGING
      instance               = lo_badi.

  "② 沒有任何作用中的實作時,lo_badi 會是空的,一定要擋
  IF lo_badi IS NOT BOUND.
    MESSAGE |目前沒有任何作用中的 BAdI 實作| TYPE 'I'.
    RETURN.
  ENDIF.

  DATA(lv_word) = p_word.
  WRITE: / |原始字串:{ lv_word }|.

  "③ 呼叫 BAdI,CHANGING 參數會被實作改掉
  lo_badi->method( CHANGING parameter = lv_word ).

  WRITE: / |增強後字串:{ lv_word }|.
不要忘了 IS BOUND 檢查。 若沒有作用中的實作,且呼叫時沒有傳 null_instance_accepted = 'X'GET_INSTANCE 會丟出例外;若傳了 'X',回傳的是空參照,接著直接呼叫方法就會得到 CX_SY_REF_IS_INITIAL 短傾印。標準程式一律先 IF lo_badi IS BOUND.,你自己寫也要照做。

如果這個 BAdI 是 Filter-Dependent,呼叫時要多帶過濾值:

"Filter-dependent 的 Classic BAdI:介面方法會自動多一個 FLT_VAL 參數
DATA lo_badi TYPE REF TO zif_ex_z_badi_by_bukrs.

CALL METHOD cl_exithandler=>get_instance
  EXPORTING
    exit_name = 'Z_BADI_BY_BUKRS'
  CHANGING
    instance  = lo_badi.

IF lo_badi IS BOUND.
  lo_badi->check_document(
    EXPORTING flt_val  = lv_bukrs      "只有掛這個公司代碼的實作會被執行
    CHANGING  cs_data  = ls_data ).
ENDIF.

6. 怎麼找到「該用哪個 BAdI」——五種找法

這是增強工作真正花時間的地方。寫程式 10 分鐘,找增強點 3 小時,講義 02-03 到 02-08 整整六頁都在做這件事。下面把老師的流程整理成可重複執行的 SOP。

找法 A:從畫面找到程式,再在程式裡搜尋關鍵字(講義主線)

  1. 在交易畫面(例如 MIGO)按 F1技術資訊,或選單 System → Status,取得程式名稱(MIGO 是 SAPLMIGO)。
  2. SE38SE80 開啟該程式 → 選單 Utilities → Find in Source Code(或 Ctrl+F 勾「In main program」+「In includes」)。
  3. 搜尋這四組關鍵字,就能把四代增強一網打盡:
    • CL_EXITHANDLER → 第三代 Classic BAdI
    • GET BADICALL BADI → 第四代 New BAdI
    • CUSTOMER-FUNCTION → 第二代 Customer Exit
    • USEREXIT → 第一代 User Exit
  4. 找到後,看 exit_name = '...' 的字面值,那就是 BAdI 名稱(講義在 MIGO 找到的是 MB_MIGO_BADI)。
  5. 把候選 BAdI 拿到 SE18 看它的方法清單與文件,確認哪個方法在你要的時間點被呼叫。
講義 02-07 誠實寫著「再搭配問了 ChatGPT 之後,選定了 MB_DOCUMENT_BADI」。這一步的判斷準則是:MB_MIGO_BADI 偏畫面/逐行互動(適合即時檢查與加欄位);MB_DOCUMENT_BADI 偏文件層級(適合過帳前的整份檢查)。要做「整張收貨單累加後判斷是否超交」,就該選文件層級的那一個。

找法 B:SE11 直接查 BAdI 目錄表(講義 03-04)

"用 SE16N 或寫一支小程式查「Classic BAdI 定義清單」
"SXS_ATTR  = BAdI 定義屬性;SXS_ATTRT = BAdI 定義的多語文字
SELECT a~exit_name,        "BAdI 定義名稱
       a~multiple_use,     "是否多重使用
       a~filter_type,      "過濾器型別
       t~text              "說明文字
  FROM sxs_attr AS a
  LEFT OUTER JOIN sxs_attrt AS t
    ON  t~exit_name = a~exit_name
    AND t~sprsl     = @sy-langu
  WHERE t~text LIKE '%Purchase Order%'   "用關鍵字撈
  INTO TABLE @DATA(lt_badi).

cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                        CHANGING  t_table      = lt_badi ).
lo_alv->get_functions( )->set_all( abap_true ).
lo_alv->display( ).
表名內容
SXS_ATTRClassic BAdI 定義的屬性(multiple use、filter type、介面名稱)
SXS_ATTRTClassic BAdI 定義的文字說明(多語)
SXC_EXITClassic BAdI 實作與定義的對應
SXC_CLASS實作對應的類別名稱
SXC_ATTR / SXC_ATTRT實作的屬性與文字(含是否啟動)
MODSAP / MODACT第二代 Customer Exit 的定義(SMOD)與專案指派(CMOD)
ENHSPOTHEADER / ENHSPOTOBJ第四代 Enhancement Spot 的表頭與內容物件
ENHHEADER / ENHOBJ第四代 Enhancement Implementation 的表頭與物件
ENHLOG增強的變更/升級調整記錄

找法 C:SE84 增強樹(講義 02-20 用它反查自己的實作)

SE84(Repository Information System)→ Enhancements 節點底下:

  • Customer Exits → Enhancements:第二代
  • Business Add-Ins → Definitions / Implementations:第三代,可用「Package(套件)」篩選,例如 MM 相關填 ME*MB*
  • Enhancement Spots / Enhancement Implementations:第四代
用 Package 找 BAdI 是最高效的招式:先在交易畫面 System → Status 看該程式屬於哪個 Package(例如 MIGO 是 MBMBGD),再到 SE84 用該 Package 列出全部 BAdI 定義,通常十來個,一眼掃過去就找到了。

找法 D:Debug 中設中斷點(最可靠,一定找得到「真正會被呼叫」的那一個)

  1. 執行交易 → /h 進入除錯器。
  2. 選單 Breakpoints → Breakpoint at → Method,輸入類別 CL_EXITHANDLER、方法 GET_INSTANCE
  3. 按 F8 繼續,每停一次就看變數 exit_name,把過程中出現的 BAdI 名稱全部抄下來。
  4. 第四代則設 Breakpoint at Statement,輸入 GET BADICALL BADI
這個方法的價值在於:它列出的是實際執行路徑上真的會被呼叫的 BAdI,不會像靜態搜尋那樣抓到一堆走不到的分支。缺點是清單很長(一次交易可能觸發數十個 BAdI),要有耐心。

找法 E:SAT/SE30 執行追蹤

SAT(S/4HANA 取代舊的 SE30)做一次交易的執行追蹤,在結果的「呼叫階層」裡過濾類別名稱含 CL_EX_CL_IM_ZCL_IM_ 的項目,就能看到整個流程呼叫過哪些 BAdI 介面卡與實作。搭配 ST05(SQL Trace)還能同時看到它們讀了哪些表。

7. 實戰一:BADI_MATERIAL_CHECK 檢核物料主檔(MM02)

講義 01-02 到 01-04 的例子:用 BADI_MATERIAL_CHECK(介面 IF_EX_BADI_MATERIAL_CHECK)在 MM01MM02 存檔前檢查物料主檔資料。這是第三代 BAdI 最經典的入門題:先在 SE24 看介面有哪些方法與參數,再到 SE18 看定義,最後到 SE19 寫實作。

*&---------------------------------------------------------------------*
*&  BAdI  : BADI_MATERIAL_CHECK
*&  Method: CHECK_DATA  (物料主檔存檔前檢查)
*&---------------------------------------------------------------------*
METHOD if_ex_badi_material_check~check_data.

  "★ 防護:只對自己的測試帳號生效
  CHECK sy-uname = 'ERP000'.

  "wmara / wmarc / wmard ... 是標準傳進來的工作區(結構視版本而異,
  "實際參數請以 SE18 → Interface → SE24 顯示為準)
  IF wmara-mtart = 'FERT' AND wmara-matkl IS INITIAL.
    "回傳訊息給 MM02 畫面:塞進標準的訊息表
    APPEND VALUE #( msgid = 'ZMM'
                    msgty = 'E'
                    msgno = '010'
                    msgv1 = wmara-matnr )
           TO ch_messages.
  ENDIF.

ENDMETHOD.
物料主檔的 BAdI 家族還有 MATERIAL_ALLOWED_FIELDS(放行欄位)、BADI_MATERIAL_REF(複製參考物料)、MG_MASS_NEWSEG(大量維護)。在 SE18 用「Package = MG」列出來看,一次就掌握全貌。BADI_MATERIAL_CHECK 的確切方法名稱與參數清單各版本略有差異,動手前務必用 SE18 → Interface 對照。
不要在物料主檔 BAdI 裡直接發 MESSAGE ... TYPE 'E' MM01/MM02 的存檔流程有自己的訊息收集機制,直接發 E 訊息可能造成畫面卡死或無法回到欄位。正確做法是把訊息 APPEND 到標準提供的訊息表參數,讓標準程式決定怎麼顯示。

8. 實戰二:ME_PROCESS_PO_CUST 採購單強制結案

講義第 01 節的主線場景:採購交貨如何強制結案。業務上,PO 已經收了 8 件、訂了 10 件,剩下 2 件供應商不送了,MRP(MD04)還一直顯示 2 件在途需求。解法是把 EKPO-ELIKZ(交貨完成指示碼,Delivery Completed) 打勾。

要在 ME22N 存檔時自動打勾,就用 ME_PROCESS_PO_CUST(介面 IF_EX_ME_PROCESS_PO_CUST)。這個 BAdI 的參數不是結構,而是物件參照,要用 GET_DATA( )SET_DATA( ) 存取,這點跟一般 BAdI 很不一樣。

方法時機典型用途
INITIALIZE物件初始化清全域變數
OPEN開始處理一張 PO準備快取
PROCESS_HEADER抬頭被處理時改抬頭欄位(EKKO)
PROCESS_ITEM每一個項目被處理時改項目欄位(EKPO),如 ELIKZ
PROCESS_SCHEDULE交期行改 EKET 交期/數量
PROCESS_ACCOUNT會計指派改成本中心/WBS
CHECK存檔前總檢查發錯誤訊息、擋存檔(設 ch_failed
POST過帳時寫 Z 表、觸發後續
FIELDSELECTION_ITEM欄位選擇把欄位改成必填/唯讀/隱藏
CLOSE處理結束釋放資源
*&---------------------------------------------------------------------*
*&  BAdI  : ME_PROCESS_PO_CUST
*&  Method: PROCESS_ITEM  (每個 PO 項目都會進來一次)
*&  目的  : 指定料號在指定條件下,自動打上「交貨完成」旗標 ELIKZ
*&---------------------------------------------------------------------*
METHOD if_ex_me_process_po_cust~process_item.

  "★ 防護:只對自己的帳號生效,避免影響其他人的 ME21N/ME22N
  CHECK sy-uname = 'ERP000'.

  "① 取出目前項目的資料(回傳 MEPOITEM 結構)
  DATA(ls_item) = im_item->get_data( ).

  "② 已刪除或已結案的就不用管
  IF ls_item-loekz IS NOT INITIAL OR ls_item-elikz = abap_true.
    RETURN.
  ENDIF.

  "③ 只處理指定料號
  CHECK ls_item-matnr = 'CHLK1300'.

  "④ 查歷史已收量(EKBE:S 借方加、H 貸方減)
  SELECT SINGLE
         SUM( CASE shkzg WHEN 'H' THEN menge * -1 ELSE menge END ) AS wemng
    FROM ekbe
    WHERE ebeln = @ls_item-ebeln
      AND ebelp = @ls_item-ebelp
      AND vgabe = '1'                    "1 = 收貨(Goods Receipt)
    INTO @DATA(lv_wemng).

  "⑤ 已收量 >= 訂購量的 90% 就視為可結案(門檻請依實際需求調整)
  IF lv_wemng >= ls_item-menge * '0.9'.
    ls_item-elikz = abap_true.           "打上交貨完成指示碼
    im_item->set_data( ls_item ).        "★ 一定要寫回去,否則不生效
  ENDIF.

ENDMETHOD.

如果要的是「擋下存檔」,就寫在 CHECK 方法,並用 MM 採購模組專屬的訊息巨集:

METHOD if_ex_me_process_po_cust~check.

  CHECK sy-uname = 'ERP000'.

  "im_header 是抬頭物件,get_items( ) 取回所有項目物件
  DATA(lt_items) = im_header->get_items( ).

  LOOP AT lt_items INTO DATA(ls_wrap).
    DATA(ls_item) = ls_wrap-item->get_data( ).

    IF ls_item-matnr = 'CHLK1300' AND ls_item-menge > 100.
      "MM 採購專用巨集:把訊息掛到採購單的訊息清單上
      "參數:訊息類型 訊息類別 訊息號碼 v1 v2 v3 v4
      mmpur_message_forced 'E' 'ZMM' '020'
                           ls_item-ebelp '' '' ''.
      ch_failed = abap_true.        "★ 設了才會真的擋下存檔
    ENDIF.
  ENDLOOP.

ENDMETHOD.
三個實務坑
get_data( ) 拿到的是複本,改了不寫回 set_data( ) 完全沒作用。
② 在 PROCESS_ITEM 裡發 MESSAGE ... TYPE 'E' 會讓 ME21N 直接當掉;要擋存檔請一律改用 CHECK 方法 + ch_failed = abap_true
③ 這個 BAdI 是 Multiple Use,你的實作跟其他人(或 SAP Note 帶來的實作)會同時被呼叫,執行順序不保證。如果兩個實作都改同一個欄位,結果會不穩定——這正是老師在專案上遇到「一下抓到 H、一下抓到 E,成本結果不穩定」那類問題的同型 bug:沒有明確排序的多重來源,就會有不穩定的答案
驗證流程(講義 01-06~01-09)ME21N 建 PO → MD04 看到需求 → ME22N 存檔觸發 BAdI 打上 ELIKZ → 再看一次 MD04,在途需求消失。用 MD04 前後對照來證明增強真的生效,比在除錯器裡看變數更有說服力。

9. 第四代:Enhancement Spot 與 New BAdI 架構

第四代把「增強」抽象成兩個容器,觀念上就是「定義端」與「實作端」各有一個資料夾:

層級定義端(SAP 或你建立)實作端(你建立)
容器Enhancement Spot(增強點)Enhancement Implementation(增強實作)
內容BAdI Definition(可多個)BAdI Implementation(可多個)
最底層Interface(IF_*,名稱自由,不必 IF_EX_)Implementing Class(ZCL_*
工具SE18 / SE80 / ADTSE19 / SE80 / ADT
另有一種Composite Enhancement Spot(把多個 Spot 群組起來)Composite Enhancement Implementation

New BAdI 相對第三代多出來的能力,每一個都很實用:

能力說明在哪裡設
Fallback Class(後備類別)沒有任何作用中的實作時,框架自動改叫這個類別。等於「內建的預設行為」BAdI 定義的屬性頁
Default Implementation實作標成「預設」,只要有其他非預設實作作用中,它就自動退場BAdI 實作的屬性
Implementation Sequence(排序)多重使用時可指定執行順序,解決第三代「順序不保證」的老問題BAdI 定義勾「Sortable」,在實作上填順序號
Filter(過濾器)可定義多個過濾欄位,支援 =BETWEENCP 等運算子與 AND/OR 組合BAdI 定義的 Filter 頁 + 實作上填值
Instance Creation ModeNewly(每次新建)/Reusing(重用同一實例)/Context-dependentBAdI 定義屬性
Switch Framework整個 Enhancement Implementation 可掛在 Switch 上,靠 Business Function 開關建立時指定 Switch ID
Internal / External 呼叫控制SAP 可宣告某 BAdI 只供內部使用(客戶不得實作)BAdI 定義的 Contract
「重用實例(Reusing)」的重要性:如果你的 BAdI 實作需要跨方法保存狀態(例如在 OPEN 撈一批資料快取起來,給後面每個 PROCESS_ITEM 用),就必須把 Instance Creation Mode 設成 Reusing,否則每次呼叫都是新實例,類別屬性永遠是空的。第三代 BAdI 沒有這個選項,只能用 CLASS-DATA(靜態屬性)勉強達成——而靜態屬性在同一個 session 裡不會自動清空,是很常見的髒資料來源。

10. GET BADI / CALL BADI 語法與例外處理

第四代的呼叫語法變成兩個獨立的 ABAP 指令,不再是呼叫類別方法。這也是為什麼你在標準程式裡搜尋 CL_EXITHANDLER 找不到東西時,要改搜 GET BADI

"===== 第四代 New BAdI 的標準呼叫樣式 =====
"① 宣告:型別直接就是「BAdI 定義名稱」(BAdI 參照型別)
DATA lo_badi TYPE REF TO zbadi_demo01.

TRY.
    "② 取得 BAdI handle;有 filter 就在這裡給值
    GET BADI lo_badi
      FILTERS
        bukrs = lv_bukrs
        werks = lv_werks.

    "③ 呼叫方法;多重使用時,框架會依序呼叫所有符合的實作
    CALL BADI lo_badi->print
      EXPORTING input  = lv_input
      IMPORTING output = DATA(lv_output).

  CATCH cx_badi_not_implemented.
    "沒有任何實作、也沒有 fallback class → 走原本的預設邏輯
    lv_output = lv_input.

  CATCH cx_badi_multiply_implemented.
    "非多重使用的 BAdI 卻有多個作用中實作 → 設定錯誤
    MESSAGE |BAdI 設定錯誤:同時有多個作用中實作| TYPE 'E'.

  CATCH cx_badi_filter_error.
    "過濾值缺漏或型別不符
    MESSAGE |BAdI 過濾器設定錯誤| TYPE 'E'.

  CATCH cx_badi_initial_reference.
    "handle 是空的就呼叫 CALL BADI
    MESSAGE |BAdI handle 尚未取得| TYPE 'E'.

  CATCH cx_root INTO DATA(lo_err).
    MESSAGE lo_err->get_text( ) TYPE 'E'.
ENDTRY.
指令作用要點
GET BADI handle向框架要一個 BAdI handle此時才決定「哪些實作符合過濾條件」;成本較高,不要放在迴圈裡
GET BADI ... FILTERS f = v帶過濾值定義有幾個 filter 就要給幾個;漏給會 CX_BADI_FILTER_ERROR
CALL BADI handle->meth呼叫方法多重使用時是「一次呼叫、多個實作依序執行」
GET BADI ... CONTEXT objContext-dependent BAdI少見,SAP 內部使用居多
效能口訣:GET 一次,CALL 多次。 GET BADI 會去查詢有哪些實作、算過濾條件,開銷不小。正確做法是在迴圈外面 GET,迴圈裡面 CALL:
GET BADI lo_badi FILTERS bukrs = p_bukrs.   "★ 迴圈外,只做一次

LOOP AT lt_items INTO DATA(ls_item).
  CALL BADI lo_badi->check_item              "★ 迴圈內,只有呼叫
    EXPORTING is_item = ls_item
    CHANGING  ct_msg  = lt_msg.
ENDLOOP.
Fallback Class 讓 CX_BADI_NOT_IMPLEMENTED 幾乎不會發生。 如果 BAdI 定義掛了 Fallback Class,即使沒有任何客戶實作,框架也會呼叫 fallback,因此呼叫端不必寫「沒實作時的預設邏輯」。你自己設計 BAdI 時,建議一律準備 Fallback Class,把預設行為寫在裡面,呼叫端的程式碼會乾淨很多。

11. 自建第四代增強完整實例:ZBADI_DEMO01

講義第 04 節的完整清單,把四層物件的名稱都列出來了,這正是第四代的四層結構:

層級物件名稱建立位置
Enhancement SpotZBADI_DEMO01SE18(新式)
BAdI Definition(Element definition)ZBADI_DEMO01SE18 Spot 內
InterfaceZBADI_DEMO_ITF01SE24(由 SE18 帶出)
Enhancement ImplementationZBADI_DEMO01_IMPSE19
Implementing ClassZBADI_DEMO01_CLASSSE19 自動建立

步驟(依講義步驟整理)

  1. SE18 → 輸入 Enhancement Spot 名稱 ZBADI_DEMO01 → Create。指定 Composite Spot(可留空)與 Package。
  2. 在 Spot 的 Enh. Spot Element Definitions 節點按右鍵 → Create BAdI,輸入定義名稱、勾選 Multiple Use/Limited Filter Use,填入介面名稱 ZBADI_DEMO_ITF01
  3. 雙擊介面名稱進 SE24,新增方法 PRINT:參數 INPUT(IMPORTING, STRING)、OUTPUT(EXPORTING, STRING)。啟動。
  4. 回 SE18 啟動 Spot;再進 SE19 → New BAdI → 輸入 Enhancement Spot → 建立 Enhancement Implementation ZBADI_DEMO01_IMP → 建立 BAdI Implementation 與實作類別 ZBADI_DEMO01_CLASS
  5. 雙擊方法寫程式 → 啟動。三個東西都要 active:Spot、Implementation、Class。
METHOD zbadi_demo_itf01~print.
  "講義原文用 CONCATENATE,S/4HANA 建議改用字串範本
  output = |你輸入的是:{ input }|.
ENDMETHOD.

測試程式。講義原本的 ZBADITEST手動 CREATE OBJECT 實作類別再指派給介面參照——這在教學上可以看清楚「介面參照指向實作物件」的多型概念,但它繞過了 BAdI 框架:不會套用過濾器、不會呼叫多個實作、不會走 fallback。正式程式一定要用 GET BADI / CALL BADI。兩種寫法對照如下:

REPORT zbaditest.

PARAMETERS: p_str TYPE string DEFAULT 'ABAP',
            p_mode TYPE c LENGTH 1 DEFAULT '2'.   "1=手動 2=框架

START-OF-SELECTION.

  IF p_mode = '1'.
*----------------------------------------------------------------------*
* 寫法 A(講義原文):直接 new 實作類別 —— 只適合觀念示範,不要用在正式程式
*----------------------------------------------------------------------*
    DATA(lo_impl) = NEW zbadi_demo01_class( ).     "取代舊的 CREATE OBJECT
    DATA lo_itf TYPE REF TO zbadi_demo_itf01.
    lo_itf = lo_impl.                              "向上轉型(多型)

    lo_itf->print( EXPORTING input  = p_str
                   IMPORTING output = DATA(lv_out1) ).
    WRITE / lv_out1.

  ELSE.
*----------------------------------------------------------------------*
* 寫法 B(正確):走 BAdI 框架
*----------------------------------------------------------------------*
    DATA lo_badi TYPE REF TO zbadi_demo01.         "型別 = BAdI 定義名稱

    TRY.
        GET BADI lo_badi.

        CALL BADI lo_badi->print
          EXPORTING input  = p_str
          IMPORTING output = DATA(lv_out2).

        WRITE / lv_out2.

      CATCH cx_badi_not_implemented.
        WRITE / |目前沒有作用中的 BAdI 實作,走預設邏輯|.
        WRITE / p_str.
    ENDTRY.
  ENDIF.
SE18 與 SE19 的分工(講義 04-02)SE19 只看得到 Implementation;要看「這個 BAdI 定義底下總共有幾個實作、分別叫什麼」,要進 SE18 打開定義,再點選單 Enhancement Spot → Overview(或工具列的「Implementation」按鈕)。這是老師特別註記的操作細節,很多人找不到。

12. Classic BAdI 被「強制升級」為 New BAdI

講義 02-11「被強迫升級」、02-17「強制轉回 NEW BADI 了」講的就是這件事。SAP 已經把大量 Classic BAdI 定義遷移(migrate)成 New BAdI,並收進 Enhancement Spot 裡。於是會發生這個現象:

  • 你在 SE19 選「Classic BAdI」輸入 MB_DOCUMENT_BADI,系統跳出訊息說這個 BAdI 已經遷移,要求你改用 New BAdI 的方式建立
  • 照做之後,你得到的是一個 Enhancement Implementation(而不是舊的 Classic Implementation),它掛在 Enhancement Spot MB_GOODSMOVEMENT 底下(講義 02-21)。
  • 介面名稱與方法簽章完全沒變,還是 IF_EX_MB_DOCUMENT_BADI~MB_DOCUMENT_BEFORE_UPDATE。所以你原本寫好的程式碼可以原封不動貼過去。
怎麼判斷一個 BAdI 是不是已經遷移?SE18 輸入名稱按「Display」,如果畫面直接開的是 Enhancement Spot 的樹狀結構,它就是(或已遷移成)New BAdI;如果開的是舊式的 Attributes/Interface 四頁籤畫面,那還是純 Classic。另一個線索:New BAdI 的定義畫面上會有 Fallback ClassFilter 這兩個 Classic 沒有的欄位。

MIGO 禁止超交(講義 02-01~02-18)的完整實作。這是全章最有價值的一段程式,我把它改寫成 S/4HANA 的現代寫法並修掉幾個坑:

*&---------------------------------------------------------------------*
*&  BAdI  : MB_DOCUMENT_BADI (已遷移,屬 Enhancement Spot MB_GOODSMOVEMENT)
*&  Method: MB_DOCUMENT_BEFORE_UPDATE
*&  目的  : MIGO 收貨時,料號 CHLK1300 且使用者 ERP000 不可超交
*&---------------------------------------------------------------------*
METHOD if_ex_mb_document_badi~mb_document_before_update.

  "① 防護:只針對 MIGO + 指定使用者
  CHECK sy-tcode = 'MIGO' AND sy-uname = 'ERP000'.

  TYPES: BEGIN OF ty_po,
           ebeln    TYPE ekpo-ebeln,
           ebelp    TYPE ekpo-ebelp,
           po_qty   TYPE ekpo-menge,   "訂購量
           hist_qty TYPE ekpo-menge,   "歷史已收量
           new_qty  TYPE ekpo-menge,   "本次收貨量
         END OF ty_po.

  DATA lt_po TYPE SORTED TABLE OF ty_po WITH UNIQUE KEY ebeln ebelp.

  "② 收集本次過帳(XMSEG)內的目標 PO 行;同一 PO 行可能出現多次,要累加
  LOOP AT xmseg ASSIGNING FIELD-SYMBOL(<ls_mseg>)
       WHERE bwart = '101'                "採購收貨
         AND matnr = 'CHLK1300'
         AND ebeln IS NOT INITIAL.

    READ TABLE lt_po ASSIGNING FIELD-SYMBOL(<ls_po>)
         WITH TABLE KEY ebeln = <ls_mseg>-ebeln
                        ebelp = <ls_mseg>-ebelp.
    IF sy-subrc = 0.
      <ls_po>-new_qty += <ls_mseg>-menge.
    ELSE.
      INSERT VALUE #( ebeln   = <ls_mseg>-ebeln
                      ebelp   = <ls_mseg>-ebelp
                      new_qty = <ls_mseg>-menge ) INTO TABLE lt_po.
    ENDIF.
  ENDLOOP.

  "③ FOR ALL ENTRIES 空表陷阱:空表會變成「沒有 WHERE 條件」,撈回整張表!
  IF lt_po IS INITIAL.
    RETURN.
  ENDIF.

  "④ 取 PO 訂購量
  SELECT ebeln, ebelp, menge
    FROM ekpo
    FOR ALL ENTRIES IN @lt_po
    WHERE ebeln = @lt_po-ebeln
      AND ebelp = @lt_po-ebelp
      AND loekz = @space                  "排除已刪除的行
    INTO TABLE @DATA(lt_ekpo).

  "⑤ 從 EKBE 彙總歷史已收量;直接用 CASE 在資料庫端加總,少搬一次資料
  SELECT ebeln, ebelp,
         SUM( CASE shkzg WHEN 'H' THEN menge * -1 ELSE menge END ) AS qty
    FROM ekbe
    FOR ALL ENTRIES IN @lt_po
    WHERE ebeln = @lt_po-ebeln
      AND ebelp = @lt_po-ebelp
      AND vgabe = '1'                     "1 = 收貨
      AND bwart IN ( '101', '102', '122', '123' )
    GROUP BY ebeln, ebelp
    INTO TABLE @DATA(lt_ekbe).

  "⑥ 回填訂購量與歷史量
  LOOP AT lt_po ASSIGNING FIELD-SYMBOL(<po>).

    IF line_exists( lt_ekpo[ ebeln = <po>-ebeln ebelp = <po>-ebelp ] ).
      <po>-po_qty = lt_ekpo[ ebeln = <po>-ebeln ebelp = <po>-ebelp ]-menge.
    ENDIF.

    IF line_exists( lt_ekbe[ ebeln = <po>-ebeln ebelp = <po>-ebelp ] ).
      <po>-hist_qty = lt_ekbe[ ebeln = <po>-ebeln ebelp = <po>-ebelp ]-qty.
    ENDIF.
  ENDLOOP.

  "⑦ 超交判斷
  LOOP AT lt_po INTO DATA(ls_chk).
    IF ls_chk-hist_qty + ls_chk-new_qty > ls_chk-po_qty.
      "MESSAGE ... WITH 每個變數最多 50 字元,超過會被截斷,所以拆四段
      MESSAGE e001(zmm) WITH |CHLK1300 不可超交|
                             |PO { ls_chk-ebeln }-{ ls_chk-ebelp }|
                             |訂購 { ls_chk-po_qty NUMBER = USER }|
                             |已收 { ls_chk-hist_qty NUMBER = USER }|.
    ENDIF.
  ENDLOOP.

ENDMETHOD.
這段程式的五個關鍵坑
FOR ALL ENTRIES 空表 = 撈全表lt_po 為空時,SAP 會把 WHERE 條件整段拿掉,你會把整張 EKPO 搬進記憶體。必須先 IF lt_po IS INITIAL. RETURN. ENDIF.
FOR ALL ENTRIES 會自動去重複,且驅動內表的欄位型別必須與 DB 欄位一致,否則會有隱性型別轉換。
方法選錯就沒救MB_DOCUMENT_BEFORE_UPDATE對話工作程序執行,可以發 MESSAGE E 把使用者擋回畫面;但同一個 BAdI 的 MB_DOCUMENT_UPDATE更新工作(Update Task)執行,那裡發 E 訊息會造成更新中止(Update Terminated),使用者收到快遞郵件、資料半套,是很嚴重的事故。
EKBE 的 SHKZG:'S' 借方(收貨,加)、'H' 貸方(退貨/沖銷,減)。忘了處理 H,退貨過的 PO 會永遠被誤判為超交。
MESSAGE ... WITH 每個變數上限 50 字元,且最多 4 個。講義原文把兩段長字串用 & 接成一個變數傳,會被截掉一半。
先問「標準能不能做」! 禁止超交其實是採購單的標準設定EKPO-UEBTO(超交容差百分比)設 0、EKPO-UEBTK(無限超交)不勾,SAP 就會自己擋。或用 OMCQ 把訊息 M7 系列的超交警告從 W 改成 E。能設定就不要寫 BAdI——這是第 1 節那個口訣的活教材。會寫這支 BAdI 的理由,是需求裡有「只擋特定料號+特定使用者」這種標準做不到的條件。

13. Enhancement Framework:隱式增強點(Implicit Enhancement Point)

這是第四代最強大也最危險的能力:SAP 在每一支 ABAP 程式的固定位置,都預先埋好了看不見的掛勾點,你不必等 SAP 提供 BAdI,就能在標準程式插入自己的程式碼,而且不算修改(Modification)、不需要 Access Key

位置可以做什麼常見用途
程式/Include 的最開頭與最結尾宣告全域變數、寫收尾邏輯宣告要跨 FORM 使用的變數
FORM 的第一個可執行語句前、ENDFORM 前讀/改該 FORM 的區域變數與參數最常用的插入點
METHOD ... ENDMETHOD 的開頭與結尾同上OO 標準程式的插入點
FUNCTION ... ENDFUNCTION 的開頭與結尾同上改函數的回傳值
MODULE ... ENDMODULE 的開頭與結尾PBO/PAI 邏輯Dynpro 畫面控制
函數模組的參數介面末端新增選擇性參數擴充標準 FM 的介面
類別各 Section(PUBLIC/PROTECTED/PRIVATE)末端新增屬性與方法擴充標準類別
DDIC 結構的末端Append Structure(需 Enhancement Category 允許)加自訂欄位

操作步驟(在 SE38SE80 開啟標準程式):(依講義步驟整理)

  1. 顯示模式開啟程式後,按工具列的 Enhance(螺絲起子+扳手圖示,或 Shift+F4),程式碼底色轉為灰/黃,進入增強模式。
  2. 選單 Edit → Enhancement Operations → Show Implicit Enhancement Options,程式碼中會出現一條條虛線——每一條虛線就是一個隱式增強點。
  3. 把游標放在要用的虛線上 → 右鍵 Enhancement Implementation → Create
  4. Code(Source Code Plug-In),輸入 Enhancement Implementation 名稱(Z 開頭)與短文字,選 Package 與傳輸請求。
  5. 系統在該位置產生一組 ENHANCEMENT ... ENDENHANCEMENT 區塊(底色亮黃),在裡面寫程式。
  6. 存檔 → 啟動(Ctrl+F3)
*----------------------------------------------------------------------*
* 隱式增強:插在標準 FORM 的結尾(ENDFORM 之前)
* Enhancement Implementation: ZENH_MIGO_NO_OVERDELIVERY
*----------------------------------------------------------------------*
ENHANCEMENT 1 ZENH_MIGO_NO_OVERDELIVERY.    "active version

  "★ 防護:只對指定帳號、指定交易生效
  IF sy-uname = 'ERP000' AND sy-tcode = 'MIGO'.

    "此處可直接讀取這個 FORM 的區域變數與全域變數
    IF gs_goitem-matnr = 'CHLK1300'.

      SELECT SINGLE menge, uebto, uebtk
        FROM ekpo
        WHERE ebeln = @gs_goitem-ebeln
          AND ebelp = @gs_goitem-ebelp
        INTO @DATA(ls_ekpo).

      IF sy-subrc = 0 AND ls_ekpo-uebtk = abap_false.
        "已收量 + 本次量 是否超過訂購量
        SELECT SINGLE
               SUM( CASE shkzg WHEN 'H' THEN menge * -1 ELSE menge END )
          FROM ekbe
          WHERE ebeln = @gs_goitem-ebeln
            AND ebelp = @gs_goitem-ebelp
            AND vgabe = '1'
          INTO @DATA(lv_hist).

        IF lv_hist + gs_goitem-erfmg > ls_ekpo-menge.
          MESSAGE |料號 CHLK1300 不可超交(訂購 { ls_ekpo-menge NUMBER = USER })|
                  TYPE 'E'.
        ENDIF.
      ENDIF.
    ENDIF.
  ENDIF.

ENDENHANCEMENT.
隱式增強的三大風險
你在標準程式裡執行任意程式碼,SAP 完全不保證上下文。 你用到的區域變數可能在下個 Support Package 就被改名或刪除,程式會在升級後語法錯誤,而且是在客戶的正式環境爆掉。
效能:插在迴圈裡的 SELECT 會被執行成千上萬次。要撈資料請先在迴圈外撈成內表。
難以追蹤:別人看標準程式時,如果沒開 Enhance 模式就看不到你的程式碼。務必在增強區塊第一行寫清楚:誰、什麼時候、為什麼、對應的需求單號。
結論:隱式增強是最後手段,能用 BAdI 就不要用它。
怎麼找出系統裡已經有哪些隱式增強? SE84 → Enhancements → Enhancement Implementations,或直接查表 ENHHEADERENHOBJENHNAME LIKE 'Z%')。接手一套別人做過的系統時,第一件事就是把這張清單印出來。

14. 顯式增強點:ENHANCEMENT-POINT 與 ENHANCEMENT-SECTION

顯式(Explicit)增強點是 SAP 或你自己在原始碼裡明寫出來的掛勾點,比隱式安全得多,因為位置是被官方承諾的。它有兩種:

種類語法行為
ENHANCEMENT-POINT單一敘述,標出一個位置你的程式碼插入在該位置,原始碼照常執行
ENHANCEMENT-SECTION一對 ENHANCEMENT-SECTIONEND-ENHANCEMENT-SECTION 包住一段原始碼你的程式碼取代被包住的那段原始碼
*----------------------------------------------------------------------*
* 在你自己的程式裡「預留」增強點,讓別的顧問/客戶去實作
*----------------------------------------------------------------------*
REPORT zsd_price_check.

"① 先在 SE18 建立一個 Enhancement Spot:ZSPOT_SD_PRICE

"② 靜態增強點:STATIC 表示這裡可以「宣告資料」
ENHANCEMENT-POINT zep_price_data SPOTS zspot_sd_price STATIC.

DATA: lv_price TYPE netwr,
      lv_disc  TYPE p LENGTH 8 DECIMALS 2.

START-OF-SELECTION.

  lv_price = 1000.

"③ 一般增強點:這裡可以插入可執行的敘述
ENHANCEMENT-POINT zep_price_before SPOTS zspot_sd_price.

"④ 增強區段:被包住的預設演算法,可以被整段換掉
ENHANCEMENT-SECTION zes_price_calc SPOTS zspot_sd_price.

  lv_disc = lv_price * '0.05'.        "預設折扣 5%

END-ENHANCEMENT-SECTION.

  WRITE: / |折扣:{ lv_disc NUMBER = USER }|.

實作端在 SE38 開啟這支程式、進 Enhance 模式後,游標放在增強點上右鍵建立實作,產生的程式碼是:

"針對 ENHANCEMENT-SECTION 建立的實作:整段取代原本的預設演算法
ENHANCEMENT 1 ZENH_SD_PRICE_VIP.    "active version

  "台灣區 VIP 客戶折扣改成 12%
  IF sy-uname = 'ERP000'.
    lv_disc = lv_price * '0.12'.
  ELSE.
    lv_disc = lv_price * '0.05'.     "★ 記得補回原本的邏輯,否則會消失!
  ENDIF.

ENDENHANCEMENT.
ENHANCEMENT-SECTION 是「取代」不是「附加」。 一旦你為某個 Section 建立實作,被包住的原始碼就不會執行了。如果你只是想微調,記得把原本的邏輯抄一份進來。這是 Section 最常見的事故:折扣邏輯被整段吃掉,只剩你寫的那一行。
STATIC 的意義:加了 STATIC 的 ENHANCEMENT-POINT 才允許在實作裡宣告資料物件(DATA、TYPES、CONSTANTS)。沒加 STATIC 的只能放可執行敘述。另外對 DDIC 結構做增強時要用 INCLUDE BOUND。設計自己的程式時,習慣上會在「宣告區」放一個 STATIC 增強點、在「處理前/處理後」各放一個一般增強點,這樣別人要擴充就很方便。

15. Class / Function Module Enhancement:pre-exit、post-exit、overwrite-exit

類別的方法函數模組,第四代提供了三種「方法級」的增強,這是 OO 標準程式最乾淨的增強方式。在 SE24 開啟類別 → 按 Enhance → 建立 Enhancement Implementation → 在方法上右鍵選要建立哪一種。

種類執行時機能改什麼典型用途
Pre-Exit原方法執行前可改 IMPORTING/CHANGING 參數清洗輸入值、補預設值
Post-Exit原方法執行後可改 EXPORTING/CHANGING/RETURNING 結果調整回傳結果、追加資料、寫 Log
Overwrite-Exit完全取代原方法整個實作換掉標準演算法完全不適用時;風險最高
*----------------------------------------------------------------------*
* Post-Exit:在標準方法算完之後,調整回傳結果
* 在 SE24 → Enhance → 方法上右鍵 → Post-Exit 之後,系統產生這個方法框架
*----------------------------------------------------------------------*
METHOD zcl_enh_price_calc~get_net_price.

  "① core_object 是「原始物件」的參照,可用來讀取它的公開屬性/方法
  DATA(lv_currency) = core_object->mv_currency.

  "② 此時 rv_price(RETURNING 參數)已經是標準算完的結果,可以再調整
  IF sy-uname = 'ERP000' AND lv_currency = 'TWD'.
    rv_price = rv_price * '0.95'.       "台幣單再打 95 折
  ENDIF.

ENDMETHOD.
*----------------------------------------------------------------------*
* Pre-Exit:在標準方法執行「之前」修正輸入參數
*----------------------------------------------------------------------*
METHOD zcl_enh_material_read~read_material.

  "把使用者輸入的料號補零成 18 位,避免標準方法查不到
  IF iv_matnr IS NOT INITIAL AND strlen( iv_matnr ) < 18.
    iv_matnr = |{ iv_matnr ALPHA = IN }|.
  ENDIF.

ENDMETHOD.
Overwrite-Exit 幾乎等同修改標準。 它會讓 SAP 未來對該方法的所有修正(Support Package、Note)完全失效,而且不會有任何警告。除非是 SAP 支援明確建議,否則不要用。真的用了,一定要在 Enhancement 的文件裡寫下「取代的原始版本是哪一個 SP」。
Pre/Post-Exit 的存取限制:exit 方法裡看不到原方法的區域變數,只看得到參數與經由 core_object 取得的物件屬性(且受可視性限制)。要在方法「中間」插程式,只能用隱式增強點。

16. 三、四代增強全圖解與 MM 常用 BAdI

講義 02-22 問「能圖解整個第三四代增強嗎?」。用表格把兩代的對應關係並排,比畫圖更好查:

面向第三代 Classic BAdI第四代 New BAdI
容器無(定義即最上層)Enhancement Spot → BAdI Definition
介面命名強制 IF_EX_<定義名>自由命名
介面卡CL_EX_<定義名>(自動產生)框架內部處理,看不到介面卡
取得實例CL_EXITHANDLER=>GET_INSTANCEGET BADI
呼叫方法lo_badi->method( )CALL BADI lo_badi->method
過濾器單一 filter,方法多一個 FLT_VAL多個 filter,支援 =/BETWEEN/CP 與 AND/OR
無實作時回傳空參照(需 IS BOUND 判斷)Fallback Class,或丟 CX_BADI_NOT_IMPLEMENTED
執行順序不保證可設定排序(Sortable)
Switch Framework不支援支援
目錄表SXS_ATTR / SXC_EXITENHSPOTHEADER / ENHHEADER / ENHOBJ
建立工具SE18 / SE19(Classic 模式)SE18 / SE19(New 模式)、SE80、ADT(Eclipse)
ABAP Cloud不可用只有 SAP Released 的 BAdI 可用

MM 模組最常用的 BAdI(講義 02-23):

BAdI觸發交易常用方法用途
MB_MIGO_BADIMIGOINIT、PBO_DETAIL、PAI_DETAIL、CHECK_ITEM、CHECK_HEADER、POST_DOCUMENTMIGO 畫面層:加自訂頁籤、逐行檢查、過帳後處理
MB_DOCUMENT_BADIMIGO、MB01、MB1A/B/C、BAPI 過帳MB_DOCUMENT_BEFORE_UPDATE(對話中,可擋)、MB_DOCUMENT_UPDATE(更新工作,不可發 E)物料文件文件層:整份檢查、寫 Z 表
ME_PROCESS_PO_CUSTME21N/ME22N/ME23NPROCESS_HEADER、PROCESS_ITEM、CHECK、POST採購單抬頭/項目的改值與檢查
ME_PROCESS_REQ_CUSTME51N/ME52N同上(請購單版)請購單的改值與檢查
ME_GUI_PO_CUSTME21N 系列SUBSCRIBE、MAP_DYNPRO_FIELDS在採購單畫面加自訂頁籤/欄位(須與 ME_PROCESS_PO_CUST 搭配)
BADI_MATERIAL_CHECKMM01/MM02CHECK_DATA物料主檔存檔前檢查
MB_MIGO_ITEM_BADIMIGOMIGO 行項目層的額外處理
MB_CIN_MM07MFB7MIGO/MB*過帳前對 MSEG 的最後調整
畫面層還是文件層? 一句話判斷:「要跟使用者互動、要看到欄位、要加頁籤」用 MB_MIGO_BADI;「要在資料真的寫進去之前做整份判斷」用 MB_DOCUMENT_BADI。 前者只在 MIGO 有效,後者連 BAPI(BAPI_GOODSMVT_CREATE)與背景過帳都會經過——這一點在做介面(EDI/WMS 回傳)時非常關鍵。

17. S/4HANA、Clean Core 與 ABAP Cloud 三層擴充模型

從 S/4HANA(特別是 Cloud 版與 2022 之後的 On-Premise)開始,SAP 主推 Clean Core(乾淨核心):客戶的擴充不能弄髒標準,升級才能自動化。擴充被分成三層:

名稱誰做工具典型內容
Key User Extensibility(關鍵使用者擴充)顧問/業務端Fiori App:Custom Fields and Logic、Custom Business Objects、Custom CDS Views加自訂欄位、用受限的 ABAP 語言片段寫 BAdI 邏輯、做自訂報表
Developer Extensibility(開發者擴充,In-App)ABAP 開發者ADT(Eclipse),語言版本 ABAP for Cloud Development寫 RAP 應用、CDS View、Behavior Definition、實作 Released BAdI
Side-by-Side Extensibility(並列擴充)開發者SAP BTP(ABAP Environment/CAP/Node.js/Java)獨立跑在 BTP 上,用 OData/SOAP/Events 與 S/4 溝通
在 ABAP Cloud(語言版本 = ABAP for Cloud Development)裡,以下全部禁止
・Classic BAdI(CL_EXITHANDLER
・User Exit、Customer Exit(SMOD/CMOD)
隱式增強點與 Source Code Plug-in
・直接 SELECT 未釋出的標準資料表(例如直接讀 MARA、BSEG)
・大部分傳統 UI(Dynpro、SAP List Viewer 的舊 FM、WRITE 清單)
唯一被允許的增強方式,是實作 SAP 標記為 Released(C1 契約)的 Enhancement Spot/BAdI,並且只能透過 ADT 建立。
怎麼知道一個物件有沒有 Released?
① 在 ADT 開啟該物件,看 Properties → API State,顯示「Released for Cloud Development / Use System-Internally」。
② 在 SAP 的 API Business HubReleased Object 清單查詢。
③ 系統內可查 CDS View I_APIRELEASESTATE 之類的釋出狀態視圖(版本相依)。
On-Premise 的 S/4HANA 仍可用 ABAP 的標準語言版本(Standard ABAP)寫所有舊寫法,但 SAP 建議新開發一律用 ABAP Cloud 語言版本,只有真的做不到時才降級——這叫 Tiered Extensibility Model(三層優先序)
務實建議(On-Premise S/4HANA 專案)
・新做的增強,先在 SE18 查該 BAdI 是不是 Released;是的話盡量用 ADT 做,未來搬雲端不必重寫。
・隱式增強點只在沒有 BAdI 且時程緊迫時使用,並在增強文件裡寫下「這是技術債,未來要換成 BAdI」。
・自訂欄位優先用 Key User 的 Custom Fields(會自動延伸到 CDS/OData/Fiori),而不是 Append Structure + 手改畫面。

18. 升級與傳輸:SPAU、SPAU_ENH、SPDD 與傳輸注意事項

交易碼處理對象什麼時候跑
SPDDDDIC 物件的修改(表、資料元素、Domain)升級中(Phase:ACT_UPG 期間),不做會掉資料
SPAURepository 物件的修改與修復(程式、FM、類別、畫面)升級/SP 之後
SPAU_ENHEnhancement Framework 的增強(增強點實作、BAdI 實作、Class Exit)升級/SP 之後,第四代增強專用
SE95Modification Browser:列出系統中所有修改升級前盤點

可以寫一支小程式,在升級前先盤點自己有多少增強要面對:

REPORT z_enh_inventory.

"盤點系統中所有 Z/Y 開頭的 Enhancement Implementation(第四代)
SELECT h~enhname,          "增強實作名稱
       h~version,          "A=作用中 I=非作用中
       o~pgmid,
       o~obj_type,
       o~obj_name          "被增強的物件
  FROM enhheader AS h
  INNER JOIN enhobj AS o
    ON o~enhname = h~enhname
  WHERE h~enhname LIKE 'Z%'
     OR h~enhname LIKE 'Y%'
  ORDER BY h~enhname
  INTO TABLE @DATA(lt_enh).

"第三代 Classic BAdI 實作
SELECT e~imp_name,         "實作名稱
       e~exit_name,        "BAdI 定義
       c~class_name        "實作類別
  FROM sxc_exit AS e
  INNER JOIN sxc_class AS c
    ON c~imp_name = e~imp_name
  WHERE e~imp_name LIKE 'Z%'
  INTO TABLE @DATA(lt_classic).

cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                        CHANGING  t_table      = lt_enh ).
lo_alv->get_columns( )->set_optimize( abap_true ).
lo_alv->display( ).
傳輸(Transport)的五個坑
BAdI 定義與實作是兩個不同的物件,可能落在不同的傳輸單。只傳實作、忘了傳定義(自建 BAdI 時),目標系統會啟動失敗。
Enhancement Implementation 與它產生的 Class 也是兩個物件;SE19 通常會一起放進同一個請求,但手動改過名稱/Package 之後就不一定。傳輸前用 SE09 展開請求內容逐項確認
啟動狀態不會自動帶過去:在開發機是 Active,到品保機若相依物件缺漏就會變成 Inactive,而且不會報錯,只是不生效。傳完一定要在目標系統用 SE19 確認狀態。
隱式增強相依於標準程式的版本。開發機是 SP10、正式機是 SP08,同一個隱式增強點可能根本不存在,傳輸會失敗或位置跑掉。
Classic BAdI 實作在同一個 BAdI 上,非 Multiple Use 時只能有一個作用中。目標系統若已經有別人(或 SAP Note)的實作,你的傳過去會啟動失敗。
升級後的 SOP:跑完 SPAU_ENH → 進 SE84 列出所有 Z 開頭的 Enhancement Implementation → 逐一確認 Active → 對重點交易(ME21N、MIGO、VA01)做一次冒煙測試。「升級後沒有語法錯誤」不等於「增強還有生效」,這是最容易被漏掉的一步。

練習與自我檢核

  1. (講義 01 節) 建立一個 Classic BAdI 實作,在 ME22N 存檔時,把料號 CHLK1300 的採購單項目自動打上交貨完成旗標。
    提示:BAdI = ME_PROCESS_PO_CUST,方法 PROCESS_ITEM,用 im_item->get_data( ) 取值、改 ELIKZim_item->set_data( ) 寫回。用 MD04 前後對照驗證。別忘了 CHECK sy-uname = 'ERP000'
  2. 為什麼「同一個 interface 底下的所有 method 都要正確,否則會無效」?
    答案要點:實作類別必須實作介面的全部方法才能啟動;任一方法語法錯誤 → 類別無法啟動 → BAdI 實作形同不存在,而且執行時不會報錯,只是靜靜地沒作用。
  3. 你在 SE38 開啟 SAPLMIGO,要找出所有第三代與第四代的增強點,分別搜尋哪些關鍵字?
    答案:第三代搜 CL_EXITHANDLER;第四代搜 GET BADICALL BADI;順便搜 CUSTOMER-FUNCTION(第二代)與 USEREXIT(第一代)。
  4. 在除錯器裡要一次列出「這筆交易到底呼叫了哪些 Classic BAdI」,要怎麼設中斷點?
    答案:Breakpoints → Breakpoint at → Method,類別 CL_EXITHANDLER、方法 GET_INSTANCE,每次停下來看變數 exit_name
  5. 寫一段 ABAP,從 SXS_ATTRSXS_ATTRT 撈出所有說明文字含「Purchase」的 BAdI 定義,用 CL_SALV_TABLE 顯示。
    提示:見第 6 節「找法 B」的程式碼;記得用 @sy-langu 過濾語言,並用 LEFT OUTER JOIN(有些 BAdI 沒有中文說明)。
  6. (講義 02 節) 為什麼講義最後選了 MB_DOCUMENT_BADI 而不是 MB_MIGO_BADI?兩者的分工是什麼?
    答案要點:需求是「整張收貨單累加後判斷是否超交」,屬文件層檢查;而且 MB_DOCUMENT_BADI 連 BAPI 與背景過帳都會經過,MB_MIGO_BADI 只在 MIGO 畫面生效。
  7. MB_DOCUMENT_BEFORE_UPDATEMB_DOCUMENT_UPDATE 有什麼致命差別?
    答案:前者在對話工作程序,可以發 MESSAGE E 擋下使用者;後者在更新工作,發 E 會造成更新中止、資料不一致,只能寫 Log 或用 MESSAGE ... TYPE 'E' INTO
  8. 第 12 節那段超交檢查程式裡,如果把 IF lt_po IS INITIAL. RETURN. ENDIF. 拿掉會發生什麼事?
    答案FOR ALL ENTRIES 遇到空的驅動表,會忽略整個 WHERE 條件,把整張 EKPO/EKBE 撈回來,在正式環境等於系統癱瘓。
  9. 解釋隱式增強點與顯式增強點的差別,並各舉一個 MIGO 情境。
    答案要點:隱式是 SAP 在每支程式的固定位置(程式/FORM/METHOD/FUNCTION 的頭尾)自動提供、看不見、不保證上下文;顯式是 SAP 在原始碼裡明寫 ENHANCEMENT-POINTENHANCEMENT-SECTION、位置被承諾。優先用顯式與 BAdI,隱式是最後手段。
  10. ENHANCEMENT-POINTENHANCEMENT-SECTION 最重要的行為差異是什麼?
    答案:POINT 是插入(原始碼照常執行);SECTION 是取代(被包住的原始碼不再執行),所以要自己把原邏輯抄一份回來。
  11. Pre-Exit、Post-Exit、Overwrite-Exit 各在什麼時候用?哪一個最不該用?
    答案:Pre 改輸入、Post 改輸出、Overwrite 整個換掉。Overwrite 會讓 SAP 對該方法的所有後續修正失效,最不該用。
  12. New BAdI 的 Fallback Class 解決了 Classic BAdI 的什麼痛點?
    答案:Classic BAdI 沒有實作時回傳空參照,呼叫端必須自己寫 IS BOUND 判斷與預設邏輯;Fallback Class 讓框架在無實作時自動執行預設行為,呼叫端程式碼變乾淨。
  13. 為什麼 GET BADI 不能放在迴圈裡?
    答案:GET BADI 要查詢實作清單、計算過濾條件,成本高。正確做法是迴圈外 GET 一次、迴圈內 CALL 多次。
  14. 如果你的 BAdI 實作需要在 OPEN 撈一批資料快取,給後面每次 PROCESS_ITEM 使用,第四代要怎麼設定?第三代只能怎麼做?
    答案:第四代把 Instance Creation Mode 設成 Reusing;第三代沒這個選項,只能用 CLASS-DATA 靜態屬性,但要小心跨交易的髒資料,記得在 OPEN 時先清空。
  15. 升級後要跑哪三個交易碼來處理修改與增強?各自負責什麼?
    答案SPDD(DDIC,升級中就要做)、SPAU(Repository 修改)、SPAU_ENH(Enhancement Framework 增強)。
  16. 在 ABAP Cloud(ABAP for Cloud Development)裡,第 5 節那支 Z_BUSINESSADDIN 測試程式為什麼一行都跑不動?
    答案要點CL_EXITHANDLER 不是 Released API、Classic BAdI 不被允許、WRITE 清單輸出不被允許。正確做法是實作 SAP Released 的 Enhancement Spot,或用 RAP + Fiori。
  17. 綜合題:需求是「MIGO 收貨時,若供應商是黑名單廠商,跳警告但仍可過帳;若料號屬於管制品且數量超過 1000,直接擋下」。請規劃:用哪一代增強、哪個 BAdI、哪個方法、訊息型別怎麼給、黑名單資料放哪裡。
    答案要點:用 MB_DOCUMENT_BADI~MB_DOCUMENT_BEFORE_UPDATE(對話中可擋);黑名單與管制品清單不要寫死在程式裡,建 Z 表 + SM30 維護視圖;警告用 MESSAGE ... TYPE 'W'、擋下用 TYPE 'E';資料一次撈成內表用 line_exists( ) 判斷,不要在迴圈內 SELECT。

第 4-1 章 介面整合:RFC / BAPI 實戰(請購單 PR、物料主檔、商業夥伴 BP)

這一章講的是「SAP 怎麼跟外面的世界說話」。從 RFC 的五種通訊型態(sRFC/aRFC/tRFC/qRFC/bgRFC)、SM59 目的地、如何做出一支能被 Python、Java、.NET 呼叫的 RFC 函數,到 BAPI 的商業物件(BOR)觀念、BAPIRET2 的正確判讀、BAPI_TRANSACTION_COMMIT 的必要性,再到請購單、採購單、物料主檔、商業夥伴 BP 的實戰程式。學完之後你能做到老師說的那句話:「只要懂得 SAP 請購單的 BAPI,就能讓外部系統(EEP/MES/電商)直接在 SAP 開請購單。」

核心 TCODESE37SE80SM59BAPISWO1SWO2SWO3SM58SMQ1SMQ2SBGRFCMONST22SLG1SU53SOAMANAGERSEGW/IWFND/MAINT_SERVICE/AIF/ERRME51NME53NME22NMM02MM03MD04MMBEBPVA01MIGOCS15
關鍵物件BAPIRET2、BAPIRET1、BAPI_TRANSACTION_COMMIT、BAPI_TRANSACTION_ROLLBACK、BAPI_PR_CREATE、BAPI_REQUISITION_CREATE、BAPI_PO_CREATE1、BAPI_PO_CHANGE、BAPI_MATERIAL_SAVEDATA、BAPI_MATERIAL_GET_ALL、BAPI_BUPA_CREATE_FROM_DATA、CMD_EI_API、VMD_EI_API、CL_MD_BP_MAINTAIN、BUT000、BUT020、BUT100、EBAN、EKKO、EKPO、MARA、MARC、KNA1、LFA1、CL_ABAP_CHAR_UTILITIES、RS_RFC_BLACKLIST_CUSTOM

1. SAP 介面技術地圖:先搞清楚該用哪一種

老師開場就把常用的介面技術列出來,實務上選型錯了,後面全都是白工。目前 SAP 的介面技術主要有五類:

技術本質最適合的場景缺點 / 注意
Web Service(SOAP)把函數包成可經由 Web 呼叫的 API,靠 WSDL 定義SAP 對非 SAP、跨防火牆、跨平台;彈性最大要用 SOAMANAGER 建邏輯埠(Logical Port);效能較差、XML 冗長
ALE / IDoc以商業物件(BO)為單位,用 IDoc 格式非同步傳遞SAP 對 SAP、主資料同步、EDI對接收端配置人員要求高;IDoc 結構=1 控制記錄+n 資料記錄+n 狀態記錄
RFC遠端函數呼叫,是 BAPI/ALE 的底層系統間即時呼叫函數;效能最好SM59 目的地;跨防火牆需 SAP Cloud Connector 或 SAProuter
BAPI具 RFC 屬性、且註冊在 BOR 的標準商業介面建立/修改/查詢單據與主資料,有版本相容保證參數結構固定,客製欄位要靠 Extension
Middleware(仲介軟體)CRM 特有,靠 Adapter 傳 BDocCRM 與 ERP 之間BDoc 的本質是 qRFC;流程是 CRM → BDoc → IDoc/XML/BAPI 結構 → 外部系統

IDoc 的出站與入站四步驟(面試常考):

  • 出站:① 應用層產生應用資料 → ② ALE 層建立出站 IDoc → ③ 通訊層尋找夥伴與埠 → ④ 發送 IDoc。
  • 入站:① 通訊層接收入站埠資料 → ② ALE 層建立入站 IDoc、依夥伴參數檔找 IDoc 處理類型 → ③ 應用層寫入文件。
SAP 與 SAP 之間的 SM59 邏輯連接是 R/3 型(型別 3);SAP 對外部系統(例如產生檔案給 FTP、或註冊型外部程式)用的是 T 型(TCP/IP)。老師提到「SAP 發送到 FTP」的做法:建立 RFC 邏輯連接 → 用 FTP_CONNECT / FTP_COMMAND 系列函數連線登入 → 在指定位置建立檔案(詳見 4-4 章 FTP 段落)。

2. BOPF 與 BAPI 的層次關係

BOPF(Business Object Processing Framework)是一個基於物件導向 ABAP 的框架,是 SAP Business Suite 用來開發商業運用物件的基礎設施。用 BOPF 開發可以省下大量時間,因為權限控制、交易處理(transaction handling)、緩存管理這些技術細節框架都幫你做掉了,你只要專注在業務邏輯。

在 BOPF 的架構中,BAPI 是第三層——介面層(Interface Layer)。老師的重點:

  • Dynpro 是傳統 SAP 介面;SAPUI5 是 HTML5 的介面;兩者底下呼叫的商業邏輯可以是同一組。
  • SAP ERP 提供無數 BAPI 函數:單據新增、變更、查詢、庫存查詢、MRP 展算⋯⋯「你想得到的、想不到的都有」。
  • BAPI 若具有 RFC 屬性,就表示能讓外部程式執行。這是整章的核心句。
老師的白話口訣:「B-API,就是 Business 的 API。」 學會請購單的 BAPI,EEP/MES/電商網站就能直接在 SAP 開請購單,不用人工重打。

3. RFC 的五種型態:sRFC / aRFC / tRFC / qRFC / bgRFC

RFC 是 SAP 系統之間、以及 SAP 與外部系統之間程式通訊的基本介面技術,BAPI、ALE 等更高層技術都是基於 RFC 實現。依通訊方向分三種情境:

  • 兩個獨立的 SAP 系統之間通訊。
  • SAP 作為呼叫端,去呼叫外部遠端系統(非 SAP ABAP 系統)。
  • 外部系統作為呼叫端,來呼叫 SAP。

依同步性與可靠度,共有五種型態。選錯型態是介面專案最貴的錯誤

型態全名特性監控 TCODE典型用途
sRFCSynchronous RFC同步,呼叫端等回應;目標系統必須在線,否則直接短拋(DUMP)ST22即時查詢:查庫存、查價格、查客戶信用
aRFCAsynchronous RFC非同步,不等回應(STARTING NEW TASK);可用 RECEIVE RESULTS 回收結果;目標仍須在線SM50平行處理、把大批資料切成多工同時跑
tRFCTransactional RFCIN BACKGROUND TASK;先寫進 ARFCSSTATE / ARFCSDATA,COMMIT WORK 後才送出;保證只執行一次,但不保證順序SM58ALE/IDoc 傳輸、目標系統可能離線的情境
qRFCQueued RFCtRFC + 佇列,保證 只執行一次且照順序;分出站佇列(Outbound)與入站佇列(Inbound)SMQ1 / SMQ2 / SMQRCRM Middleware 的 BDoc、必須保序的主資料變更
bgRFCBackground RFCNetWeaver 7.0 之後推出,取代 tRFC/qRFC;分 Type T(無序單元)與 Type Q(有序單元);用類別 API 而非語法附加SBGRFCMON / SBGRFCCONFS/4HANA 新專案的建議做法
*&-------------------------------------------------------------*
*& 五種 RFC 呼叫語法對照
*&-------------------------------------------------------------*

" ① sRFC:同步。目標不在線 → 立刻例外/DUMP,必須 CATCH
TRY.
    CALL FUNCTION 'Z_MM_STOCK_GET'
      DESTINATION 'PRD_CLNT100'          "SM59 建立的 RFC 目的地
      EXPORTING  iv_werks = '1710'
      TABLES     et_stock = lt_stock
      EXCEPTIONS system_failure        = 1 MESSAGE DATA(lv_msg)
                 communication_failure = 2 MESSAGE lv_msg
                 OTHERS                = 3.
    IF sy-subrc <> 0.
      MESSAGE |RFC 失敗({ sy-subrc }):{ lv_msg }| TYPE 'E'.
    ENDIF.
  CATCH cx_root INTO DATA(lo_ex).
    MESSAGE lo_ex->get_text( ) TYPE 'E'.
ENDTRY.

" ② aRFC:非同步、平行處理。用 CALLING ... ON END OF TASK 回收
CALL FUNCTION 'Z_MM_STOCK_GET'
  STARTING NEW TASK  |TASK{ lv_idx }|          "任務名稱必須唯一
  DESTINATION IN GROUP DEFAULT                  "用 RZ12 的 Server Group
  CALLING lo_handler->on_task_end ON END OF TASK
  EXPORTING iv_werks = lv_werks
  EXCEPTIONS resource_failure = 1 OTHERS = 2.

" ③ tRFC:保證只執行一次,但不保證順序。一定要 COMMIT WORK 才真的送出
CALL FUNCTION 'Z_IF_SEND_DOC'
  IN BACKGROUND TASK
  DESTINATION 'MES_SYSTEM'
  EXPORTING is_doc = ls_doc.
COMMIT WORK.                                    "沒有這行,資料只躺在 ARFCSSTATE

" ④ qRFC:tRFC + 佇列,保證順序。先指定佇列名再呼叫
CALL FUNCTION 'TRFC_SET_QUEUE_NAME'
  EXPORTING qname = |ZPR{ lv_banfn }|           "同一佇列名者依序執行
  EXCEPTIONS OTHERS = 1.
CALL FUNCTION 'Z_IF_SEND_DOC'
  IN BACKGROUND TASK
  DESTINATION 'MES_SYSTEM'
  EXPORTING is_doc = ls_doc.
COMMIT WORK.

" ⑤ bgRFC(S/4HANA 建議做法):Type Q=有序單元
TRY.
    DATA(lo_dest) = cl_bgrfc_destination_outbound=>create( 'MES_SYSTEM' ).
    DATA(lo_unit) = lo_dest->create_qrfc_unit( ).
    lo_unit->add_queue_name_outbound( |ZPR{ lv_banfn }| ).
    CALL FUNCTION 'Z_IF_SEND_DOC'
      IN BACKGROUND UNIT lo_unit
      EXPORTING is_doc = ls_doc.
    COMMIT WORK.
  CATCH cx_bgrfc_invalid_destination
        cx_bgrfc_invalid_unit INTO DATA(lo_bg_ex).
    MESSAGE lo_bg_ex->get_text( ) TYPE 'E'.
ENDTRY.
tRFC/qRFC/bgRFC 少了 COMMIT WORK 就什麼都不會發生。資料只會停在 ARFCSSTATE(tRFC)或 bgRFC 單元表裡,SM58 看得到一筆「Transaction recorded」永遠不執行。這是介面上線第一天最常見的災難。
除錯順序背起來:同步失敗看 ST22;tRFC 卡住看 SM58(可手動 Execute LUW 重送);qRFC 卡住看 SMQ1(出站)/ SMQ2(入站),常見狀態 SYSFAIL、CPICERR、RETRY;bgRFC 看 SBGRFCMON

4. SM59:RFC 目的地與連線測試

RFC 目的地(Destination)是「連到哪裡、用什麼身分」的定義。SM59 裡最常用的型別:

型別說明常見用途
3(ABAP Connection)連到另一套 SAP ABAP 系統DEV → QAS → PRD、ALE/IDoc、跨系統 BAPI
T(TCP/IP Connection)連到外部程式;分 Registered Server Program 與 Start on Explicit HostJava/.NET/Python 註冊型伺服器、SAP Gateway 外掛
G(HTTP to External Server)對外部 HTTP/REST 端點ABAP 呼叫外部 REST API(配 CL_HTTP_CLIENT)
H(HTTP to ABAP System)對另一套 ABAP 系統的 HTTPOData/SOAP 內部呼叫
L(Logical Destination)指向另一個既有目的地做環境切換的間接層
NONE / 空白不指定=呼叫本機系統本身測試時用 DESTINATION 'NONE' 走本機 RFC 路徑

建立目的地的步驟((依講義步驟整理)):SM59 → 選型別(例如 3)→ 按 Create → 填 RFC Destination 名稱(建議 <SID>CLNT<MANDT> 命名)→ Technical Settings 填 Target Host、System Number → Logon & Security 填 Language、Client、User、Password(務必用 Type = System 或 Communication 的技術帳號,不要用個人帳號)→ 存檔 → 按 Connection TestRemote Logon 驗證。

*&-------------------------------------------------------------*
*& 目的地健檢:先 PING 再呼叫,避免整支程式被同步 RFC 拖死
*&-------------------------------------------------------------*
DATA(lv_dest) = CONV rfcdest( 'PRD_CLNT100' ).

" 確認 SM59 目的地確實存在(RFCDES 是目的地主檔)
SELECT SINGLE rfcdest FROM rfcdes
  WHERE rfcdest = @lv_dest
  INTO  @DATA(lv_exist).
IF sy-subrc <> 0.
  MESSAGE |RFC 目的地 { lv_dest } 不存在,請先在 SM59 建立| TYPE 'E'.
ENDIF.

" RFC_PING:最輕量的連線測試,逾時就不要往下打
CALL FUNCTION 'RFC_PING'
  DESTINATION lv_dest
  EXCEPTIONS system_failure        = 1 MESSAGE DATA(lv_err)
             communication_failure = 2 MESSAGE lv_err
             OTHERS                = 3.
IF sy-subrc <> 0.
  MESSAGE |目的地 { lv_dest } 無法連線:{ lv_err }| TYPE 'E'.
ENDIF.

" 取回遠端系統資訊,寫進介面 Log,日後對帳非常好用
CALL FUNCTION 'RFC_SYSTEM_INFO'
  DESTINATION lv_dest
  IMPORTING  rfcsi_export = DATA(ls_info)
  EXCEPTIONS OTHERS       = 1.
IF sy-subrc = 0.
  WRITE: / |遠端系統:{ ls_info-rfcsysid } / Host:{ ls_info-rfchost } | &&
           |/ Release:{ ls_info-rfcsaprl } / 時區:{ ls_info-rfctzone }|.
ENDIF.
SM59 目的地內嵌的帳號密碼等於一把鑰匙。絕不可以在生產機建立指向生產機、又存有 DDIC/SAP* 密碼的目的地;也不要讓開發機的目的地存生產機密碼(這是 SAP 稽核必查項)。技術帳號請設為 Type = System 並限制到最小權限的角色(S_RFC 只開必要的函數群組)。

5. 建立 RFC-Enabled Function Module:SE37 的規矩

任何 ABAP 程式都能用 CALL FUNCTION ... DESTINATION 呼叫遠端功能;反過來,要讓外部程式(Python/Java/.NET)呼叫你,就得做一支具 RFC 屬性的函數模組。步驟:

  • SE37(或 SE80 的函數群組)建立函數 → Attributes 頁籤 → Processing Type 選 Remote-Enabled Module
  • 勾選後,SAP 會對介面套用一組強制限制,違反的話啟動(Activate)就報錯。
限制說明做法
所有參數必須 pass by valueImport/Export/Changing 全部要勾 Pass Value;不能用 referenceSE37 介面畫面把 值傳遞 欄位打勾
型別必須是 DDIC 型別不能用程式內部型別、不能用 TYPE ANY/泛型;結構與表都要是 SE11 建立的先在 SE11 建 Structure 與 Table Type
不支援 CHANGING 的參考語意Changing 參數雖可用,但外部連接器支援度不一介面設計建議只用 IMPORTING / EXPORTING / TABLES
例外只能用傳統例外不能用 class-based exception 當介面例外(外部連接器看不懂)內部用 TRY/CATCH,介面用回傳 BAPIRET2 表告知結果
TABLES 參數雖已過時但仍可用對外部連接器相容性最好回傳多筆資料時用 TABLES 或 EXPORTING 一個 Table Type
不可有 Dynpro 互動WRITE、CALL SCREEN、MESSAGE 都會讓遠端呼叫掛掉訊息一律塞進 RETURN 表
字串型別的陷阱:RFC 介面可以用 STRING / XSTRING,但部分舊版連接器(尤其 NCo 舊版、C RFC SDK 傳統 API)處理深層型別(deep type)會有問題。對外介面最安全的是扁平字元型別(CHAR、NUMC、DEC)+ DDIC 結構。另外,DECIMAL 欄位的小數位由 DDIC Domain 決定,外部系統若用 float 接會有精度誤差,金額欄位請雙方約定用字串傳遞。

6. 實作:給 Python / pyrfc 呼叫的 Z RFC 函數

這是實務上最常被要求的東西:外部系統要撈 SAP 的庫存或主資料。老師的專案要點也寫得很清楚——「基礎資料能自動下載到 MSSQL,不必每個都動態」。下面是一支完整、可直接照抄的 RFC 函數樣板。

先在 SE11 建好 DDIC 物件(依講義步驟整理)):

  • Structure ZS_STOCK_S:MATNR、MAKTX、WERKS、LGORT、CHARG、LABST、UMLME、INSME、SPEME、MEINS、STPRS、PEINH、WAERS。
  • Structure ZS_RANGE_S:SIGN(CHAR1)、OPTION(CHAR2)、LOW(CHAR40)、HIGH(CHAR40)——用來把 SELECT-OPTIONS 從外部傳進來。
  • Table Type ZTT_STOCK(行型別 ZS_STOCK_S)、ZTT_RANGE(行型別 ZS_RANGE_S)。
FUNCTION z_mm_stock_get.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  IMPORTING
*"     VALUE(IV_WERKS) TYPE  WERKS_D OPTIONAL
*"     VALUE(IV_MAXROW) TYPE  I DEFAULT 10000
*"  EXPORTING
*"     VALUE(EV_COUNT) TYPE  I
*"  TABLES
*"      IT_MATNR STRUCTURE  ZS_RANGE_S OPTIONAL    "物料 select-option
*"      IT_LGORT STRUCTURE  ZS_RANGE_S OPTIONAL    "儲存地點 select-option
*"      ET_STOCK STRUCTURE  ZS_STOCK_S
*"      ET_RETURN STRUCTURE  BAPIRET2
*"----------------------------------------------------------------------
*"  注意:Remote-Enabled 時每個參數都必須勾 Pass Value,型別必須是 DDIC 型別
*"----------------------------------------------------------------------

  CLEAR: et_stock[], et_return[], ev_count.

  " 外部傳進來的 range 內表,型別要轉成 ABAP 的 RANGE 型別才能放進 WHERE
  DATA lr_matnr TYPE RANGE OF matnr.
  DATA lr_lgort TYPE RANGE OF lgort_d.

  lr_matnr = CORRESPONDING #( it_matnr[] ).
  lr_lgort = CORRESPONDING #( it_lgort[] ).

  " 參數檢核:介面一定要自己擋,外部系統什麼都敢送
  IF iv_werks IS INITIAL AND lr_matnr IS INITIAL.
    APPEND VALUE bapiret2( type       = 'E'
                           id         = 'ZIF'
                           number     = '001'
                           message    = '工廠與物料至少要給一個條件'
                           message_v1 = 'Z_MM_STOCK_GET' ) TO et_return.
    RETURN.
  ENDIF.

  TRY.
      SELECT FROM mard AS d
        INNER JOIN mara AS a ON a~matnr = d~matnr
        LEFT OUTER JOIN makt AS t ON  t~matnr = d~matnr
                                  AND t~spras = @sy-langu
        LEFT OUTER JOIN mbew AS b ON  b~matnr = d~matnr
                                  AND b~bwkey = d~werks
        FIELDS d~matnr, t~maktx, d~werks, d~lgort,
               d~labst, d~umlme, d~insme, d~speme,
               a~meins, b~stprs, b~peinh
        WHERE d~werks = @iv_werks              "空值時此條件由下面的 range 補
          AND d~matnr IN @lr_matnr
          AND d~lgort IN @lr_lgort
          AND ( d~labst <> 0 OR d~umlme <> 0
             OR d~insme <> 0 OR d~speme <> 0 )
        ORDER BY d~matnr, d~werks, d~lgort      "外部系統要穩定順序,一定要 ORDER BY
        INTO CORRESPONDING FIELDS OF TABLE @et_stock
        UP TO @iv_maxrow ROWS.

    CATCH cx_sy_open_sql_error INTO DATA(lo_sql_ex).
      APPEND VALUE bapiret2( type    = 'E' id = 'ZIF' number = '002'
                             message = lo_sql_ex->get_text( ) ) TO et_return.
      RETURN.
  ENDTRY.

  " 幣別補值(公司代碼幣別);外部系統看到金額一定會問幣別
  SELECT SINGLE waers FROM t001k AS k
    INNER JOIN t001 AS c ON c~bukrs = k~bukrs
    FIELDS c~waers
    WHERE k~bwkey = @iv_werks
    INTO  @DATA(lv_waers).

  LOOP AT et_stock ASSIGNING FIELD-SYMBOL(<ls_stock>).
    <ls_stock>-waers = lv_waers.
  ENDLOOP.

  ev_count = lines( et_stock ).

  APPEND VALUE bapiret2(
    type       = COND #( WHEN ev_count = 0 THEN 'W' ELSE 'S' )
    id         = 'ZIF'
    number     = '000'
    message    = |查詢完成,共 { ev_count } 筆|
    message_v1 = |{ iv_werks }| ) TO et_return.

ENDFUNCTION.

Python 端用 pyrfc(PyRFC 套件,底層是 SAP NW RFC SDK)呼叫,把結果寫進 MSSQL:

# -*- coding: utf-8 -*-
# 需求:pip install pyrfc(並先安裝 SAP NW RFC SDK 並設定 PATH / LD_LIBRARY_PATH)
from pyrfc import Connection, ABAPApplicationError, CommunicationError

CONN = dict(
    user='RFC_USER', passwd='********',
    ashost='10.1.1.10', sysnr='00', client='100', lang='ZH',
    # 若走 SAP Cloud Connector / SAProuter:saprouter='/H/router/S/3299/H/host'
)

def get_stock(werks, matnr_list=None):
    # select-option 的四欄結構:SIGN / OPTION / LOW / HIGH
    it_matnr = [{'SIGN': 'I', 'OPTION': 'EQ', 'LOW': m, 'HIGH': ''}
                for m in (matnr_list or [])]
    with Connection(**CONN) as conn:
        result = conn.call('Z_MM_STOCK_GET',
                           IV_WERKS=werks,
                           IV_MAXROW=50000,
                           IT_MATNR=it_matnr)
    # RETURN 一定要判:type 為 E / A 就是失敗
    for msg in result['ET_RETURN']:
        if msg['TYPE'] in ('E', 'A'):
            raise RuntimeError(f"SAP 回傳錯誤:{msg['MESSAGE']}")
    return result['ET_STOCK']

if __name__ == '__main__':
    try:
        rows = get_stock('1710', ['TG11', 'TG12'])
        print(f'取得 {len(rows)} 筆庫存')
        # 之後用 pyodbc / SQLAlchemy 批次寫入 MSSQL
    except (ABAPApplicationError, CommunicationError) as e:
        print('RFC 失敗:', e)
老師強調過的實務眉角:條件用「區間(BT)」與用「列舉(EQ)」答案會不一樣,用列舉的才正確。介面設計時把 SELECT-OPTIONS 開成 range 表傳進來,外部系統就能用 SIGN='I' OPTION='EQ' 一筆一筆列舉,避開區間在非連號編碼(物料號、成本中心)上抓到不該抓的資料。
SELECT SINGLE 沒有排序保證,多筆符合時 HANA 回傳哪一筆是不確定的。介面函數尤其危險——同一支程式今天抓到 A、明天抓到 B。要嘛加足主鍵條件,要嘛改成 SELECT ... ORDER BY ... UP TO 1 ROWS

7. 外部呼叫端:JCo、NCo、pyrfc、Cloud Connector

SAP 為多種語言與作業系統提供 RFC 函式庫(RFC Library,內含 RFC API)與各式連接器:

連接器語言/平台備註
SAP JCo(Java Connector)Java最成熟,SAP PI/PO、多數中介系統的底層;JCoDestinationManager 取得目的地、JCoFunction 執行
SAP NCo(.NET Connector)C# / VB.NETWindows 端 WinForm/ASP.NET 常用;需注意版本與 x86/x64 一致
RFC SDKC / C++所有連接器的底層;新版為 NW RFC SDK(舊 Classic RFC SDK 已停止支援)
PyRFCPython依賴 NW RFC SDK;適合資料工程、排程抓數、FastAPI 包裝成 REST
Lotus ConnectorLotus Notes講義有列,現在幾乎絕跡
SAP Cloud Connector連線元件S/4HANA 私有雲/地端要被 BTP 上的雲端 App 呼叫時的反向 Proxy;在 SCC 設定 Virtual Host → Internal Host 的映射,並逐一白名單允許的 Function Module 名稱

兩種連線方向要分清楚:

  • 外部 → SAP(Client 模式):外部程式帶 ashost/sysnr/client 直接連 SAP Gateway,呼叫 RFC-enabled FM。這是 pyrfc 範例的模式。
  • SAP → 外部(Server / Registered 模式):外部程式以 Program ID 向 SAP Gateway 註冊;SM59 建 T 型目的地、指定同一個 Program ID 與 Gateway Host/Service,ABAP 就能 CALL FUNCTION ... DESTINATION 呼叫外面的程式。註冊狀態可在 SMGW 的 Logged on Clients 看到。

RFC 黑名單(老師特別標紅的坑)

SAP 為了防止外部任意呼叫危險函數,導入了 RFC 黑名單(Blacklist)機制。報表 RS_RFC_BLACKLIST_CUSTOM 用來維護客製白/黑名單。老師寫得很直接:「要將欲執行的外部讀取的 Function Module 從黑名單中剔除才行。」(2022.12.15)

  • 相關報表:RS_RFC_BLACKLIST_CUSTOM(客製名單)、RS_RFC_BLACKLIST_SAP(SAP 預設名單)。
  • 相關 Profile 參數:rfc/callback_security_methodrfc/reject_expired_passwd;UCON(Unified Connectivity)也可能把函數擋在外面,要在 UCONCOCKPIT 把函數放進 Communication Assembly。
  • 症狀:外部呼叫回 Function module ... is blacklistednot released for remote calls,但在 SE37 本機測試完全正常。
老師記錄的 iCoder & SAP 專案測試要點(介面專案的驗收清單):PP 能抓出缺料表(基礎資料能自動下載到 MSSQL,不必每個都動態);MM 能做請購簽核;SD 能用 pivot 做業績分析;FICO 財務類二次開發需求不高,可不做。

8. BAPI 的本質:BO、BOR 與怎麼把 BAPI 找出來

BAPI(Business Application Programming Interface)是 SAP 企業物件的特定方法,儲存在企業物件儲藏庫(BOR)中,用來執行特定企業工作細項。在 SAP 系統中,BAPI 以 RFC 功能模組的形式存在於函數建立器裡。BAPI 具有標準企業介面,允許外部應用程式(透過 SAP 企業物件的協助)存取 SAP 程式、函式和資料。用戶端可以是同一套 SAP、外部系統(例如 .NET 應用程式)、HTTP 閘道或其他 SAP 系統;連接技術可以是 JS、Java、VB、ABAP。

先懂兩個名詞:

  • BO(Business Object,業務物件):代表真實世界的物件,例如一張採購訂單、一筆物料資料。BO 包含業務資料,並完整描述業務過程。系統內部應用與系統之間就是在 BO 級別通訊。透過呼叫 BAPI,內外部應用可以用 Portal、Java、.NET、Web Service 等標準方式存取和操作業務物件。
  • BOR(Business Object Repository):SAP 業務物件類型(BO 的定義與描述)和相關 BAPI 的組織中心。BO 依所屬應用層級組織以便查找;BOR 管理不同發佈版本,並保證介面的版本相容性(這是 BAPI 相對於自寫 RFC 最大的價值)。
TCODE用途
BAPIBAPI Explorer:依應用元件階層瀏覽所有已釋出(Released)的 BAPI,可看文件、參數、範例
SWO1Business Object Builder:顯示/維護業務物件類型,看它的 Key、Method、Attribute、Event
SWO2BOR Browser:以樹狀瀏覽業務物件
SWO3查看 BOR(Business Object Repository)
SE37直接看 BAPI 的函數介面與原始碼、做單獨測試
BAPI → Alphabetical用字母排序找,配合關鍵字(CREATE、CHANGE、GETDETAIL、GETLIST)猜名字

BO 的結構:SAP R/3 企業目標的目標類型(Object Type)相當於物件導向語言中「類別(Class)」的概念,其定義由六部分組成:基本資料、介面(Interfaces)、鍵(Key Fields)、方法(Methods)、特徵(Attributes)、事件(Events)

老師的絕招:由 TCODE 反查 BAPI(10-2)

如果只知道交易代碼,可以這樣把 BAPI 挖出來。以「建立銷售訂單」VA01 為例:

  1. 進入 VA01 畫面 → 功能表 System → Status
  2. 在交易代碼欄位上雙擊,進入交易定義,找到它的 Package(開發類別)= VA
  3. SE80 打開 Package VA
  4. 展開 Business Engineering → Business Object Types(業務工程 → 業務物件類型)。依業務需求判斷,要建立銷售訂單,BUS2032(Sales Order) 可能性最大。
  5. 雙擊 BUS2032 打開 Methods,尋找和「建立銷售訂單」同名的方法——這裡就是 SalesOrder.CreateFromDat2。把游標停在該行,點上方的「程式」按鈕,就能看到原始碼,找到 BAPI_SALESORDER_CREATEFROMDAT2。同樣方法可以找到修改(CHANGE)、刪除的 BAPI。
  6. 也可以雙擊 SalesOrder.CreateFromDat2 這一行,在彈出視窗中找到 ABAP 子畫面:如果單選鈕是「API 功能」,名稱欄就是我們要找的 BAPI;如果是「函數模組」,那就是一支一般 FM。
另外三招也很好用:① SE84 → Enterprise Services / Business Objects 搜尋;② 在標準交易上開 ST05 SQL TraceSAT,看它到底呼叫哪支 FM;③ 直接在 SE37 用萬用字元搜 BAPI_*CREATE*BAPI_*GETDETAIL*

9. BAPIRET2 與 COMMIT/ROLLBACK:BAPI 的兩條命

幾乎所有 BAPI 都有一個 RETURN 參數,型別 BAPIRET2(舊版是 BAPIRET1 / BAPIRETURN)。這是判斷成功失敗的唯一依據——sy-subrc 在 BAPI 呼叫後幾乎永遠是 0,看它是沒有意義的。

欄位意義
TYPE訊息類型:S=成功、I=資訊、W=警告、E=錯誤、A=中止。E 與 A 才算失敗
ID / NUMBER訊息類別(SE91)與訊息編號,組起來可用 MESSAGE ... INTO 重建文字
MESSAGE已組好的訊息文字(含變數替換)
MESSAGE_V1~V4訊息變數,訊息文字裡的 &1~&4
ROW / FIELD / PARAMETER出錯的內表列號、欄位、參數名稱——回饋給外部系統時一定要帶上
LOG_NO / LOG_MSG_NO應用 Log 編號,可到 SLG1 查完整記錄
BAPI 內部不會自己 COMMIT。SAP 標準 BAPI 只把資料放進 update task(更新任務)緩衝區,你必須自己呼叫 BAPI_TRANSACTION_COMMIT,資料才會真的寫進資料庫。失敗時則要呼叫 BAPI_TRANSACTION_ROLLBACK 清掉緩衝區與鎖,否則同一個 LUW 裡的下一次呼叫會被前一次的髒資料汙染。

WAIT = 'X' 的意義:讓 COMMIT 等到更新任務真的做完才返回(同步更新)。如果你 COMMIT 完馬上要用剛產生的單號去做下一步(例如建完 PR 馬上轉 PO、或馬上 SELECT 出來),沒有 WAIT = 'X' 就會抓不到資料。代價是速度較慢,批次大量建單時可考慮省略,但要自行確保後續不立即讀取。

把「判 RETURN + COMMIT/ROLLBACK」寫成一支共用的工具類別,是專案上最值得做的投資:

*&-------------------------------------------------------------*
*& BAPI 回傳處理共用類別(放在自建的 ZCL_BAPI_UTIL)
*&-------------------------------------------------------------*
CLASS zcl_bapi_util DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    " 判斷 RETURN 表裡有沒有 E / A
    CLASS-METHODS has_error
      IMPORTING it_return       TYPE bapiret2_t
      RETURNING VALUE(rv_error) TYPE abap_bool.

    " 把 RETURN 表串成一段可讀的訊息(給 MESSAGE 或介面回傳用)
    CLASS-METHODS to_text
      IMPORTING it_return      TYPE bapiret2_t
                iv_only_error  TYPE abap_bool DEFAULT abap_true
      RETURNING VALUE(rv_text) TYPE string.

    " 依 RETURN 結果自動決定 COMMIT 或 ROLLBACK
    CLASS-METHODS commit_or_rollback
      IMPORTING it_return    TYPE bapiret2_t
                iv_wait      TYPE abap_bool DEFAULT abap_true
      RETURNING VALUE(rv_ok) TYPE abap_bool.
ENDCLASS.

CLASS zcl_bapi_util IMPLEMENTATION.

  METHOD has_error.
    " line_exists 比 LOOP ... EXIT 乾淨,也不會動到 sy-subrc 的語意
    rv_error = xsdbool( line_exists( it_return[ type = 'E' ] ) OR
                        line_exists( it_return[ type = 'A' ] ) ).
  ENDMETHOD.

  METHOD to_text.
    rv_text = REDUCE string(
      INIT lv_txt = ``
      FOR <ls_r> IN it_return
        WHERE ( type = 'E' OR type = 'A' OR type = 'W' OR type = 'S' )
      NEXT lv_txt = COND #(
        WHEN iv_only_error = abap_true AND <ls_r>-type NA 'EA'
          THEN lv_txt
        ELSE |{ lv_txt }{ COND #( WHEN lv_txt IS INITIAL THEN `` ELSE |; | ) }| &&
             |[{ <ls_r>-type }]{ <ls_r>-id }{ <ls_r>-number } { <ls_r>-message }| ) ).
  ENDMETHOD.

  METHOD commit_or_rollback.
    IF has_error( it_return ) = abap_true.
      CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.   "清掉緩衝區與鎖
      rv_ok = abap_false.
    ELSE.
      CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
        EXPORTING wait = COND char1( WHEN iv_wait = abap_true
                                     THEN 'X' ELSE space ).
      rv_ok = abap_true.
    ENDIF.
  ENDMETHOD.

ENDCLASS.
*&-------------------------------------------------------------*
*& 標準呼叫樣板:任何 BAPI 都照這個骨架寫
*&-------------------------------------------------------------*
DATA lt_return TYPE bapiret2_t.

CALL FUNCTION 'BAPI_XXXX_CREATE'
  EXPORTING  ...
  IMPORTING  ...
  TABLES     return = lt_return.

IF zcl_bapi_util=>commit_or_rollback( it_return = lt_return
                                      iv_wait   = abap_true ) = abap_true.
  MESSAGE |建立成功:{ lv_docno }| TYPE 'S'.
ELSE.
  MESSAGE zcl_bapi_util=>to_text( lt_return ) TYPE 'S' DISPLAY LIKE 'E'.
ENDIF.

" 訊息要給使用者看清楚時,用 CL_SALV_TABLE 把整張 RETURN 表列出來
TRY.
    cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_salv)
                            CHANGING  t_table      = lt_return ).
    lo_salv->get_columns( )->set_optimize( abap_true ).
    lo_salv->get_functions( )->set_all( abap_true ).
    lo_salv->get_display_settings( )->set_list_header( 'BAPI 回傳訊息' ).
    lo_salv->display( ).
  CATCH cx_salv_msg INTO DATA(lo_salv_ex).
    MESSAGE lo_salv_ex->get_text( ) TYPE 'E'.
ENDTRY.
兩個口訣:「BAPI 只看 RETURN,不看 SY-SUBRC」「有建就要 COMMIT,有錯就要 ROLLBACK」。另外,同一支程式在一個 LUW 裡連續呼叫多支 BAPI(例如建 PR 再建 PO),中間不要各自 COMMIT,應該全部成功才一次 COMMIT,才有交易一致性。

10. 物料主檔 BAPI:GET_ALL 與 SAVEDATA

講義第 02 節的兩個範例:BAPI_MATERIAL_GET_ALL 讀出物料現況、BAPI_MATERIAL_SAVEDATA 寫回。老師的兩個案例是「修改物料主檔的淨重與單位」和「更改 MRP2 的前置天數(PLIFZ)」。

SAVEDATA 的核心觀念——X 結構(Checkbox Structure):每個資料結構(CLIENTDATA、PLANTDATA、SALESDATA⋯)都有一個對應的 X 結構(CLIENTDATAX、PLANTDATAX⋯)。你要改哪個欄位,就在 X 結構的同名欄位填 'X';沒填 'X' 的欄位即使有值也不會被寫入,反之填了 'X' 但值為空,就會把該欄位清空。這是所有 SAP 主資料 BAPI 的共通設計。

*&---------------------------------------------------------------------*
*& Report ZBAPI_MATERIAL_CHANGE
*& 讀取物料 → 修改淨重/重量單位(基本視圖)+ MRP2 前置時間(工廠視圖)
*&---------------------------------------------------------------------*
REPORT zbapi_material_change.

PARAMETERS: p_matnr TYPE mara-matnr OBLIGATORY,
            p_werks TYPE marc-werks OBLIGATORY DEFAULT '1710',
            p_ntgew TYPE mara-ntgew,          "淨重
            p_gewei TYPE mara-gewei,          "重量單位
            p_plifz TYPE marc-plifz.          "MRP2 計畫交貨時間(前置天數)

START-OF-SELECTION.

  DATA lt_return TYPE STANDARD TABLE OF bapi_matreturn2.

  " ① 先讀出現況(GET_ALL 只讀不寫,可先確認物料存在與現值)
  CALL FUNCTION 'BAPI_MATERIAL_GET_ALL'
    EXPORTING material   = CONV bapimatall-material( p_matnr )
              plant      = CONV bapimatall-plant( p_werks )
              val_area   = CONV bapimatall-val_area( p_werks )
    IMPORTING clientdata = DATA(ls_client_old)
    TABLES    return     = lt_return.

  IF line_exists( lt_return[ type = 'E' ] ).
    MESSAGE |物料 { p_matnr } 讀取失敗:{ lt_return[ type = 'E' ]-message }|
            TYPE 'E'.
  ENDIF.

  WRITE: / |原淨重:{ ls_client_old-net_weight } { ls_client_old-unit_of_wt }|.

  " ② 抬頭:指定物料+要維護哪些視圖(沒開視圖旗標,資料是寫不進去的)
  DATA(ls_head) = VALUE bapimathead(
    material    = p_matnr
    basic_view  = abap_true      "基本資料視圖
    mrp_view    = abap_true ).   "MRP 視圖

  " ③ Client 層(MARA):淨重與單位
  DATA(ls_client)  = VALUE bapi_mara(  net_weight = p_ntgew
                                       unit_of_wt = p_gewei ).
  DATA(ls_clientx) = VALUE bapi_marax( net_weight = abap_true
                                       unit_of_wt = abap_true ).
  "★ 老師講義原稿此處把 CLIDAOUT-UNIT_OF_WT 寫成 'X'(誤植),
  "   正確是「值放在 ls_client、旗標放在 ls_clientx」,不可混寫

  " ④ Plant 層(MARC):MRP2 的計畫交貨時間 PLND_DELRY = PLIFZ
  DATA(ls_plant)  = VALUE bapi_marc(  plant       = p_werks
                                      plnd_delry  = p_plifz ).
  DATA(ls_plantx) = VALUE bapi_marcx( plant       = p_werks   "X 結構的 KEY 要填實際值
                                      plnd_delry  = abap_true ).

  CLEAR lt_return.
  CALL FUNCTION 'BAPI_MATERIAL_SAVEDATA'
    EXPORTING headdata       = ls_head
              clientdata     = ls_client
              clientdatax    = ls_clientx
              plantdata      = ls_plant
              plantdatax     = ls_plantx
    IMPORTING return         = DATA(ls_return)
    TABLES    returnmessages = lt_return.

  " ⑤ 判斷 + COMMIT / ROLLBACK
  IF ls_return-type CA 'EA' OR line_exists( lt_return[ type = 'E' ] ).
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    LOOP AT lt_return INTO DATA(ls_msg) WHERE type CA 'EAW'.
      WRITE: / |[{ ls_msg-type }] { ls_msg-message }|.
    ENDLOOP.
  ELSE.
    CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
      EXPORTING wait = 'X'.
    WRITE: / |物料 { p_matnr } 修改成功|.
  ENDIF.
X 結構的 KEY 欄位要填真值,不是 'X'。例如 PLANTDATAX-PLANT 要填 '1710'(跟 PLANTDATA-PLANT 一樣),只有非 KEY 的資料欄位才填 'X'。填錯會得到「Plant is not maintained」之類看起來莫名其妙的錯誤。同理,SALESDATAX 要填 SALES_ORG / DISTR_CHAN、STORAGELOCATIONDATAX 要填 PLANT / STGE_LOC。
S/4HANA 補充:物料號碼欄位長度可延伸到 40 碼(MATNR 40)。BAPI_MATERIAL_SAVEDATA 的 HEADDATA-MATERIAL 仍是 18 碼,長物料號要改用 HEADDATA-MATERIAL_LONG;同理 BAPI_MARA 有 MATERIAL_LONG 版本。若系統啟用了長物料號而你只填 18 碼欄位,會出現「Material does not exist」。

11. 請購單 BAPI:BAPI_PR_CREATE 與 BAPI_REQUISITION_CREATE

請購單(Purchase Requisition,資料表 EBAN)有兩支建立 BAPI,這是新手最容易搞混的地方。老師在程式註解裡直接寫了官方參考:Note 499627 — FAQ BAPIs for purchase requisitions

比較BAPI_PR_CREATEBAPI_REQUISITION_CREATE
推出時間ECC 6.0 之後的新版(ME_REQUISITION 系列)舊版(4.6 時代)
結構PRHEADER / PRHEADERX / PRITEM / PRITEMX(有 X 結構)REQUISITION_ITEMS(無 X 結構)
回傳單號EXPORTING NUMBEREXPORTING NUMBER
功能支援較新的欄位、增強、服務項目欄位較少,但寫法簡單
建議S/4HANA 新開發一律用這支維護舊程式時才會遇到
*&---------------------------------------------------------------------*
*& Report ZBAPI_PR_CREATE
*& 建立請購單 — BAPI_PR_CREATE
*& 參考 SAP Note 499627 - FAQ BAPIs for purchase requisitions
*&---------------------------------------------------------------------*
REPORT zbapi_pr_create.

PARAMETERS: p_matnr TYPE matnr    OBLIGATORY,   "物料編號
            p_werks TYPE werks_d  OBLIGATORY,   "工廠
            p_ekorg TYPE ekorg    OBLIGATORY,   "採購組織
            p_ekgrp TYPE ekgrp,                 "採購群組
            p_menge TYPE bapicurr_d DEFAULT 10, "數量
            p_test  TYPE abap_bool AS CHECKBOX. "測試執行不真的建單

START-OF-SELECTION.

  " ① 抬頭:請購類型 NB=標準請購
  DATA(ls_head)  = VALUE bapimereqheader( pr_type = 'NB' ).
  DATA(ls_headx) = VALUE bapimereqheaderx( pr_type = abap_true ).

  " ② 項目:用 VALUE # 一次組好,項次固定 10 起跳
  DATA(lv_item) = CONV bnfpo( 10 ).

  DATA(lt_item) = VALUE bapimereqitemimp_t(
    ( preq_item  = lv_item
      preq_name  = sy-uname            "請購者
      material   = p_matnr
      plant      = p_werks
      purch_org  = p_ekorg
      pur_group  = p_ekgrp
      quantity   = p_menge
      deliv_date = sy-datum + 7 ) ).   "交貨日期,實務上不要用當天

  " ③ X 結構:要傳的欄位全部給 'X'(PREQ_ITEM 是 KEY,填項次不是 'X')
  DATA(lt_itemx) = VALUE bapimereqitemx_t(
    ( preq_item  = lv_item
      material   = abap_true
      plant      = abap_true
      purch_org  = abap_true
      pur_group  = abap_true
      quantity   = abap_true
      deliv_date = abap_true ) ).

  DATA lt_return TYPE bapiret2_t.

  CALL FUNCTION 'BAPI_PR_CREATE'
    EXPORTING prheader     = ls_head
              prheaderx    = ls_headx
              testrun      = p_test        "測試模式:只驗證不寫入
    IMPORTING number       = DATA(lv_banfn)  "傳回的請購單號
    TABLES    return       = lt_return
              pritem       = lt_item
              pritemx      = lt_itemx.

  " ④ 有單號才 COMMIT,沒單號一律 ROLLBACK
  IF lv_banfn IS NOT INITIAL AND
     NOT line_exists( lt_return[ type = 'E' ] ).
    IF p_test = abap_false.
      CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
        EXPORTING wait = 'X'.             "要馬上讀 EBAN,WAIT 一定要給
    ENDIF.
    MESSAGE |請購單建立成功:{ lv_banfn }| TYPE 'S'.
  ELSE.
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    LOOP AT lt_return INTO DATA(ls_ret) WHERE type CA 'EAW'.
      WRITE: / |[{ ls_ret-type }] { ls_ret-id }{ ls_ret-number } { ls_ret-message }|.
    ENDLOOP.
  ENDIF.
TESTRUN = 'X' 是 BAPI 的隱藏好朋友。BAPI_PR_CREATE、BAPI_PO_CREATE1、BAPI_SALESORDER_CREATEFROMDAT2 都支援,可以「跑一次驗證但不真的建單」。做介面時先用 TESTRUN 把使用者的資料驗一輪、把錯誤一次全吐回去,比建到一半失敗好太多。

舊版 BAPI_REQUISITION_CREATE 與查詢用的 BAPI_REQUISITION_GETDETAIL(老師的案例是「請購備品」):

*&---------------------------------------------------------------------*
*& 舊版請購 BAPI:CREATE + GETDETAIL(請購備品案例)
*&---------------------------------------------------------------------*
REPORT zbapi_requisition.

PARAMETERS: p_matnr TYPE matnr   OBLIGATORY,
            p_werks TYPE werks_d OBLIGATORY DEFAULT '1710',
            p_menge TYPE bamng   DEFAULT 5,
            p_ban   TYPE banfn.        "查詢用:既有請購單號

START-OF-SELECTION.

  DATA lt_return TYPE STANDARD TABLE OF bapireturn.

  IF p_ban IS INITIAL.
    " ---------- 建立(無 X 結構,欄位直接給值) ----------
    DATA(lt_items) = VALUE bapiebanc_t(
      ( preq_item  = '00010'
        doc_type   = 'NB'
        material   = p_matnr
        plant      = p_werks
        quantity   = p_menge
        deliv_date = sy-datum + 7
        preq_name  = sy-uname
        pur_group  = '001'
        acctasscat = ''          "帳戶指派類別;備品常用 'K'(成本中心)
        item_cat   = '' ) ).     "項目類別

    CALL FUNCTION 'BAPI_REQUISITION_CREATE'
      IMPORTING number            = DATA(lv_banfn)
      TABLES    requisition_items = lt_items
                return            = lt_return.

    IF lv_banfn IS NOT INITIAL.
      CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
      MESSAGE |請購單 { lv_banfn } 建立完成| TYPE 'S'.
    ELSE.
      CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
      LOOP AT lt_return INTO DATA(ls_r).
        WRITE: / |[{ ls_r-type }] { ls_r-message }|.
      ENDLOOP.
    ENDIF.

  ELSE.
    " ---------- 查詢明細 ----------
    DATA: lt_req_items TYPE STANDARD TABLE OF bapiebanc,
          lt_req_acct  TYPE STANDARD TABLE OF bapiebkn,
          lt_req_text  TYPE STANDARD TABLE OF bapiebantx.

    CALL FUNCTION 'BAPI_REQUISITION_GETDETAIL'
      EXPORTING number                  = p_ban
      TABLES    requisition_items       = lt_req_items
                requisition_account_assignment = lt_req_acct
                requisition_item_text   = lt_req_text
                return                  = lt_return.

    " 用 CL_SALV_TABLE 直接顯示明細
    TRY.
        cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                                CHANGING  t_table      = lt_req_items ).
        lo_alv->get_functions( )->set_all( abap_true ).
        lo_alv->get_columns( )->set_optimize( abap_true ).
        lo_alv->display( ).
      CATCH cx_salv_msg INTO DATA(lo_ex).
        MESSAGE lo_ex->get_text( ) TYPE 'E'.
    ENDTRY.
  ENDIF.
相關的請購/採購 BAPI 一次列齊:BAPI_PR_CREATE(建立)、BAPI_PR_CHANGE(變更)、BAPI_PR_GETDETAIL(明細)、BAPI_REQUISITION_CREATE / CHANGE / DELETE / GETDETAIL / GETITEMS / RELEASE / RESET_RELEASE簽核(Release)是老師專案要點裡的「MM 能做請購簽核」,用的就是 BAPI_REQUISITION_RELEASE(傳 REL_CODE 與 NUMBER),簽核策略在 EBAN-FRGKZ / FRGZU 欄位。

12. 採購單 BAPI:BAPI_PO_CREATE1 與 BAPI_PO_CHANGE

講義第 08 節是「BAPI_PO_CHANGE 採購單修改日期」:改交貨日期要改的是交貨排程行(Schedule Line,資料表 EKET),不是項目本身。老師在原始碼旁寫的重點:「上面要轉的參數,在下面都要給 'X' 值,如 po_itemx = 'X'。」

*&---------------------------------------------------------------------*
*& Report ZPO_CHANGE — 修改採購單交貨日期(EKET 排程行)
*&---------------------------------------------------------------------*
REPORT zpo_change.

PARAMETERS: p_ebeln TYPE ekko-ebeln OBLIGATORY,
            p_ebelp TYPE ekpo-ebelp OBLIGATORY DEFAULT '00010',
            p_etenr TYPE eket-etenr  OBLIGATORY DEFAULT '0001',
            p_eindt TYPE eket-eindt  OBLIGATORY.

START-OF-SELECTION.

  " 排程行資料:del_datcat_ext = 'D'(日)/'W'(週)/'M'(月)
  DATA(lt_sched) = VALUE bapimeposchedule_tt(
    ( po_item        = p_ebelp
      sched_line     = p_etenr
      del_datcat_ext = 'D'
      delivery_date  = p_eindt ) ).

  " X 結構:KEY 欄位填真值,另外要把 KEY 的 X 旗標也給 'X'
  DATA(lt_schedx) = VALUE bapimeposchedulx_tt(
    ( po_item        = p_ebelp
      po_itemx       = abap_true
      sched_line     = p_etenr
      sched_linex    = abap_true
      del_datcat_ext = abap_true
      delivery_date  = abap_true ) ).

  DATA lt_return TYPE bapiret2_t.

  CALL FUNCTION 'BAPI_PO_CHANGE'
    EXPORTING purchaseorder = p_ebeln
    TABLES    return        = lt_return
              poschedule    = lt_sched
              poschedulex   = lt_schedx.

  "★ 講義原稿用 IF sy-subrc NE 0 判斷是錯的:
  "   BAPI 呼叫後 sy-subrc 幾乎永遠是 0,必須改判 RETURN 表
  IF line_exists( lt_return[ type = 'E' ] ) OR
     line_exists( lt_return[ type = 'A' ] ).
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    LOOP AT lt_return INTO DATA(ls_r) WHERE type CA 'EA'.
      WRITE: / |[{ ls_r-type }] { ls_r-message }|.
    ENDLOOP.
  ELSE.
    CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
    MESSAGE |採購單 { p_ebeln } 項次 { p_ebelp } 交期已改為 { p_eindt DATE = USER }|
            TYPE 'S'.
  ENDIF.
*&---------------------------------------------------------------------*
*& 建立採購單 — BAPI_PO_CREATE1(含 Header / Item / Schedule 三層)
*&---------------------------------------------------------------------*
REPORT zbapi_po_create.

PARAMETERS: p_lifnr TYPE lifnr    OBLIGATORY,   "供應商(S/4 是 BP 的供應商角色)
            p_ekorg TYPE ekorg    OBLIGATORY,
            p_ekgrp TYPE ekgrp    OBLIGATORY,
            p_bukrs TYPE bukrs    OBLIGATORY,
            p_matnr TYPE matnr    OBLIGATORY,
            p_werks TYPE werks_d  OBLIGATORY,
            p_menge TYPE bstmg    DEFAULT 100,
            p_netpr TYPE bprei.

START-OF-SELECTION.

  DATA(ls_header)  = VALUE bapimepoheader(
    comp_code  = p_bukrs
    doc_type   = 'NB'            "標準採購單
    vendor     = |{ p_lifnr ALPHA = IN }|
    purch_org  = p_ekorg
    pur_group  = p_ekgrp
    doc_date   = sy-datum ).

  DATA(ls_headerx) = VALUE bapimepoheaderx(
    comp_code = abap_true  doc_type  = abap_true
    vendor    = abap_true  purch_org = abap_true
    pur_group = abap_true  doc_date  = abap_true ).

  DATA(lt_item) = VALUE bapimepoitem_tt(
    ( po_item   = '00010'
      material  = p_matnr
      plant     = p_werks
      quantity  = p_menge
      net_price = p_netpr
      po_unit   = 'PC' ) ).

  DATA(lt_itemx) = VALUE bapimepoitemx_tt(
    ( po_item   = '00010'   po_itemx  = abap_true
      material  = abap_true plant     = abap_true
      quantity  = abap_true net_price = abap_true
      po_unit   = abap_true ) ).

  " 交貨排程:不給的話 BAPI 會用項目的交期自動生一行
  DATA(lt_sched) = VALUE bapimeposchedule_tt(
    ( po_item = '00010' sched_line = '0001'
      delivery_date = sy-datum + 14  quantity = p_menge ) ).

  DATA(lt_schedx) = VALUE bapimeposchedulx_tt(
    ( po_item = '00010' po_itemx = abap_true
      sched_line = '0001' sched_linex = abap_true
      delivery_date = abap_true quantity = abap_true ) ).

  DATA lt_return TYPE bapiret2_t.

  CALL FUNCTION 'BAPI_PO_CREATE1'
    EXPORTING poheader     = ls_header
              poheaderx    = ls_headerx
              no_price_from_po = abap_true   "不自動抓價格條件時用
    IMPORTING exppurchaseorder = DATA(lv_ebeln)
    TABLES    return       = lt_return
              poitem       = lt_item
              poitemx      = lt_itemx
              poschedule   = lt_sched
              poschedulex  = lt_schedx.

  IF lv_ebeln IS NOT INITIAL.
    CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
    MESSAGE |採購單建立成功:{ lv_ebeln }| TYPE 'S'.
  ELSE.
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    MESSAGE zcl_bapi_util=>to_text( lt_return ) TYPE 'S' DISPLAY LIKE 'E'.
  ENDIF.
S/4HANA 的採購單建立不要用 BAPI_PO_CREATE(舊版、已不建議),一律用 BAPI_PO_CREATE1。另外注意 S/4 的 MM 供應商即 BP:LIFNR 仍存在於 LFA1,但是由 BP(CVI)產生的,不能再用 XK01/BAPI_VENDOR_CREATE 直接建。

13. 商業夥伴 BP:BAPI_BUPA_* 與 S/4HANA 的 CVI

S/4HANA 最大的主資料改變之一:客戶(KNA1)與供應商(LFA1)合併成商業夥伴 BP,交易只剩 BP,XD01/XK01 等舊交易被封鎖。底層靠 CVI(Customer/Vendor Integration) 把 BUT000 與 KNA1/LFA1 同步。

13-1 用 BAPI 建立 BP 基本資料(ZBP)

*&---------------------------------------------------------------------*
*& Report ZBP — 用 BAPI_BUPA_CREATE_FROM_DATA 建立商業夥伴
*&---------------------------------------------------------------------*
REPORT zbp.

PARAMETERS: p_grp   TYPE bapibus1006_head-partn_grp DEFAULT 'BP02', "分組
            p_name1 TYPE bapibus1006_central_organ-name1
                    DEFAULT 'CASPER TRADING CO.',
            p_land  TYPE bapibus1006_address-country DEFAULT 'TW'.

START-OF-SELECTION.

  " ① 中心資料:搜尋詞與稱謂
  DATA(ls_central) = VALUE bapibus1006_central(
    searchterm1 = 'CASPER'
    searchterm2 = 'CASPER2'
    title_key   = '0003' ).            "0003=公司

  " ② 名稱資訊(組織類型才有 NAME1~NAME4)
  DATA(ls_org) = VALUE bapibus1006_central_organ(
    name1 = p_name1
    name2 = ''
    name3 = '' ).

  " ③ 標準地址
  DATA(ls_addr) = VALUE bapibus1006_address(
    postl_cod1 = '40401'
    city       = 'TAICHUNG'
    country    = p_land
    region     = ''
    street     = 'SAN MIN RD. SEC.3, NO.129'
    langu      = sy-langu ).

  " ④ 通訊資料:電話、行動電話、傳真、E-mail
  DATA(lt_tel) = VALUE bapiadtel_tt(
    ( country = p_land std_no = abap_true home_flag = abap_true
      r_3_user = '1'  telephone = '04-22195678' )      "1=市話
    ( country = p_land r_3_user = '3'
      telephone = '0912345678' ) ).                     "3=行動電話

  DATA(lt_fax)  = VALUE bapiadfax_tt(  ( fax    = '04-22195679' ) ).
  DATA(lt_mail) = VALUE bapiadsmtp_tt( ( e_mail = 'tocasper@hotmail.com' ) ).

  DATA: lv_partner TYPE bapibus1006_head-bpartner,
        lt_return  TYPE bapiret2_t.

  " ⑤ 建立 BP。PARTNERCATEGORY:1=自然人 Person、2=組織 Organization、3=群組 Group
  CALL FUNCTION 'BAPI_BUPA_CREATE_FROM_DATA'
    EXPORTING businesspartnerextern   = lv_partner   "外部編碼;內部編碼留空
              partnercategory         = '2'
              partnergroup            = p_grp
              centraldata             = ls_central
              centraldataorganization = ls_org
              addressdata             = ls_addr
    IMPORTING businesspartner         = lv_partner
    TABLES    telefondata             = lt_tel
              faxdata                 = lt_fax
              e_maildata              = lt_mail
              return                  = lt_return.

  " ⑥ 判 RETURN 再決定 COMMIT(原稿用 LOOP ... EXIT + sy-subrc,
  "   改用 line_exists 更直觀,也不會被中途的其他語句改掉 sy-subrc)
  IF line_exists( lt_return[ type = 'E' ] ) OR
     line_exists( lt_return[ type = 'A' ] ).
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    LOOP AT lt_return INTO DATA(ls_r) WHERE type CA 'EA'.
      WRITE: / |[{ ls_r-type }] { ls_r-message }|.
    ENDLOOP.
  ELSE.
    CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
    MESSAGE |BP 建立成功:{ lv_partner ALPHA = OUT }| TYPE 'S'.

    " ⑦ 建完基本資料後,通常還要指派角色(例如 FLCU01 客戶、FLVN01 供應商)
    CALL FUNCTION 'BAPI_BUPA_ROLE_ADD_2'
      EXPORTING businesspartner     = lv_partner
                businesspartnerrole = 'FLCU01'      "客戶:SD 角色
      TABLES    return              = lt_return.
    IF NOT line_exists( lt_return[ type = 'E' ] ).
      CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
    ENDIF.
  ENDIF.

13-2 BP 相關 BAPI 全表(講義第 07 節「HANA BP 詳解」)

分類函數用途
建立BAPI_BUPA_CREATE_FROM_DATA建立 BP,含中心資料、地址等
BAPI_BUPA_FS_CREATE_FROM_DATA2功能同上,多一個角色分配功能
角色維護BAPI_BUPA_ROLES_GET_2取得已分配的 Role(按日期)
BAPI_BUPA_ROLE_ADD_2新增 Role
BAPI_BUPA_ROLE_CHANGE修改已存在的 Role 屬性
BAPI_BUPA_ROLE_EXIST_CHECK_2檢核 Role 是否存在
BAPI_BUPA_ROLE_REMOVE刪除已分配的 Role
中心資料BAPI_BUPA_CENTRAL_GETDETAIL取得中心資料
BAPI_BUPA_CENTRAL_CHANGE修改中心資料
地址資訊BAPI_BUPA_ADDRESSES_GET取得所有地址
BAPI_BUPA_ADDRESS_ADD新增地址
BAPI_BUPA_ADDRESS_CHANGE修改既有地址
BAPI_BUPA_ADDRESS_GETDETAIL取得某地址的明細
BAPI_BUPA_ADDRESS_GET_NUMBERS取得地址對應的 address number
BAPI_BUPA_ADDRESS_REMOVE刪除地址
銀行資訊BAPI_BUPA_BANKDETAILS_GET取得所有銀行資訊
BAPI_BUPA_BANKDETAIL_ADD新增銀行資訊
BAPI_BUPA_BANKDETAIL_CHANGE修改銀行資訊
BAPI_BUPA_BANKDETAIL_GETDETAIL取得單一銀行明細
BAPI_BUPA_BANKDETAIL_NUMBERS取得銀行 ID
BAPI_BUPA_BANKDETAIL_REMOVE刪除銀行資訊
證件號碼
(Identification)
BAPI_IDENTIFICATIONDETAILS_GET取得所有標識號
BAPI_IDENTIFICATION_GET_DETAIL取得單一標識號明細
BAPI_IDENTIFICATION_ADD新增標識號
BAPI_IDENTIFICATION_CHANGE修改標識號
BAPI_IDENTIFICATION_REMOVE刪除標識號
API(推薦)CMD_EI_API_EXTRACT讀取 BP 客戶資訊
VMD_EI_API_EXTRACT讀取 BP 供應商資訊
CMD_EI_API維護 BP 客戶(類別)
VMD_EI_API維護 BP 供應商(類別)
CL_MD_BP_MAINTAIN維護 BP(較全功能)——S/4HANA 首選
增強PARTNER_UPDATEBP 建立或修改時的 BAdI/Exit
ADDRESS_UPDATEBP 地址(通訊)資料維護

13-3 BP / 客戶 / 供應商 的資料表清單

群組資料表內容
BPBUT000BP 一般資料(BP 的核心表)
BUT020BP 地址(連到 ADRC)
BUT100BP 角色(FLCU01、FLVN01⋯)
BUT0BKBP 銀行
BUT05*BP Relation(夥伴關係)
BUT0CCPayment(付款卡/付款資料)
客戶KNA1客戶基本資料
KNB1客戶公司代碼
KNVV客戶 SD 資料
KNVI客戶稅別標識
KNAS加值稅登記號(VAT)
KNB5催款資料
KNBK銀行明細
KNVALoading Point
KNVD銷售查詢表(Output for Document)
KNVL許可證(Licenses)
KNVSShipping Point
KNEXLegal Control - Sanctioned Party List
KNZA備選付款人
KNKK信貸管理(S/4 已改 FSCM,見警告)
KNC1客戶註記錄(過帳金額)
VCKUN信用相關
VCNUM信用相關
供應商LFA1供應商主資料
LFB1供應商公司代碼
LFM1供應商採購視圖
LFASTax Number
S/4HANA 的 BP 相關陷阱
BAPI_CUSTOMER_CREATE / BAPI_VENDOR_CREATE 已不可用。建客戶/供應商一律走 BP:優先用 CL_MD_BP_MAINTAIN=>maintain( )CVI_EI_INBOUND_MAINCMD_EI_API=>maintain_bapi( )VMD_EI_API=>maintain_bapi( );最新的做法是用已釋出的 OData API API_BUSINESS_PARTNER
② 查詢型 BAPI_CUSTOMER_GETDETAIL2 / BAPI_VENDOR_GETDETAIL 在 S/4 仍可用(讀 KNA1/LFA1),但要記得 KNA1 是 CVI 同步出來的從表,主資料真相在 BUT000。
③ 信用管理 KNKK / KNKA 在 S/4 已被 FSCM Credit Management(UKMBP_CMS_SGM) 取代。
④ BP 編號與客戶編號可能不同(視編碼配置),介面對接時務必確認雙方交換的到底是 PARTNER 還是 KUNNR。

13-4 自建 RFC:ZSD_BP_DATA 一次查出客戶+供應商

老師的示範重點之一:「這個例子,是自己寫的 BAPI 有 RFC」。把三支標準 BAPI 包成一支對外介面,外部系統只要呼叫一次。

參數型別說明
I_CUSTOMERNOBAPICUSTOMER_ID-CUSTOMER客戶代碼(供應商也用同一個代碼查)
I_PI_SALESORGBAPIKNA102-SALESORG銷售組織
I_VENCHAR1(選填)= 'X' 時一併抓供應商資料
LT_CUSTOMERADDRESSBAPICUSTOMER_04客戶地址
LT_CUSTOMERGENERALDETAILBAPICUSTOMER_KNA1客戶一般資料
LT_RETURNBAPIRET1客戶查詢狀態
LT_PE_PERSONALDATABAPIKNA101_1聯絡人個人資料
LT_PE_COMPANYDATABAPIKNA106聯絡人公司資料
LT_VENDORGENERALDETAILBAPIVENDOR_04供應商基本資料
LT_CUSTOMERBANKDETAIL(TABLES)BAPICUSTOMER_02客戶銀行資料
LT_VENDORBANKDETAIL(TABLES)BAPIVENDOR_06供應商銀行資料
FUNCTION zsd_bp_data.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  IMPORTING
*"     VALUE(I_CUSTOMERNO) LIKE  BAPICUSTOMER_ID-CUSTOMER
*"     VALUE(I_PI_SALESORG) LIKE  BAPIKNA102-SALESORG
*"     VALUE(I_VEN) TYPE  CHAR1 OPTIONAL
*"  EXPORTING
*"     VALUE(LT_CUSTOMERADDRESS) LIKE  BAPICUSTOMER_04
*"     VALUE(LT_CUSTOMERGENERALDETAIL) LIKE  BAPICUSTOMER_KNA1
*"     VALUE(LT_RETURN) LIKE  BAPIRET1
*"     VALUE(LT_PE_PERSONALDATA) LIKE  BAPIKNA101_1
*"     VALUE(LT_PE_COMPANYDATA) LIKE  BAPIKNA106
*"     VALUE(LT_VENDORGENERALDETAIL) LIKE  BAPIVENDOR_04
*"  TABLES
*"      LT_CUSTOMERBANKDETAIL STRUCTURE  BAPICUSTOMER_02 OPTIONAL
*"      LT_VENDORBANKDETAIL STRUCTURE  BAPIVENDOR_06 OPTIONAL
*"----------------------------------------------------------------------
*"  屬性頁籤要選 Remote-Enabled Module,每個參數都要勾 Pass Value
*"----------------------------------------------------------------------

  CLEAR: lt_customeraddress, lt_customergeneraldetail, lt_return,
         lt_pe_personaldata, lt_pe_companydata, lt_vendorgeneraldetail,
         lt_customerbankdetail[], lt_vendorbankdetail[].

  " 客戶代碼要補零(ALPHA 轉換),外部系統常常只送 '1000'
  DATA(lv_kunnr) = |{ i_customerno ALPHA = IN }|.

  " ① 地址 + 銀行資料(對應 BP 角色 FLCU01 客戶)
  CALL FUNCTION 'BAPI_CUSTOMER_GETDETAIL2'
    EXPORTING customerno             = lv_kunnr
    IMPORTING customeraddress        = lt_customeraddress
              customergeneraldetail  = lt_customergeneraldetail
              return                 = lt_return
              "★ 若回 "Customer XXXXXXXX does not exist",
              "   代表這個 BP 沒有建 FLCU01 客戶角色
    TABLES    customerbankdetail     = lt_customerbankdetail.

  " ② 聯絡人(要給銷售組織)
  CALL FUNCTION 'BAPI_CUSTOMER_GETDETAIL1'
    EXPORTING customerno      = lv_kunnr
              pi_salesorg     = i_pi_salesorg
    IMPORTING pe_personaldata = lt_pe_personaldata
              pe_companydata  = lt_pe_companydata.

  " ③ 供應商(同一個 BP 也可能有 FLVN01 角色)
  IF i_ven = abap_true.
    CALL FUNCTION 'BAPI_VENDOR_GETDETAIL'
      EXPORTING vendorno      = lv_kunnr
      IMPORTING generaldetail = lt_vendorgeneraldetail
      TABLES    bankdetail    = lt_vendorbankdetail.
  ENDIF.

ENDFUNCTION.

測試方式((依講義步驟整理)):SE37 → 輸入 ZSD_BP_DATA → 按 F8 測試 → 填 I_CUSTOMERNO、I_PI_SALESORG、I_VEN='X' → 執行 → 逐一展開 EXPORTING 結構與 TABLES 內表檢視結果;再用 pyrfc / SoapUI 從外部呼叫一次,確認沒有被 RFC 黑名單擋住

*&-------------------------------------------------------------*
*& S/4HANA 的 BP 讀寫:直接走 BUT000 + CVI 對照,或用 CL_MD_BP_MAINTAIN
*&-------------------------------------------------------------*

" ① 讀:BP 一般資料 + 角色 + 客戶/供應商對照(CVI 對照表)
SELECT FROM but000 AS b
  LEFT OUTER JOIN but100 AS r ON r~partner = b~partner
  LEFT OUTER JOIN cvi_cust_link AS cl ON cl~partner_guid = b~partner_guid
  LEFT OUTER JOIN cvi_vend_link AS vl ON vl~partner_guid = b~partner_guid
  FIELDS b~partner, b~bu_group, b~type, b~name_org1, b~name_org2,
         b~bpkind, r~rltyp, cl~customer, vl~vendor
  WHERE b~partner IN @s_partnr
  ORDER BY b~partner, r~rltyp
  INTO TABLE @DATA(lt_bp).

" ② 寫:用 CL_MD_BP_MAINTAIN(S/4 建議做法,會同時處理 CVI 同步)
DATA(ls_bp_data) = VALUE cvis_ei_extern(
  partner-header-object_instance-bpartner = lv_partner
  partner-header-object_task              = 'M'    "I=Insert / U=Update / M=Modify
  partner-central_data-common-data-bp_centraldata-searchterm1 = 'CASPER'
  partner-central_data-common-datax-bp_centraldata-searchterm1 = abap_true ).

DATA(lt_bp_msg) = VALUE bapiretm( ).
cl_md_bp_maintain=>maintain(
  EXPORTING i_data    = VALUE cvis_ei_extern_t( ( ls_bp_data ) )
  IMPORTING et_return = lt_bp_msg ).

" bapiretm 是「每個 BP 一組 BAPIRET2」的巢狀表,要兩層迴圈判斷
DATA(lv_bp_error) = abap_false.
LOOP AT lt_bp_msg INTO DATA(ls_bp_msg).
  LOOP AT ls_bp_msg-object_msg INTO DATA(ls_one) WHERE type CA 'EA'.
    WRITE: / |[{ ls_one-type }] { ls_one-message }|.
    lv_bp_error = abap_true.
  ENDLOOP.
ENDLOOP.

IF lv_bp_error = abap_false.
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
ELSE.
  CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ENDIF.

14. 常用 BAPI 速查表(依模組)

模組BAPI用途備註
MM 採購BAPI_PR_CREATE / _CHANGE / _GETDETAIL請購單(EBAN)S/4 建議版本
BAPI_REQUISITION_CREATE / GETDETAIL / RELEASE請購單舊版與簽核Note 499627
BAPI_PO_CREATE1 / BAPI_PO_CHANGE / BAPI_PO_GETDETAIL1採購單(EKKO/EKPO/EKET)不要用 BAPI_PO_CREATE
BAPI_PO_RELEASE / BAPI_PO_RESET_RELEASE採購單簽核/取消簽核要傳 REL_CODE
MM 主資料
與庫存
BAPI_MATERIAL_SAVEDATA建立/修改物料主檔X 結構是關鍵
BAPI_MATERIAL_GET_ALL / BAPI_MATERIAL_GET_DETAIL讀取物料主檔GET_ALL 一次拿多視圖
BAPI_GOODSMVT_CREATE物料移動過帳(MIGO)GM_CODE 01=收貨對 PO、03=其他收發、04=轉儲、05=其他收貨、06=退貨
BAPI_MATERIAL_AVAILABILITY / BAPI_MATERIAL_STOCK_REQ_LISTATP 可用量 / MD04 庫存需求表見第 16 節
SDBAPI_SALESORDER_CREATEFROMDAT2建立銷售訂單(VA01)BO=BUS2032、Method=CreateFromDat2
BAPI_SALESORDER_CHANGE / _GETLIST / _GETSTATUS銷售訂單變更/查詢CHANGE 要傳 UPDATEFLAG
BAPI_OUTB_DELIVERY_CONFIRM_DEC出庫交貨過帳發貨(PGI)另有 _CHANGE、_CREATE_SLS、_SAVEREPLICA
BAPI_BILLINGDOC_CREATEMULTIPLE開立發票(VF01)
FIBAPI_ACC_DOCUMENT_POST過帳會計憑證(FB01/FB50)S/4 資料落在 ACDOCA
BAPI_INCOMINGINVOICE_CREATE採購發票驗證(MIRO)另有 _PARK、_CHANGE、_CANCEL
BAPI_AR_ACC_GETOPENITEMS / BAPI_AP_ACC_GETOPENITEMS應收/應付未清項
PPBAPI_PRODORDCONF_CREATE_TT生產工單確認(時間事件 Time Ticket,CO11N)另有 _CREATE_HDR(抬頭確認)、_CANCEL
BAPI_PRODORD_CREATE / _RELEASE / _CHANGE生產工單建立/下達
BAPI_PLANNEDORDER_CREATE / _CHANGE計畫訂單
BPBAPI_BUPA_CREATE_FROM_DATA 系列商業夥伴見第 13 節
CL_MD_BP_MAINTAIN / CMD_EI_API / VMD_EI_APIS/4 建議做法會同時處理 CVI 同步
共用BAPI_TRANSACTION_COMMIT / ROLLBACK提交/回復COMMIT 記得 WAIT='X'
BAPI_MESSAGE_GETDETAIL由 ID/NUMBER 取回完整訊息文字介面回饋常用
*&-------------------------------------------------------------*
*& 三支高頻 BAPI 的骨架:發貨過帳 / 會計憑證 / 生產工單確認
*&-------------------------------------------------------------*

" ① BAPI_GOODSMVT_CREATE:對採購單收貨(GM_CODE = '01',移動類型 101)
DATA(ls_gm_head) = VALUE bapi2017_gm_head_01(
  pstng_date = sy-datum
  doc_date   = sy-datum
  ref_doc_no = |IF{ sy-datum }| ).

DATA(lt_gm_item) = VALUE bapi2017_gm_item_create_t(
  ( material   = lv_matnr
    plant      = '1710'
    stge_loc   = '171A'
    move_type  = '101'
    entry_qnt  = 100
    entry_uom  = 'PC'
    po_number  = lv_ebeln
    po_item    = '00010'
    mvt_ind    = 'B' ) ).           "B=對採購單收貨

DATA lt_return TYPE bapiret2_t.
CALL FUNCTION 'BAPI_GOODSMVT_CREATE'
  EXPORTING goodsmvt_header  = ls_gm_head
            goodsmvt_code    = VALUE bapi2017_gm_code( gm_code = '01' )
  IMPORTING materialdocument = DATA(lv_mblnr)
            matdocumentyear  = DATA(lv_mjahr)
  TABLES    goodsmvt_item    = lt_gm_item
            return           = lt_return.

IF lv_mblnr IS NOT INITIAL.
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
  MESSAGE |物料憑證 { lv_mblnr }/{ lv_mjahr } 過帳成功| TYPE 'S'.
ELSE.
  CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ENDIF.

" ② BAPI_ACC_DOCUMENT_POST:會計憑證(借貸必須平衡,否則整筆退回)
DATA(ls_doc_hdr) = VALUE bapiache09(
  bus_act    = 'RFBU'              "RFBU=FI 一般過帳
  username   = sy-uname
  header_txt = 'IF POSTING'
  comp_code  = '1710'
  doc_date   = sy-datum
  pstng_date = sy-datum
  doc_type   = 'SA' ).

DATA(lt_gl) = VALUE bapiacgl09_t(
  ( itemno_acc = '0000000001' gl_account = '0000400000'
    item_text  = '費用' costcenter = '0000017101' )
  ( itemno_acc = '0000000002' gl_account = '0000113100'
    item_text  = '銀行' ) ).

" 金額表:借正貸負,幣別要跟公司代碼一致
DATA(lt_amt) = VALUE bapiaccr09_t(
  ( itemno_acc = '0000000001' currency = 'TWD' amt_doccur = '1000.00' )
  ( itemno_acc = '0000000002' currency = 'TWD' amt_doccur = '1000.00-' ) ).

CLEAR lt_return.
CALL FUNCTION 'BAPI_ACC_DOCUMENT_POST'
  EXPORTING documentheader = ls_doc_hdr
  IMPORTING obj_key        = DATA(lv_objkey)
  TABLES    accountgl      = lt_gl
            currencyamount = lt_amt
            return         = lt_return.

IF NOT line_exists( lt_return[ type = 'E' ] ).
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
  MESSAGE |會計憑證 { lv_objkey }| TYPE 'S'.
ELSE.
  CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ENDIF.

" ③ BAPI_PRODORDCONF_CREATE_TT:生產工單時間事件確認
DATA(lt_conf) = VALUE bapi_pp_timeticket_t(
  ( orderid    = lv_aufnr
    operation  = '0010'
    yield      = 50
    conf_quan_unit = 'PC'
    fin_conf   = ''                "'X'=最終確認
    postg_date = sy-datum
    ex_time_1  = 2  un_time_1 = 'STD' ) ).

CLEAR lt_return.
CALL FUNCTION 'BAPI_PRODORDCONF_CREATE_TT'
  EXPORTING post_wrong_entries = '0'      "0=有錯就整批不過
  TABLES    timetickets        = lt_conf
            return             = lt_return.

IF NOT line_exists( lt_return[ type = 'E' ] ).
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
ELSE.
  CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ENDIF.
S/4HANA 的資料表已經換人:BAPI_ACC_DOCUMENT_POST 過完帳,資料實際落在 ACDOCA(Universal Journal);BKPF/BSEG 仍在(BSEG 是真表、BSIS/BSAS/BSID/BSAD/BSIK/BSAK 已變成 CDS 相容視圖)。物料憑證則從 MKPF/MSEG 改為 MATDOC(MKPF/MSEG 變成相容視圖)。做介面對帳時要查的是 ACDOCA / MATDOC,效能才會對。

15. Extension In / Out:把客製欄位塞進標準 BAPI

標準 BAPI 的參數是固定的,但客戶一定會有客製欄位(Append 在 EKPO、VBAP 上的 ZZ 欄位)。SAP 的官方解法是 Extension(擴展)

  • EXTENSIONIN:呼叫方把客製欄位資料塞進去(型別多為 BAPIPAREX,欄位 STRUCTURE + VALUEPART1~4)。
  • EXTENSIONOUT:BAPI 把客製欄位回傳出來。
  • SAP 端要在對應的 BAdI / Customer Exit 中把 EXTENSIONIN 解開,寫進資料庫。常見的擴展結構命名是 BAPI_TE_<table>(例如 BAPI_TE_MEPOITEM、BAPI_TE_VBAP)與其 X 結構 BAPI_TE_<table>X
*&-------------------------------------------------------------*
*& 用 EXTENSIONIN 傳客製欄位(以採購單項目 ZZ 欄位為例)
*&-------------------------------------------------------------*
" 前置:SE11 對 CI_EKPODB / CI_EKPO 加 ZZFIELD1;
"       並 Append 到 BAPI_TE_MEPOITEM / BAPI_TE_MEPOITEMX

DATA(ls_te)  = VALUE bapi_te_mepoitem(  po_item  = '00010'
                                        zzfield1 = 'MES-JOB-001' ).
DATA(ls_tex) = VALUE bapi_te_mepoitemx( po_item  = '00010'
                                        zzfield1 = abap_true ).

" BAPIPAREX 的 VALUEPART1~4 各 240 碼,要把結構「壓平」再切段
DATA(lt_ext) = VALUE bapiparex_t( ).

APPEND VALUE bapiparex(
  structure  = 'BAPI_TE_MEPOITEM'
  valuepart1 = ls_te+0(240)
  valuepart2 = ls_te+240(240) ) TO lt_ext.

APPEND VALUE bapiparex(
  structure  = 'BAPI_TE_MEPOITEMX'
  valuepart1 = ls_tex+0(240) ) TO lt_ext.

CALL FUNCTION 'BAPI_PO_CREATE1'
  EXPORTING poheader     = ls_header
            poheaderx    = ls_headerx
  IMPORTING exppurchaseorder = DATA(lv_po)
  TABLES    return       = lt_return
            poitem       = lt_item
            poitemx      = lt_itemx
            extensionin  = lt_ext.        "★ 客製欄位由這裡進去
EXTENSIONIN 不是自動生效的。你必須同時:① 在 SE11 把欄位 Append 到 BAPI_TE_* 結構;② 在對應的 BAdI(採購單是 ME_BAPI_PO_CUST / ME_PROCESS_PO_CUST;銷售訂單是 VBAP 相關的 BAPI_SD_SALESORDER Exit 或 MV45AFZZ)實作把值搬到內部結構。少了第二步,資料進去了也不會被寫進去,而且連錯誤訊息都不會有——這是介面除錯最鬼打牆的情形。
另外 VALUEPART 的切段位置一定要對齊結構長度,錯位會造成欄位內容整段偏移。

16. MD04 / MMBE 與 BOM 反查:讀取型 API

講義第 03 節列了三支「MD04 / MMBE」相關的 API,這是做 PP 缺料表(老師專案要點的第一項)最常用的:

函數對應交易說明
MD_STOCK_REQUIREMENTS_LIST_APIMD04庫存需求表,回傳 MDPSX(明細行)、MDEZX(含日期彙總)、MDSUX(彙總)。非 RFC-enabled,要對外要自己包一層
BAPI_MATERIAL_STOCK_REQ_LISTMD04官方 BAPI 版本,有 RFC 屬性,可直接被外部呼叫
BAPI_MATERIAL_AVAILABILITYCO09 / ATP可用量檢查(講義寫成 API_MATERIAL_AVAILABILITY,正確函數名是 BAPI_MATERIAL_AVAILABILITY
CS_WHERE_USED_MATCS15物料被用在哪些 BOM——會找多階
CSEP_MAT_BOM_SELECT_WHERE_USEDCS15同上,但僅找上一階
*&-------------------------------------------------------------*
*& MD04 庫存需求表 + BOM 反查(缺料分析的兩塊拼圖)
*&-------------------------------------------------------------*

" ① MD04:取得庫存/需求明細
DATA: lt_mdpsx TYPE STANDARD TABLE OF mdps,
      lt_mdezx TYPE STANDARD TABLE OF mdez,
      lt_mdsux TYPE STANDARD TABLE OF mdsu.

CALL FUNCTION 'MD_STOCK_REQUIREMENTS_LIST_API'
  EXPORTING  matnr        = lv_matnr
             werks        = lv_werks
             input_par    = VALUE mdinp( bertyp = 'A'      "MRP 元素全部
                                          disp_lev = space )
  IMPORTING  e_mt61d      = DATA(ls_mt61d)                 "抬頭:安全存量等
  TABLES     mdpsx        = lt_mdpsx
             mdezx        = lt_mdezx
             mdsux        = lt_mdsux
  EXCEPTIONS material_plant_not_found = 1
             plant_not_found          = 2
             OTHERS                   = 3.

IF sy-subrc <> 0.
  MESSAGE |MD04 讀取失敗({ sy-subrc }):{ lv_matnr } / { lv_werks }| TYPE 'E'.
ENDIF.

" 用 FOR 過濾出「可用量為負」的日期,就是缺料點
DATA(lt_short) = VALUE mdez_tt(
  FOR <ls_e> IN lt_mdezx WHERE ( mng02 < 0 ) ( <ls_e> ) ).

" ② BOM 反查:這顆料被哪些成品用到(多階)
DATA: lt_wultb TYPE STANDARD TABLE OF stpov,
      lt_equic TYPE STANDARD TABLE OF cscequi,
      lt_kndic TYPE STANDARD TABLE OF cscknd,
      lt_matc  TYPE STANDARD TABLE OF cscmat,
      lt_stdc  TYPE STANDARD TABLE OF cscstd,
      lt_tplc  TYPE STANDARD TABLE OF cyctpl.

CALL FUNCTION 'CS_WHERE_USED_MAT'
  EXPORTING  datub                     = '99991231'
             datuv                     = sy-datum
             matnr                     = lv_matnr
             werks                     = lv_werks
             stlan                     = '1'          "BOM 用途:1=生產
             mtnrv                     = lv_matnr
             postp                     = space
  TABLES     wultb                     = lt_wultb
             equicat                   = lt_equic
             kndcat                    = lt_kndic
             matcat                    = lt_matc
             stdcat                    = lt_stdc
             tplcat                    = lt_tplc
  EXCEPTIONS call_invalid              = 1
             material_not_found        = 2
             no_where_used_rec_found   = 3
             no_where_used_rec_selected = 4
             no_where_used_rec_valid   = 5
             OTHERS                    = 6.

CASE sy-subrc.
  WHEN 0.     "有找到
  WHEN 3 OR 4 OR 5.
    MESSAGE |物料 { lv_matnr } 沒有被任何 BOM 使用| TYPE 'S'.
  WHEN OTHERS.
    MESSAGE |BOM 反查失敗({ sy-subrc })| TYPE 'E'.
ENDCASE.

" 只要上一階時,改用這支比較快
" CALL FUNCTION 'CSEP_MAT_BOM_SELECT_WHERE_USED' ...
S/4HANA 的替代方案:MD04 有 CDS View C_MRPMaterialStockRequirement 與 Fiori App「Monitor Material Coverage」;庫存概覽 MMBE 對應 CDS I_MaterialStockNSDM_V_MARD。做報表時優先用 CDS,效能與可維護性都比呼叫傳統 FM 好。

17. 測試、除錯與常用小工具

17-1 測試方法

  • SE37 單獨測試SE37 → F8。可以按「測試資料目錄(Test Data Directory)」把測試資料存起來重複使用;也可以按「Debugging」直接進偵錯。這是驗證 BAPI 參數最快的方法。
  • SE38 包裝程式:把 BAPI 呼叫寫成一支報表,加上 PARAMETERS 與 SELECT-OPTIONS,方便反覆測與交給顧問測。TESTRUN 開關一定要留。
  • SE37 → Function Module → Test → Test sequences:可以把「建單 → COMMIT → 查詢」串成一連串測試。
  • 外部端測試:pyrfc 寫一支 10 行的腳本、或用 SAP GUI 的 SM59 Connection Test;Web Service 用 SOAMANAGER 的 Test 功能或 SoapUI/Postman。

17-2 出事了去哪裡查

症狀查哪裡常見原因
同步呼叫直接短拋ST22目標系統不通、型別不符(CX_SY_CONVERSION)、參數超長
tRFC 沒送出去 / 卡住SM58忘了 COMMIT WORK、目的地帳號密碼過期、目標函數不存在
qRFC 佇列停住SMQ1 / SMQ2前一筆 SYSFAIL 卡住整條佇列(要先修好第一筆)
bgRFC 單元失敗SBGRFCMONSupervisor Destination 沒設(SBGRFCCONF
外部呼叫被拒SU53、RS_RFC_BLACKLIST_CUSTOM、UCONCOCKPITS_RFC 權限不足、函數在黑名單、UCON 未開放
BAPI 回 E 但看不懂SLG1(用 RETURN 的 LOG_NO)、SE91(用 ID+NUMBER)訊息變數 V1~V4 沒帶出來
資料建好了卻查不到檢查 COMMIT 的 WAIT = 'X'更新任務還沒完成就去 SELECT
更新任務失敗SM13COMMIT 之後在 update task 才炸掉,前台看不到錯誤
介面錯誤要給業務看/AIF/ERRAIF(Application Interface Framework)的錯誤監控畫面

17-3 特殊符號處理(講義 10-1)

SAP 和外部系統對某些特殊字元的編碼方式可能不一致,所以要對發送或接收的字串做轉換。工具類別是 CL_ABAP_CHAR_UTILITIES

*&-------------------------------------------------------------*
*& 介面字串清洗:把控制字元與 BOM 一次清掉
*&-------------------------------------------------------------*
DATA(lv_json) = lv_raw_string.   "外部回傳的 JSON 字串

" 用 VALUE # 建一張「要清掉的字元」表,比連續八行 REPLACE 好維護
DATA(lt_bad) = VALUE string_table(
  ( CONV string( cl_abap_char_utilities=>horizontal_tab ) )        "09 TAB
  ( CONV string( cl_abap_char_utilities=>cr_lf ) )                 "0D0A 回車換行
  ( CONV string( cl_abap_char_utilities=>vertical_tab ) )          "0B 垂直定位
  ( CONV string( cl_abap_char_utilities=>newline ) )               "0A 換行
  ( CONV string( cl_abap_char_utilities=>form_feed ) )             "0C 換頁
  ( CONV string( cl_abap_char_utilities=>backspace ) )             "08 退格
  ( CONV string( cl_abap_char_utilities=>byte_order_mark_little ) ) "UTF-16LE BOM
  ( CONV string( cl_abap_char_utilities=>byte_order_mark_utf8 ) ) ). "UTF-8 BOM

LOOP AT lt_bad INTO DATA(lv_bad).
  REPLACE ALL OCCURRENCES OF lv_bad IN lv_json WITH ``.
ENDLOOP.

" 產生 CSV 時反過來:用 TAB 當分隔字元、CR_LF 當換行
DATA(lv_sep) = cl_abap_char_utilities=>horizontal_tab.
DATA(lv_eol) = cl_abap_char_utilities=>cr_lf.
DATA(lv_line) = |{ ls_row-matnr }{ lv_sep }{ ls_row-maktx }{ lv_sep }| &&
                |{ ls_row-labst NUMBER = RAW }{ lv_eol }|.

" JSON 轉換直接用標準類別,不要自己拼字串
DATA(lv_out) = /ui2/cl_json=>serialize( data        = lt_stock
                                        pretty_name = /ui2/cl_json=>pretty_mode-camel_case ).
/ui2/cl_json=>deserialize( EXPORTING json = lv_json
                           CHANGING  data = lt_stock ).
講義 10-3 的另一個實用技巧:由 VA03 的 Extras → Output 找到客製程式。步驟是 VA03 → Extras → Output → Header → Edit,看到 Output Type(例如 Z001)→ SPRO → SAP Reference IMG 找到訂單的 Output Type 設定 → 點 Z001 → 左欄中間選項就會顯示「哪一支程式的哪一個 FORM」→ 到 SE38 找那支程式 → 進去找名為 ENTRY 的 FORM。老師記錄的實際路徑:entry → processing → print data → upload_data_to_external_db。測試方式:VA02 直接改資料儲存(務必在測試區),就會重新觸發輸出、重下資料。

18. S/4HANA 的介面策略:BAPI 之後該用什麼

BAPI 仍然可用,但 S/4HANA 的官方優先順序已經改變。做新專案時建議按這個順序選:

優先序技術說明入口
1Released OData APISAP 官方釋出、有版本承諾(C1 Release Contract),例如 API_BUSINESS_PARTNERAPI_PURCHASEREQ_PROCESS_SRVAPI_PURCHASEORDER_PROCESS_SRVAPI_PRODUCT_SRVAPI_SALES_ORDER_SRVAPI_MATERIAL_STOCK_SRVSAP Business Accelerator Hub(原 API Business Hub)
2Released SOAP / Event非同步大量傳輸;SAP Event Mesh 可訂閱業務事件SOAMANAGER
3Released RFC / BAPI仍是效能最好的地端整合;看 BAPI 是否標為 Released(ADT 中的 API State = Use System-Internally / Released)BAPISE37
4CDS View / ABAP RAP自己開介面:CDS + Service Definition + Service Binding,一鍵產生 OData V2/V4ADT(Eclipse)
5AIF(Application Interface Framework)介面的監控、錯誤處理、重送、對帳框架;把介面訊息、Log、權限統一管理,業務單位可自行重送/AIF/ERR/AIF/CUST
SAP Cloud ConnectorBTP 雲端 App 要打地端 S/4 時的安全通道;逐一白名單資源與函數SCC 管理介面
S/4HANA 已被取代 / 不可再用的舊介面(重點記憶)
客戶/供應商 → BP:BAPI_CUSTOMER_CREATE、BAPI_VENDOR_CREATE、XD01/XK01/VD01/MK01 都不可用,改走 BP + CVI(CL_MD_BP_MAINTAIN / CMD_EI_API / VMD_EI_API / API_BUSINESS_PARTNER)。
BAPI_PO_CREATE → 改 BAPI_PO_CREATE1
SD 的 Rebate、SD Credit Management 舊 BAPI → 改 Settlement Management / FSCM。
MB01/MB1A/MB1B/MB1C/MB11 等 MB* 交易碼 → 改 MIGO 或 BAPI_GOODSMVT_CREATE;報表 MB5B 的資料來源已改 MATDOC
FI 的 BSIS/BSAS/BSID/BSAD/BSIK/BSAK 已變成相容視圖,效能查詢請改 ACDOCA
Classic RFC SDK 已停止支援,外部連接器請升級到 NW RFC SDK(JCo 3.x / NCo 3.x / pyrfc)。
⑦ 自建的 RFC 若用了被移除的舊表欄位(例如 MSEG 的某些欄位、MARD 的部分彙總欄位),升級時會直接語法錯誤,升級前要用 SAP Readiness CheckCustom Code Migration(ATC 的 S4HANA_READINESS 檢查)掃一遍。
判斷一支 BAPI 在 S/4HANA 到底能不能用,最快的方法:在 ADT(Eclipse)打開該 FM,看 Properties → API State;或在 SAP Business Accelerator Hub 搜尋它的名字,有列出來的才是 SAP 承諾維護的。沒有 Release 承諾的 FM,SAP 隨時可以改介面。

練習與自我檢核

  1. 介面選型:一個電商網站要「即時查 SAP 庫存」和「每天晚上把 5 萬筆訂單丟進 SAP」。這兩個需求各該用哪種 RFC 型態?為什麼?
    要點:即時查用 sRFC(同步、要立刻回答);大批丟單用 qRFC/bgRFC(保證只做一次且保序、目標忙碌時可排隊),或者用 IDoc/AIF。
  2. tRFC 沒動靜:程式寫了 CALL FUNCTION ... IN BACKGROUND TASK DESTINATION ...,SM58 看到一筆記錄卻永遠不執行,最可能是什麼?
    要點:忘了 COMMIT WORK。次要可能:目的地帳號鎖住、目標系統沒有那支函數。
  3. RFC-enabled FM 的三個硬性限制是什麼?如果我想用 class-based exception 當介面例外,該怎麼辦?
    要點:① 所有參數 pass by value;② 型別必須是 DDIC 型別;③ 不可有 Dynpro 互動(WRITE/CALL SCREEN)。class-based exception 不能當介面例外,改成內部 TRY/CATCH、外部用 BAPIRET2 表回傳。
  4. 判斷 BAPI 成功失敗:為什麼不能用 IF sy-subrc NE 0?講義第 08 節的 ZPO_CHANGE 就犯了這個錯,請改寫。
    要點:BAPI 呼叫後 sy-subrc 幾乎永遠是 0。要判 RETURN 表有沒有 TYPE = 'E' 或 'A'(用 line_exists( lt_return[ type = 'E' ] ))。
  5. COMMIT 的 WAIT:什麼情況一定要給 WAIT = 'X'?什麼情況可以省略?
    要點:COMMIT 後馬上要用產生的單號去 SELECT 或做下一個 BAPI,必須 WAIT='X';純批次寫入且後續不立即讀取,可省略以換取速度。
  6. X 結構:BAPI_MATERIAL_SAVEDATA 的 PLANTDATAX-PLANT 應該填 'X' 還是填工廠代碼?為什麼?
    要點:填工廠代碼。X 結構中 KEY 欄位填真值,只有非 KEY 的資料欄位才填 'X'。
  7. 找 BAPI:只知道交易碼 ME51N,請用老師教的方法找出建立請購單的 BAPI。
    要點:System → Status 雙擊 TCODE 找 Package → SE80 開 Package → Business Object Types → 找 BUS2105(Purchase Requisition)→ 看 Method → 對應 FM。或直接在 SE37 搜 BAPI_PR_*
  8. 兩支請購 BAPI:BAPI_PR_CREATE 與 BAPI_REQUISITION_CREATE 有何差異?該查哪一份 SAP Note?
    要點:前者有 X 結構、欄位較新,S/4 建議用;後者是舊版無 X 結構。Note 499627 - FAQ BAPIs for purchase requisitions
  9. 客製欄位:客戶要在採購單項目加一個 ZZ 欄位,並由外部系統帶進來。完整要做哪幾件事?
    要點:① SE11 Append CI_EKPODB/CI_EKPO;② Append 到 BAPI_TE_MEPOITEM / BAPI_TE_MEPOITEMX;③ 呼叫時填 EXTENSIONIN(BAPIPAREX 的 VALUEPART1~4);④ 實作 BAdI ME_BAPI_PO_CUST / ME_PROCESS_PO_CUST 把值搬進去。少了第 4 步不會有任何錯誤訊息。
  10. RFC 黑名單:外部呼叫回報「function module is blacklisted」,但 SE37 測試完全正常,怎麼處理?
    要點:執行報表 RS_RFC_BLACKLIST_CUSTOM 把該 FM 從黑名單剔除;另檢查 UCON(UCONCOCKPIT)與 S_RFC 權限。
  11. S/4 主資料:舊系統的介面用 BAPI_CUSTOMER_CREATE 建客戶,升級 S/4 之後要改成什麼?
    要點:改走 BP。優先 API_BUSINESS_PARTNER(OData),地端可用 CL_MD_BP_MAINTAIN=>maintain( ) 或 CMD_EI_API=>maintain_bapi( ),記得 BUT000/BUT100/BUT020 才是主資料真相,KNA1 是 CVI 同步出來的。
  12. 缺料表:要做一支「PP 缺料表」給 MSSQL,請設計 RFC 介面。
    要點:資料來源用 MD_STOCK_REQUIREMENTS_LIST_API(或 BAPI_MATERIAL_STOCK_REQ_LIST)取 MDEZX,篩出可用量為負的日期;BOM 反查用 CS_WHERE_USED_MAT(多階)。介面設計成 RFC-enabled FM,參數用 range 表讓外部列舉(不要用區間),一定要 ORDER BY,並回傳 BAPIRET2。
  13. 條件的坑:老師強調「條件用區間跟用列舉時答案會不同,用列舉的才正確」,這件事在設計 RFC 介面時要怎麼落實?
    要點:介面參數開成 SIGN/OPTION/LOW/HIGH 的 range 表,外部系統以 OPTION = 'EQ' 一筆一筆列舉;不要在介面內部自作聰明把使用者給的清單壓成 BT 區間。
  14. 不穩定的結果:一支介面函數每天跑出來的數字都不一樣,程式邏輯卻沒改,最可能的兩個原因是什麼?
    要點:① SELECT 沒有 ORDER BY 或用了 SELECT SINGLE,多筆符合時 HANA 回傳順序不定(正如成本評價同時有 H item category 與 E cost component 兩種角度,沒排序就一下抓到 H、一下抓到 E);② FOR ALL ENTRIES 的驅動表為空時會撈出全表。兩者都要在程式裡明確擋掉。

第 4-2 章 對話式程式 Module Pool:Screen / Tabstrip / Table Control / Custom Control

前面幾章寫的都是「跑一次、出一張報表」的程式;這一章要做的是像 SAP 標準交易一樣的畫面——ME21N、VA01、MM01 那種可以輸入、可以檢查、可以分頁籤、可以一列一列維護明細的對話式程式。你會學到 Module Pool 的程式骨架與 INCLUDE 命名慣例、SE51 Screen Painter 的每一種元素、PBO/PAI 流程邏輯、SE41 功能表與功能碼型別、畫面欄位的動態控制、Subscreen 與 Tabstrip、Table Control 的完整實作(含插入/刪除/捲動/全選/排序),以及把 ALV、文字編輯器、圖片、HTML 掛進 Custom Control。最後也會說清楚:在 S/4HANA + Fiori 的時代,哪些情況還該寫 Dialog,哪些情況該改走 RAP。

核心 TCODESE80SE38SE51SE41SE91SE93SE11SM30SMW0SM12OB58VA03ME21NSE24
關鍵物件CXTAB_CONTROL、TABSTRIP、SY-UCOMM/SY-DYNNR/SY-LOOPC/SY-STEPL、SCREEN 結構、VRM_VALUES、VRM_SET_VALUES、SCROLLING_IN_TABLE、ICON_CREATE、ICON、TADIR、CL_GUI_CUSTOM_CONTAINER、CL_GUI_DOCKING_CONTAINER、CL_GUI_SPLITTER_CONTAINER、CL_GUI_TEXTEDIT、CL_GUI_PICTURE、CL_GUI_HTML_VIEWER、CL_GUI_ALV_GRID、CL_SALV_TABLE、ENQUEUE_E_TABLE

1. Dialog 程式的定位:Type 1 與 Type M 的界線其實是模糊的

很多 SAP 系統本身的功能,也是用 ABAP 寫出來的——SE11、VA03、ME21N 都是。螢幕(Screen/Dynpro)是 ABAP 設計最重要的工作之一,SAP 的單據、主資料維護等業務功能都靠螢幕,一支程式可以包含多個螢幕

Dialog(對話)指的是使用者和程式之間任意形式的互動:輸入資料、選菜單項、按按鈕、單擊或雙擊清單條目。

比較項目TYPE 1(可執行程式 REPORT)TYPE M(Module Pool)
副程式執行PERFORM / 方法呼叫MODULE
顯示前程式AT SELECTION-SCREEN OUTPUTPBO(PROCESS BEFORE OUTPUT)
輸入後程式AT SELECTION-SCREENPAI(PROCESS AFTER INPUT)
預設螢幕編號1000(標準選擇畫面)自己指定,慣例 0100、0200…
Dynpro TypeSELECTION-DYNPRO(自動產生)NORMAL / SUBSCREEN / MODAL DIALOG BOX
首頁、末頁、捲動鍵自動生效必須自己指定(PF-STATUS + 捲動邏輯)
啟動方式SE38 / SA38 可直接執行一定要建 T-CODE(SE93)才能跑
適合報表編輯資料

老師的觀念轉換範例:同一件事,在 TYPE 1 裡用 SY-DYNNR 判斷是哪一個畫面,效果就等於 TYPE M 的 PBO/PAI 分流。

REPORT zdynpro.
" 由 TYPE 1 到 TYPE M 的觀念轉換:AT SELECTION-SCREEN OUTPUT ≒ PBO,AT SELECTION-SCREEN ≒ PAI

DATA gs_makt TYPE makt.               " 現代寫法:不用 TABLES,自己宣告工作區

" 螢幕 1000=標準選擇畫面
PARAMETERS p_matnr TYPE matnr.

" 自定義的第二個選擇畫面 1100
SELECTION-SCREEN BEGIN OF SCREEN 1100.
  PARAMETERS p_werks TYPE werks_d.
SELECTION-SCREEN END OF SCREEN 1100.

AT SELECTION-SCREEN OUTPUT.           " 相當於 PBO
  CASE sy-dynnr.
    WHEN '1000'.                      " 標準選擇畫面
    WHEN '1100'.                      " 自定義畫面
    WHEN OTHERS.
  ENDCASE.

AT SELECTION-SCREEN.                  " 相當於 PAI
  CASE sy-dynnr.
    WHEN '1000'.
    WHEN '1100'.
    WHEN OTHERS.
  ENDCASE.

START-OF-SELECTION.
  CALL SELECTION-SCREEN 1100 STARTING AT 10 5.   " 也能把第二個畫面做成彈窗
老師的結論:Type 1 與 Type M 的界線其實是模糊的。 Type M 一開啟就是 Dialog,但必須包裝成 T-CODE;而 Type 1 也能透過 CALL SCREEN 讓程式一開始就進 Dialog。因此實務上都用 TYPE 1(REPORT)寫,比較方便——本章所有範例都採用這個策略:REPORT + CALL SCREEN。

2. 程式骨架與 INCLUDE 命名慣例

Dialog 程式的程式碼會被拆成多支 INCLUDE,這不是規定而是SAP 的強慣例(SE80 建 Module Pool 時會自動照這個規則產生,Screen Painter 精靈也會照這個規則塞程式)。

INCLUDE 尾碼內容另一種常見寫法
ZXXX_TOP全域宣告:DATA、CONSTANTS、CONTROLS、TABLES、CLASS 定義ZXXX_TOP
ZXXX_O01PBO 的 MODULE … OUTPUTZXXX_PBO / ZXXX_O99
ZXXX_I01PAI 的 MODULE … INPUTZXXX_PAI / ZXXX_I99
ZXXX_F01FORM 副程式(Table Control 的標準 FORM 都放這)ZXXX_FRM / ZXXX_F99
ZXXX_E01事件區塊(AT SELECTION-SCREEN、START-OF-SELECTION 等)ZXXX_E99
ZXXX_C01本地類別(CLASS … IMPLEMENTATION,事件處理器)ZXXX_CL1
*&---------------------------------------------------------------------*
*& Report ZTC_MARD_MAINT   " 主程式:只放 INCLUDE 與程式進入點
*&---------------------------------------------------------------------*
REPORT ztc_mard_maint.

INCLUDE ztc_mard_maint_top.    " 宣告(DECLARE)
INCLUDE ztc_mard_maint_o01.    " PBO
INCLUDE ztc_mard_maint_i01.    " PAI
INCLUDE ztc_mard_maint_f01.    " FORM

START-OF-SELECTION.
  PERFORM load_data.           " 先撈資料
  CALL SCREEN 0100.            " 再進入 Dialog
程式屬性、開發類別(Package)、傳輸請求都登記在 TADIR。查一支程式屬於哪個 Package、誰建的、是不是原廠物件,直接 SE16N 看 TADIR(OBJECT = 'PROG'、OBJ_NAME = 程式名)最快。
INCLUDE 寫在主程式的前面或後面效果相同(編譯時會全部串起來),但 TOP 一定要在最前面,因為宣告要先被看到。
Dialog 程式的所有 MODULE 與 FORM 都跑在同一個程式的全域資料區上。螢幕上的欄位名稱必須「等於某個全域資料物件的名稱」,例如螢幕欄位 GS_MARD-MATNR 對應到全域 DATA gs_mard TYPE mard.。這是 Dialog 程式跟 OO 封裝觀念最衝突的地方,也是它逐漸被 RAP 取代的原因之一。

3. SE51 Screen Painter:螢幕屬性與元素

設計一個螢幕固定四個步驟:

  1. 屬性(Attributes)畫面設置常規螢幕屬性(編號、類型、Next Screen、大小)。
  2. 佈局編輯器(Layout)設計版面,把元素拖上去。
  3. 欄位清單(Element list)設置欄位屬性。
  4. 流邏輯編輯器(Flow logic)寫 PBO/PAI。

螢幕編號的規則

  • 可通過「程式名 + 螢幕編號」唯一識別一個螢幕。
  • 大於 9000 的螢幕編號為系統預留,不要用。
  • 1000~1010 保留給字典表的維護螢幕(SM30 產生的)以及可執行程式的標準選擇畫面。
  • 自己寫的建議從 0100 / 0200 / 2001 這類編號開始,並讓編號有規律(主畫面 0100、明細 0110、彈窗 0200…)。

螢幕類型(Dynpro Type)

類型用途限制
Normal一般全螢幕可有自己的 PF-STATUS、Titlebar
Subscreen被嵌到別的畫面裡(多頁籤的動態切換)不可有 PF-STATUS、Titlebar、SET/LEAVE SCREEN、AT EXIT-COMMAND
Modal Dialog Box彈出視窗必須用 CALL SCREEN … STARTING AT … ENDING AT … 呼叫
Selection screen選擇畫面(由宣告自動產生)不要手動改

Layout 上可以放的元素

元素說明常用屬性
Text Field只顯示不能輸入的文字標籤,也可以掛圖示Output only
Input/Output Field輸入/輸出欄位,Dialog 的主角Input/Output、Required、Possible entries、Dropdown
Checkbox核取方塊,勾選時值為 'X'要設 FctCode 才會即時觸發 PAI
Radio Button單選鈕,至少兩個,選取後按右鍵 Define group 定義成一群群組名 ≤ 4 碼
Pushbutton按鈕,靠 FctCode(Function code)觸發程式邏輯FctCode、FctType
Frame(Box)方框,純視覺分組可放標題
Subscreen Area子畫面的挖空區,執行時用 CALL SUBSCREEN 填入名稱不可重複
Tabstrip Control頁籤,每個頁籤掛一個 Subscreen建議用精靈產生
Table Control表格式明細維護區需搭配 CONTROLS … TYPE TABLEVIEW
Custom Control容器區,用來掛 ALV/編輯器/圖片/HTML名稱要傳給 CL_GUI_CUSTOM_CONTAINER
Status Icon顯示圖示的欄位(紅綠燈)值來自 ICON
任何在 Screen 上加入的元素(Element),都會出現在 Element list 頁籤。物件該填的屬性填完之後,Layout 上的底色會消失——底色沒消,代表還有必填屬性沒設好。(依講義步驟整理)

螢幕欄位可控制的四組屬性群

PBO 中用 LOOP AT SCREEN 可以動態改的屬性,共有四個群組欄位可用:

群組欄位可控屬性
GROUP1(修改群組)INPUT 是否可輸入0 / 1
GROUP2OUTPUT 是否輸出0 / 1
GROUP3REQUIRED 必輸與否0 / 1(S/4 亦支援 2=必輸但不顯示打勾)
GROUP4INVISIBLE 顯示與否(打星號,密碼用)0 / 1
ACTIVE 啟動與否(0 = 整個消失且不參與 PAI)0 / 1
INTENSIFIED 強化顏色0 / 1
LENGTH 控制顯示長度數字

游標定位

" 把游標移到指定欄位(PBO 中使用)
SET CURSOR FIELD 'GS_SDYN_CONN-CONNID'.
SET CURSOR FIELD 'GS_SDYN_CONN-CONNID' OFFSET 3.
SET CURSOR LINE 5.                    " Table Control 中:移到第 5 個可見列

" 反向:讀出使用者現在把游標放在哪(PAI 中使用,做雙擊 / Hotspot 很好用)
GET CURSOR FIELD DATA(lv_fname) VALUE DATA(lv_value) LINE DATA(lv_line).
IF sy-subrc = 0.
  MESSAGE |游標在 { lv_fname },值 = { lv_value },第 { lv_line } 列| TYPE 'S'.
ENDIF.
截至 2022/05/16,SCREEN LAYOUT(圖形版 Screen Painter)在 macOS 的 SAP GUI for Java 無法執行,只能用字母版(Alphanumeric)編輯器或改用 Windows GUI/ADT。老師課堂上特別提醒過這一點。

4. 第一支 Dialog:Flow Logic、OK_CODE 與 CALL SCREEN

第 41 個練習:從螢幕設計到實作。目標:在螢幕上貼一個「NCU SAP!」的 Text 和一個按鈕,按下按鈕就退出整個程式。

  1. 建立螢幕:1. 定編號(2001)、2. 設 NORMAL 屬性、3. 設 Next Screen 屬性。
  2. 在 Layout 貼上 Text Field「NCU SAP!」與一個 Pushbutton,Pushbutton 的 FctCode = 'EXIT'
  3. 在 Element list 加一個欄位 OK_CODE(型別 SY-UCOMM),這是接收功能碼的出口;同名變數也要在主程式宣告。
  4. Flow logic 裡把 MODULE USER_COMMAND_2001 的註解拿掉,雙擊建立模組。
  5. 主程式用 CALL SCREEN 2001. 進入畫面。
*&---------------------------------------------------------------------*
*& Report Z202202_41   " 第 41 練習:最小的 Dialog 程式
*&---------------------------------------------------------------------*
REPORT z202202_41.

DATA ok_code TYPE sy-ucomm.       " 螢幕上必須有一個同名的 OK_CODE 欄位
DATA gv_save_ok TYPE sy-ucomm.    " 慣例:先備份再清空,避免殘留

CALL SCREEN 2001.

*----------------------------------------------------------------------*
MODULE user_command_2001 INPUT.
  gv_save_ok = ok_code.
  CLEAR ok_code.                  " 一定要清,否則下一輪還會帶著舊值

  CASE gv_save_ok.
    WHEN 'EXIT'.
      MESSAGE '您可離開了' TYPE 'I'.
      LEAVE PROGRAM.
      " LEAVE TO SCREEN 9999. 也是一種寫法,但要看目前螢幕是否設定了 Next Screen
    WHEN OTHERS.
  ENDCASE.
ENDMODULE.

螢幕的 Flow logic 長這樣(雙引號的註解列是 SE51 預設產生的,要用哪一段就把星號拿掉):

PROCESS BEFORE OUTPUT.
  MODULE status_2001.             " 設 PF-STATUS 與 Titlebar
  MODULE modify_screen.           " 動態控制欄位屬性
  MODULE initial.                 " 載入資料

PROCESS AFTER INPUT.
  MODULE cancel AT EXIT-COMMAND.  " ★ 一定放最前面,見第 5 節
  MODULE user_command_2001.
口訣:PBO 專職「螢幕的控制」(畫面長什麼樣);PAI 專職「使用者輸入後的資料處理」(順序與邏輯)。 老師反覆強調這兩句,寫錯地方是初學者最常見的 bug 來源。
若程式很簡單,PAI 的 MODULE 可以直接寫在主程式裡,不必拆 INCLUDE;程式一長就務必拆。建議建立一支空程式,照著教材一步一步做一遍,才能理解每個章節要傳達的精神。

5. SE41 Menu Painter:PF-STATUS、Titlebar 與功能碼型別

Dialog 畫面的上方工具列、功能表、以及 F 鍵配置,都由 GUI Status(PF-STATUS)決定;視窗標題由 GUI Title(Titlebar)決定。兩者都可以在 SE80 的程式樹狀圖下建立,也可以用 SE41 單獨修改或複製。

*&---------------------------------------------------------------------*
*& Module STATUS_2001 OUTPUT     " PBO:畫面一出現就設定功能表與標題
*&---------------------------------------------------------------------*
MODULE status_2001 OUTPUT.
  SET PF-STATUS 'STATUS'.
  SET TITLEBAR  'TITLE'.
ENDMODULE.

" 進階:可以隱藏某些按鈕
" SET PF-STATUS 'STATUS' EXCLUDING VALUE #( ( 'DELE' ) ( 'INSR' ) ).
" 標題帶參數:GUI Title 內容寫成「銷售訂單 &1 明細」
" SET TITLEBAR 'TITLE' WITH lv_vbeln.

Status Type(狀態類型)

類型用在什麼螢幕
Normal screenDynpro Type = Normal 的全螢幕
Dialog boxDynpro Type = Modal Dialog Box 的彈出視窗(工具列會自動縮簡)
Context menu右鍵選單

Function Type(功能碼型別)——最容易被忽略卻最關鍵

Function Type行為用途
(空白)一般功能碼。會完整跑完 PAI(包含所有 FIELD 檢查)才進到 USER_COMMAND存檔、執行、查詢
EExit command。跳過所有欄位檢查,直接執行 MODULE … AT EXIT-COMMANDBACK / EXIT / CANCEL
S系統功能(不進使用者程式)系統保留
T直接呼叫另一個 T-CODE跳轉
P本地處理(不觸發 PAI 到伺服器)Tabstrip 本地捲動、Table Control 欄寬調整
最經典的坑:使用者填錯資料被 E 類訊息擋住,結果連「返回」都按不出去,只能砍掉 SAP GUI。 解法就是把 BACK/EXIT/CANCEL 的 Function Type 設成 E,並在 PAI 最前面MODULE cancel AT EXIT-COMMAND.。講義原文明確寫著:「指定為 E 類的這一行應該移到前面,否則檢查不過時會無法跳離程式。」
*&---------------------------------------------------------------------*
*& Module CANCEL INPUT   " 掛在 AT EXIT-COMMAND,跳過所有欄位檢查
*&---------------------------------------------------------------------*
MODULE cancel INPUT.
  CASE ok_code.
    WHEN 'BACK'.   LEAVE TO SCREEN 0.   " 回上一層畫面
    WHEN 'EXIT'.   LEAVE PROGRAM.       " 離開整個程式
    WHEN 'CANCEL'. LEAVE TO SCREEN 0.
  ENDCASE.
ENDMODULE.

接續第 41 練習,加上功能表之後的完整程式:

*&---------------------------------------------------------------------*
*& Report Z202202_41   " 加上 PF-STATUS / TITLEBAR / 功能表的版本
*&---------------------------------------------------------------------*
REPORT z202202_41.

DATA ok_code TYPE sy-ucomm.

CALL SCREEN 2001.

*----------------------------------------------------------------------*
MODULE status_2001 OUTPUT.
  SET PF-STATUS 'STATUS'.
  SET TITLEBAR  'TITLE'.
ENDMODULE.

*----------------------------------------------------------------------*
MODULE user_command_2001 INPUT.
  DATA(lv_ok) = ok_code.
  CLEAR ok_code.

  CASE lv_ok.
    WHEN 'MENU11'. MESSAGE '您點了 MENU11' TYPE 'I'.
    WHEN 'MENU12'. MESSAGE '您點了 MENU12' TYPE 'I'.
    WHEN 'MENU21'. MESSAGE '您點了 MENU21' TYPE 'I'.
    WHEN 'MENU22'. MESSAGE '您點了 MENU22' TYPE 'I'.
    WHEN 'ZCAS'.   MESSAGE '您點了 ZCAS'   TYPE 'E'.   " E 類會停在本畫面
    WHEN 'EXIT'.
      MESSAGE '您可離開了' TYPE 'I'.
      LEAVE PROGRAM.
    WHEN OTHERS.
  ENDCASE.
ENDMODULE.

*----------------------------------------------------------------------*
MODULE cancel INPUT.               " Function Type = E
  LEAVE PROGRAM.
  " 若寫 LEAVE TO SCREEN 0 則表示回上一頁
ENDMODULE.
重要觀念:若同時有多個 PF-STATUS,而 Function Code 同名時,代表它們會執行同一段程式(因為 CASE 判斷的是 ok_code 的值)。這是刻意設計——顯示模式與修改模式用兩個 STATUS,共用同一批功能碼。
系統保留鍵:F1(說明)、F2(形同雙擊)、F4(可能輸入值)、F9Shift+F2Shift+F4Shift+F5,客製時勿佔用。另外選 Fiori 主題時,Function key 那一排(save / back / exit / cancel …)可能不顯示,只能靠鍵盤或功能表,設計時要留意。

6. 多螢幕的控制:SET SCREEN / LEAVE TO SCREEN / CALL SCREEN

螢幕的執行順序有兩種決定方式:

  • 靜態指定:在螢幕屬性的 Next Screen 欄位填下一個螢幕編號。填 0 表示「結束這一層、回到呼叫者」。
  • 動態指定:在 PAI 用指令改變流程。
指令行為備註
SET SCREEN n.把「下一個螢幕」暫時改成 n,但不會立刻跳,會把目前 PAI 跑完常與 LEAVE SCREEN 併用
LEAVE SCREEN.立刻結束目前螢幕,跳到 Next Screen
LEAVE TO SCREEN n.= SET SCREEN n + LEAVE SCREEN最常用
LEAVE TO SCREEN 0.結束目前這一層,回到呼叫它的畫面(沒有上層就結束程式)BACK 鍵的標準寫法
CALL SCREEN n.堆疊式呼叫,n 結束後會回到原畫面的下一行做彈窗、做明細畫面
LEAVE PROGRAM.離開整支程式EXIT 鍵
LEAVE TO TRANSACTION 'XX'.跳到別的交易不回來
" ── 方式一:設定下一個 SCREEN ────────────────────────────────
SET SCREEN 300.
LEAVE SCREEN.
" 等價於: LEAVE TO SCREEN 300.

" ── 方式二:呼叫 SCREEN,結束後仍回到原 SCREEN ─────────────
CALL SCREEN 101.                     " 呼叫端
" …… 101 結束後,程式回到這一行的下一行

" 被呼叫的 101 要這樣結束:
"   SET SCREEN 0.
"   LEAVE SCREEN.          (或直接 LEAVE TO SCREEN 0.)

" ── 方式三:彈出視窗(螢幕屬性必須是 Modal Dialog Box)────────
CALL SCREEN 0200
  STARTING AT 10 5      " 左上角:column 10, row 5
  ENDING   AT 80 20.    " 右下角:column 80, row 20
彈出視窗必須同時滿足三個條件才會正常:(1) 螢幕屬性 Dynpro Type = Modal Dialog Box;(2) 呼叫時給 STARTING AT / ENDING AT;(3) 該畫面用的 GUI Status 的 Status Type = Dialog box。少一個就會變成怪怪的全螢幕或跑版。
不想自己畫彈窗時,先找標準 FM:POPUP_TO_CONFIRM(是/否)、POPUP_TO_DECIDEPOPUP_TO_INFORMPOPUP_WITH_TABLE_DISPLAY(選一列)、POPUP_GET_VALUES(臨時輸入欄位)。90% 的彈窗需求不必自己建 Screen。

7. 欄位動態控制:LOOP AT SCREEN 與 Group

第 03 節:單選按鈕 RADIO、框 BOX、輸入框的運用。 Radio 要兩個以上,貼好之後全選按右鍵 Define group 定成一群。把 FIELD2、FIELD3 的 Element list 中 Group1 設為 'SEL',就可以在 PBO 一次控制整群。

*&---------------------------------------------------------------------*
*& Module MODIFY_SCREEN OUTPUT   " PBO:動態控制欄位屬性
*&---------------------------------------------------------------------*
MODULE modify_screen OUTPUT.
  LOOP AT SCREEN.
    IF screen-group1 = 'SEL'.        " GROUP1 = SEL 的整群不能輸入(FIELD2、FIELD3)
      screen-input = 0.
    ENDIF.
    IF screen-name = 'FIELD1'.       " FIELD1 設成必輸
      screen-required = 1.
    ENDIF.
    MODIFY SCREEN.                   " ★ 一定要 MODIFY,屬性才會生效
  ENDLOOP.
ENDMODULE.
三個必記的坑:(1) 改完屬性一定要 MODIFY SCREEN.,忘了寫完全沒效果;(2) screen-namescreen-group1 的比較值一律用大寫(Modif ID 在判斷時一定要大寫);(3) Modif ID 最多 3 碼,Screen Painter 的 Group1~Group4 各 3 碼

在選擇畫面(Type 1)用 MODIF ID 也是同一套機制,老師的兩個示範:

REPORT zradio.
" 用 MODIF ID 分兩群,一群反白、一群不反白
PARAMETERS: test1(10) TYPE c MODIF ID sc1,
            test2(10) TYPE c MODIF ID sc2,
            test3(10) TYPE c MODIF ID sc1,
            test4(10) TYPE c MODIF ID sc2.

AT SELECTION-SCREEN OUTPUT.
  LOOP AT SCREEN.
    IF screen-group1 = 'SC1'.        " 判斷值必須大寫
      screen-intensified = '0'.
      MODIFY SCREEN.
      CONTINUE.
    ENDIF.
    IF screen-group1 = 'SC2'.
      screen-intensified = '1'.
      MODIFY SCREEN.
    ENDIF.
  ENDLOOP.
*&---------------------------------------------------------------------*
*& Report ZMODIFYID   " MODIF ID 的完整運用:選什麼、才開放什麼欄位
*&---------------------------------------------------------------------*
REPORT zmodifyid.

" 第一個框框:選擇物料類型
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001.
  SELECTION-SCREEN COMMENT 1(20) txt01.
  PARAMETERS:
    pa TYPE c RADIOBUTTON GROUP g1 DEFAULT 'X' USER-COMMAND flag,
    " USER-COMMAND flag 表示這組元件會觸發事件(沒有它,畫面不會即時重繪)
    pb TYPE c RADIOBUTTON GROUP g1.
SELECTION-SCREEN END OF BLOCK b1.

" 第二個框框:自製 → 開放物料編號 / 物料描述
SELECTION-SCREEN BEGIN OF BLOCK b2 WITH FRAME TITLE TEXT-002.
  SELECTION-SCREEN BEGIN OF LINE.
    SELECTION-SCREEN COMMENT 1(8) txt02 FOR FIELD p1.
    PARAMETERS p1 TYPE c LENGTH 20 MODIF ID in2.   " Modif ID 最多三碼
  SELECTION-SCREEN END OF LINE.
  SELECTION-SCREEN SKIP 1.
  SELECTION-SCREEN BEGIN OF LINE.
    SELECTION-SCREEN COMMENT 1(8) txt03.
    PARAMETERS p2 TYPE c LENGTH 30 MODIF ID in2.
  SELECTION-SCREEN END OF LINE.
SELECTION-SCREEN END OF BLOCK b2.

" 第三個框框:採購 → 開放採購單號
SELECTION-SCREEN BEGIN OF BLOCK b3 WITH FRAME TITLE TEXT-003.
  SELECTION-SCREEN BEGIN OF LINE.
    SELECTION-SCREEN COMMENT 1(10) txt04 FOR FIELD p3.
    PARAMETERS p3 TYPE c LENGTH 20 MODIF ID in3.
  SELECTION-SCREEN END OF LINE.
SELECTION-SCREEN END OF BLOCK b3.

" 第四個框框:勾了才「出現」備註(ACTIVE = 0 是整個消失)
SELECTION-SCREEN BEGIN OF BLOCK b4 WITH FRAME TITLE TEXT-004.
  PARAMETERS pc AS CHECKBOX USER-COMMAND mark.
  SELECTION-SCREEN BEGIN OF LINE.
    SELECTION-SCREEN COMMENT 1(10) txt05.
    PARAMETERS p4(20) TYPE c MODIF ID in4.
  SELECTION-SCREEN END OF LINE.
SELECTION-SCREEN END OF BLOCK b4.

INITIALIZATION.
  txt01 = '請選擇物料類型:'.
  txt02 = '物料編號:'.
  txt03 = '物料描述:'.
  txt04 = '採購單號:'.
  txt05 = '備註說明:'.

AT SELECTION-SCREEN OUTPUT.
  LOOP AT SCREEN.                    " SCREEN 是系統結構,可在 SE11 查看
    IF screen-group1 = 'IN2' AND pa <> 'X'.
      screen-input = 0.              " 1 = 可輸入,0 = 不可輸入(變灰)
      CLEAR: p1, p2.
    ENDIF.
    IF screen-group1 = 'IN3' AND pb <> 'X'.
      screen-input = 0.
      CLEAR p3.
    ENDIF.
    IF screen-group1 = 'IN4' AND pc <> 'X'.
      screen-active = 0.             " ACTIVE = 0:整個欄位消失
      CLEAR p4.
    ENDIF.
    MODIFY SCREEN.                   " 確認修改後的屬性
  ENDLOOP.

AT SELECTION-SCREEN.                 " 提交錯誤訊息
  CASE sy-ucomm.
    WHEN 'ONLI'.                     " 點執行時
      IF pa = 'X' AND ( p1 IS INITIAL OR p2 IS INITIAL ).
        MESSAGE '請填寫相關物料信息!' TYPE 'E'.
      ELSEIF pb = 'X' AND p3 IS INITIAL.
        MESSAGE '請填寫採購單號!' TYPE 'E'.
      ELSEIF pc = 'X' AND p4 IS INITIAL.
        MESSAGE '請填寫備註說明!' TYPE 'E'.
      ENDIF.
  ENDCASE.

START-OF-SELECTION.
  " 現代寫法:不用 WRITE 排版,直接丟 CL_SALV_TABLE
  DATA(lt_out) = VALUE ztt_material_info(
    ( matnr = COND #( WHEN pa = 'X' THEN p1 ELSE 'WG0000000001' )
      maktx = COND #( WHEN pa = 'X' THEN p2 ELSE '採購物料' )
      ebeln = COND #( WHEN pb = 'X' THEN p3 ELSE space )
      remark = p4 ) ).

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = lt_out ).
      lo_alv->get_functions( )->set_all( ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->display( ).
    CATCH cx_salv_msg INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.
SCREEN-INPUT = 0 與 SCREEN-ACTIVE = 0 的差別:INPUT = 0 只是變灰(欄位還在、值還會傳回 PAI);ACTIVE = 0 是整個欄位消失且不參與 PAI 傳輸。要「藏起來」用 ACTIVE,要「唯讀顯示」用 INPUT。另外被 INPUT = 0 灰掉的欄位,使用者改不了,但程式改得了——安全檢查不能只靠畫面。

8. 在畫面上顯示與維護資料表

第 04 節。課程最開始建立了自己的資料表 Z00_VBAK,現在用 Screen 讓使用者維護它。從 Screen Painter 的「Get from Dictionary」把欄位拉上畫面時,欄位名稱會是 Z00_VBAK-XXXX,所以程式裡要有一個同名的全域結構。

寫法一:TABLES(講義原始寫法)

" 講義原寫法:TABLES 會產生一個同名全域工作區,剛好接上畫面欄位名
TABLES z00_vbak.

MODULE initial OUTPUT.
  SELECT SINGLE * FROM z00_vbak INTO @z00_vbak.  " 直接載入 TABLES 工作區
ENDMODULE.
SPEC 明訂 TABLES 屬於應淘汰的語法,在報表程式中一律不要用。但在 Dialog 程式中它有一個「無法完全避開」的用途:快速產生一個與資料表同名的全域工作區,好讓 Screen Painter 拉出來的欄位名對得上。現代做法是:自己宣告一個結構,並在 Screen Painter 中把欄位名改成該結構的名稱

寫法二:自宣告工作區(現代寫法,建議)

*&---------------------------------------------------------------------*
*& Report Z202202_41   " 用自宣告的工作區承接畫面欄位
*&---------------------------------------------------------------------*
REPORT z202202_41.

DATA gs_vbak TYPE z00_vbak.          " 畫面欄位命名為 GS_VBAK-VBELN … 即可
DATA gt_vbak TYPE STANDARD TABLE OF z00_vbak WITH EMPTY KEY.

MODULE initial OUTPUT.
  " 只在第一次進畫面時載入,避免每次 PBO 都打 DB
  IF gs_vbak IS INITIAL.
    SELECT SINGLE vbeln, erdat, ernam, kunnr, netwr, waerk
      FROM z00_vbak
      INTO CORRESPONDING FIELDS OF @gs_vbak.
  ENDIF.
ENDMODULE.
兩種寫法執行結果一樣。差別在於:用 TABLES 的工作區,Screen Painter 會自動帶出欄位抬頭(Label);用自宣告的變數則不會自動顯示抬頭,要自己貼 Text Field 或在 Element list 手動指定 Dict. 參考。
老師的重要提醒:對筆數比較大的資料表,直接把整張表放在 Screen 上是不妥的做法——會影響效能,也會增加 LOCK 的機率。正確做法是「先用選擇畫面過濾 → 撈進內表 → 用 Table Control 分頁顯示」。

鎖定與資料一致性

" 進入編輯模式前一定要鎖,離開時一定要解
" 前提:SE11 建立 Lock Object EZ00_VBAK(表 Z00_VBAK,Mode E=獨佔寫入鎖)
CALL FUNCTION 'ENQUEUE_EZ00_VBAK'
  EXPORTING
    mode_z00_vbak  = 'E'
    mandt          = sy-mandt
    vbeln          = gs_vbak-vbeln
    _scope         = '2'          " 2=鎖到 COMMIT WORK 為止(Dialog 最常用)
    _wait          = abap_true
  EXCEPTIONS
    foreign_lock   = 1
    system_failure = 2
    OTHERS         = 3.

IF sy-subrc <> 0.
  " sy-msgv1 會帶出「是誰鎖住的」
  MESSAGE |單據 { gs_vbak-vbeln } 正被 { sy-msgv1 } 編輯中| TYPE 'E'.
ENDIF.

" …… 使用者編輯、按下存檔 ……
MODIFY z00_vbak FROM TABLE @gt_vbak.
IF sy-subrc = 0.
  COMMIT WORK AND WAIT.
  MESSAGE '存檔成功' TYPE 'S'.
ELSE.
  ROLLBACK WORK.
  MESSAGE '存檔失敗' TYPE 'E'.
ENDIF.

CALL FUNCTION 'DEQUEUE_EZ00_VBAK'    " 離開畫面時解鎖
  EXPORTING
    mode_z00_vbak = 'E'
    mandt         = sy-mandt
    vbeln         = gs_vbak-vbeln.
鎖住不放是 Dialog 程式最常被抱怨的問題。三個保命習慣:(1) 只在真的要改的時候才鎖,顯示模式不要鎖;(2) 在 AT EXIT-COMMAND 的模組裡一定要 DEQUEUE;(3) SM12 是查/砍殘留鎖的地方,上線後一定要教會 key user 用。

9. 欄位合理性檢查:FIELD、CHAIN、ON INPUT / ON REQUEST

第 05 節。PAI 裡最重要的兩個關鍵字是 FIELDCHAIN。它們決定了「檢查失敗時,哪些欄位會保持可輸入狀態」——這是 Dialog 程式比選擇畫面強大的地方。

語法意義
FIELD f MODULE m.一律檢查。檢查失敗(E 訊息)時,只有 f 這個欄位保持可輸入
FIELD f MODULE m ON INPUT.欄位不為初始值時才觸發
FIELD f MODULE m ON REQUEST.欄位被改過(值與上次不同)時才觸發
CHAIN … ENDCHAIN.把多個欄位綁成一組;檢查失敗時整組都保持可輸入
MODULE m ON CHAIN-INPUT.CHAIN 中任一欄位不為空就觸發
MODULE m ON CHAIN-REQUEST.CHAIN 中任一欄位被改過就觸發(Table Control 的標準寫法)
MODULE m AT EXIT-COMMAND.只有 Function Type = E 的功能碼會觸發,跳過所有檢查
FIELD f VALUES (…).不寫程式的固定值檢查
FIELD f MODULE m AT CURSOR-SELECTION.游標停在該欄位並按 F2/雙擊時觸發
*----------------------------------------------------------------------*
*  Flow logic:PAI 的各種檢查寫法
*----------------------------------------------------------------------*
PROCESS AFTER INPUT.

  MODULE cancel AT EXIT-COMMAND.     " ★ E 類必須放最前面

  " 單個欄位檢查
  FIELD fld1 MODULE check1 ON REQUEST.   " 有修改才檢查
  FIELD fld2 MODULE check2 ON INPUT.     " 不為空就檢查
  FIELD fld3 MODULE check3.              " 一律檢查

  " 單個欄位、多個 MODULE
  FIELD fld1 MODULE check1a
             MODULE check1b.

  " 多欄位一起檢查:失敗時 FLD1~FLD4 都保持可輸入
  CHAIN.
    FIELD fld1.
    FIELD fld2, fld3, fld4.
    MODULE chk_all_1.
    MODULE chk_all_2.
  ENDCHAIN.

  " 只在「有輸入」時才檢查,避免不必要的檢查
  CHAIN.
    FIELD fld1.
    FIELD fld2, fld3, fld4.
    MODULE chk_all_1 ON CHAIN-INPUT.
    MODULE chk_all_2.
  ENDCHAIN.

  " 只在「有改變」時才檢查
  CHAIN.
    FIELD fld1.
    FIELD fld2, fld3, fld4.
    MODULE chk_all_1 ON CHAIN-REQUEST.
    MODULE chk_all_2.
  ENDCHAIN.

  MODULE user_command_2001.
*&---------------------------------------------------------------------*
*& 三個檢查模組(配合上面的 flow logic)
*&---------------------------------------------------------------------*
MODULE check1 INPUT.
  CASE fld1.
    WHEN 'NG'.  MESSAGE e005(ymess) WITH 'FLD1 ERROR'.     " E:停在本畫面
    WHEN 'NON'. MESSAGE i005(ymess) WITH 'FLD1 WARNING'.   " I:彈窗,按確定續行
    WHEN 'OK'.  MESSAGE s005(ymess) WITH 'FLD1 OK'.        " S:狀態列,續行
  ENDCASE.
ENDMODULE.

MODULE check2 INPUT.
  CASE fld2.
    WHEN 'NG'.  MESSAGE e005(ymess) WITH 'FLD2 ERROR'.
    WHEN 'NON'. MESSAGE i005(ymess) WITH 'FLD2 WARNING'.
    WHEN 'OK'.  MESSAGE s005(ymess) WITH 'FLD2 OK'.
  ENDCASE.
ENDMODULE.

MODULE check3 INPUT.
  MESSAGE i005(ymess) WITH 'FLD3 一律警告'.
ENDMODULE.

SAP 標準的四層輸入檢查,前三層不必寫程式,設定好就自動生效:

層次檢查內容設定在哪
Mandatory fields check是否必輸Element list 的 Required 屬性 / screen-required
Field format check欄位型態(日期、數值)Data Element 的型別
Fixed ValuesDomain 值域SE11 Domain 的 Value Range
Foreign key check外來鍵值SE11 欄位的 Foreign Key
(第五層)自訂邏輯跨欄位、跨表的商業規則PAI 的 FIELD / CHAIN MODULE
補充:若元素用 Checkbox,勾選表示值為 'X';沒勾就是空白。判斷一律寫 IF gs_x-flag = abap_true.

10. 訊息控制 SE91 與 Dialog 中的訊息行為

第 06 節。自定義訊息類別(Message Class)要以 YZ 起頭,用 SE91 建立,存在自己的 Package(課程是 ZP_CASPER)。

" 兩種等價的寫法
MESSAGE e005(ymess) WITH 'FLD1 ERROR'.

MESSAGE ID 'YMESS' TYPE 'E' NUMBER 005
        DISPLAY LIKE 'I'                  " 只改「外觀」,行為仍是 E
        WITH 'FLD1 ERROR'.

" 動態組訊息(訊息編號放變數時只能用這種)
DATA(lv_msgno) = '005'.
MESSAGE ID 'YMESS' TYPE 'E' NUMBER lv_msgno WITH lv_matnr.

" 把訊息接起來不要跳出(做 log 用)
MESSAGE e005(ymess) WITH 'FLD1 ERROR' INTO DATA(lv_msg).
APPEND VALUE #( type = 'E' message = lv_msg ) TO gt_log.
TYPE意義在 Dialog(PAI)中的行為
SSuccess 成功訊息顯示在下一個畫面的狀態列。假設由 Screen 100 跳到 200,訊息會等到 200 開啟時才出現
IInformation 訊息即時彈出對話方塊,按確定後繼續原流程
WWarning 警告顯示在狀態列,欄位保持可輸入,按 Enter 可以繼續
EError 錯誤停在原畫面(Screen 100)且不重跑 PBO,只有 FIELD/CHAIN 涵蓋的欄位可再輸入
AAbend 異常終止終止交易,回到 SAP Easy Access(Dialog 少用)
XExit產生 short dump(Dialog 少用)
E 訊息不重跑 PBO是 Dialog 最容易誤判的行為:你在 PBO 裡設定的欄位屬性、下拉清單內容不會被重新計算,畫面停留在使用者剛剛看到的狀態。所以「E 訊息之後要順便改畫面」的需求做不到,要改用 W 或改用 SET SCREEN 重進。
前景/背景要區分訊息:背景(Batch)執行時 E/A 訊息會直接讓 Job 失敗,I/S 訊息只寫進 Job Log。程式要能前景也能背景執行的話,一定要用 IF sy-batch = abap_true. 分流,否則背景會在 E 訊息處掛掉。DISPLAY LIKE 'I' 只決定提示訊息的抬頭風格,不改變 E 的行為。

11. 下拉選單(Listbox)

第 07 節。在 Screen Painter 的欄位屬性中,把 Dropdown 設為 Listbox with key(顯示 key + 文字)或 Listbox without key,然後在 PBO 用 FM VRM_SET_VALUES 灌入清單值。

" ── TOP:宣告 ─────────────────────────────────────────────
TYPE-POOLS vrm.                   " S/4 仍相容;新程式可直接用型別 VRM_VALUES
DATA gv_listbox(4) TYPE c.        " 畫面上的下拉欄位(名稱要與畫面元素同名)
DATA gv_fname TYPE vrm_id.
DATA gt_vva   TYPE vrm_values.
*&---------------------------------------------------------------------*
*& Module LIST_VALUES OUTPUT   " PBO:建立下拉清單內容(講義原始做法)
*&---------------------------------------------------------------------*
MODULE list_values OUTPUT.
  gv_fname = 'GV_LISTBOX'.        " 一定要是畫面欄位名(大寫)

  gt_vva = VALUE #( ( key = 'TWD' text = '新台幣' )
                    ( key = 'USD' text = '美金'   ) ).

  CALL FUNCTION 'VRM_SET_VALUES'
    EXPORTING
      id              = gv_fname
      values          = gt_vva
    EXCEPTIONS
      id_illegal_name = 1
      OTHERS          = 2.
  IF sy-subrc <> 0.
    MESSAGE '下拉清單建立失敗' TYPE 'S' DISPLAY LIKE 'E'.
  ENDIF.
ENDMODULE.

更實用的做法:直接把幣別表 TCURC(或自建的 Z00_CURR)撈進來當清單。

MODULE list_values OUTPUT.
  IF gt_vva IS INITIAL.            " 只建一次,別每次 PBO 都打 DB
    SELECT waers AS key, waers AS text
      FROM tcurc
      INTO TABLE @DATA(lt_curr)
      UP TO 200 ROWS
      ORDER BY waers.

    gt_vva = CORRESPONDING #( lt_curr ).

    CALL FUNCTION 'VRM_SET_VALUES'
      EXPORTING id = 'GV_LISTBOX' values = gt_vva
      EXCEPTIONS OTHERS = 2.
  ENDIF.
ENDMODULE.
老師的忠告:下拉式(Dropdown)選單的屬性,只有在資料筆數不大時才使用。 筆數一多(幾百筆以上)不但畫面卡,使用者也找不到——那時應該改用 Search Help(F4),它有搜尋、有分頁、有多欄條件。

12. Subscreen 子畫面

第 08 節、第 19 節。Dynpro(Dynamic Program)= 一個螢幕 + 它的 flow logic + controls。Subscreen 讓你把畫面切成可替換的區塊,是 Tabstrip 與 SAP 標準增強(Screen Exit)的基礎。

作法

  1. 在主畫面 Layout 貼上 Subscreen Area(命名例如 SUB1)。
  2. 新增螢幕 2002,Dynpro Type 一定要選 Subscreen;貼上要顯示的元素後啟動。
  3. 同法建立第二個子螢幕 2003。
  4. 主程式宣告一個變數決定要顯示哪一個:DATA gv_screenid TYPE sy-dynnr VALUE '2002'.
  5. 主畫面 PBO 末尾加上 CALL SUBSCREEN;主畫面 PAI 對應加上 CALL SUBSCREEN(不帶 INCLUDING)。
*----------------------------------------------------------------------*
*  主畫面 2001 的 flow logic
*----------------------------------------------------------------------*
PROCESS BEFORE OUTPUT.
  MODULE status_2001.
  MODULE modify_screen.
  MODULE initial.
  MODULE list_values.
  " SY-REPID 表示當前主程式;gv_screenid 決定顯示哪一個子畫面
  CALL SUBSCREEN sub1 INCLUDING sy-repid gv_screenid.

PROCESS AFTER INPUT.
  MODULE cancel AT EXIT-COMMAND.
  CALL SUBSCREEN sub1.              " PAI 側只寫區域名,不帶 INCLUDING
  FIELD fld1 MODULE check1 ON REQUEST.
  FIELD fld2 MODULE check2 ON INPUT.
  FIELD fld3 MODULE check3.
  MODULE user_command_2001.
*&---------------------------------------------------------------------*
*& 由功能表決定顯示哪一個子畫面
*&---------------------------------------------------------------------*
DATA gv_screenid TYPE sy-dynnr VALUE '2002'.

MODULE user_command_2001 INPUT.
  DATA(lv_ok) = ok_code.
  CLEAR ok_code.

  gv_screenid = SWITCH #( lv_ok
                  WHEN 'MENU11' THEN '2002'      " 第一個子畫面
                  WHEN 'MENU12' THEN '2003'      " 第二個子畫面
                  ELSE gv_screenid ).

  CASE lv_ok.
    WHEN 'EXIT'. LEAVE PROGRAM.
    WHEN OTHERS.
  ENDCASE.
ENDMODULE.

呼叫 Subscreen 的鐵律(老師逐條列出)

  • 不可放在 LOOP … ENDLOOP 之內。
  • 不可放在 CHAIN … ENDCHAIN 之內。
  • 不必有自己的 OK_CODE。
  • Subscreen Area 的名稱不可重複
  • 不會MODULE … AT EXIT-COMMAND
  • 不會有自己的 Titlebar、PF-STATUS、SET SCREEN、LEAVE SCREEN。
  • Dynpro Type 必須是 Subscreen
*----------------------------------------------------------------------*
*  跨程式呼叫 Subscreen(Screen Exit / Enhancement 的核心機制)
*  情境:程式 A 的畫面 100,要嵌入程式 B(通常是 Function Group)的 0100
*----------------------------------------------------------------------*
PROCESS BEFORE OUTPUT.
  MODULE export_data.                              " 先把資料丟給 B
  CALL SUBSCREEN suba1 INCLUDING 'SAPLXXXX' '0100'.

PROCESS AFTER INPUT.
  MODULE user_exit_cmd AT EXIT-COMMAND.
  CALL SUBSCREEN suba1.
  MODULE import_data.                              " 再把 B 改過的值收回來
  FIELD f1 MODULE check_f1.
  CHAIN.
    FIELD f2, f3.
    MODULE check_f3.
  ENDCHAIN.
  MODULE user_command_0100.
" 對應的兩個模組:靠 Function Module 當「資料傳送帶」
MODULE export_data OUTPUT.
  CALL FUNCTION 'Z_SUBSCR_SET_DATA'      " 存進 FG 的全域變數
    EXPORTING is_data = gs_data.
ENDMODULE.

MODULE import_data INPUT.
  CALL FUNCTION 'Z_SUBSCR_GET_DATA'      " 取回 FG 的全域變數
    IMPORTING es_data = gs_data.
ENDMODULE.
當程式 A 的 CALL SUBSCREEN 指向程式 B 時,Subscreen 只能存取 B 自己的 GLOBAL DATA——它看不到 A 的變數。所以程式 B 一般會是一個 Function Group,用 FM 當進出口。這正是 SAP Screen Exit(Enhancement)的原理:標準畫面挖一塊 Subscreen Area,你在自己的 FG 裡畫子畫面,再用 EXIT_SAPxxx 的 FM 收送資料。

13. Tabstrip 表頁籤控制項

第 09 節。Tabstrip 就是「多個 Subscreen + 一排頁籤按鈕」。強烈建議用精靈(Wizard)產生,它會自動幫你建常數、CONTROLS 宣告、PBO/PAI 模組,並把程式塞進三支 INCLUDE。

精靈第一個選項:兩種捲動模式(一定要選對)

模式Subscreen AreaFunction Type特性
Scrolling local in tabstrip control(scroll in SAP GUI)一個頁籤一個 Subscreen AreaP切換頁籤不送伺服器,速度快;但所有頁籤的資料一次全部載入
PAI scrolling in tabstrip control(scroll on application server)所有頁籤共用一個 Subscreen Area空白切換頁籤會觸發 PAI,可以做檢查、可以延遲載入;頁籤多時省記憶體
*&---------------------------------------------------------------------*
*& Include Z202202_41_TOP   " Tabstrip 精靈產生的宣告(不必修改)
*&---------------------------------------------------------------------*
*&SPWIZARD: FUNCTION CODES FOR TABSTRIP 'TAB'
CONSTANTS: BEGIN OF c_tab,
             tab1 TYPE sy-ucomm VALUE 'TAB_FC1',
             tab2 TYPE sy-ucomm VALUE 'TAB_FC2',
           END OF c_tab.

*&SPWIZARD: DATA FOR TABSTRIP 'TAB'
CONTROLS: tab TYPE TABSTRIP.              " ★ Tabstrip 的專用型別

DATA: BEGIN OF g_tab,
        subscreen   TYPE sy-dynnr,                       " 目前顯示的子螢幕編號
        prog        TYPE sy-repid VALUE 'Z202202_41',    " 目前程式
        pressed_tab TYPE sy-ucomm VALUE c_tab-tab1,      " 目前被按的頁籤
      END OF g_tab.
*&---------------------------------------------------------------------*
*& Include Z202202_41_PBO   " 設定 active tab 與對應的子螢幕
*&---------------------------------------------------------------------*
MODULE tab_active_tab_set OUTPUT.
  tab-activetab = g_tab-pressed_tab.       " ★ 讓頁籤反白在正確位置
  CASE g_tab-pressed_tab.
    WHEN c_tab-tab1. g_tab-subscreen = '2002'.
    WHEN c_tab-tab2. g_tab-subscreen = '2003'.
    WHEN OTHERS.
      "&SPWIZARD: DO NOTHING
  ENDCASE.
ENDMODULE.
*&---------------------------------------------------------------------*
*& Include Z202202_41_PAI   " 記住使用者按了哪個頁籤
*&---------------------------------------------------------------------*
MODULE tab_active_tab_get INPUT.
  ok_code = sy-ucomm.
  CASE ok_code.
    WHEN c_tab-tab1. g_tab-pressed_tab = c_tab-tab1.
    WHEN c_tab-tab2. g_tab-pressed_tab = c_tab-tab2.
    WHEN OTHERS.
      "&SPWIZARD: DO NOTHING
  ENDCASE.
ENDMODULE.
*----------------------------------------------------------------------*
*  主畫面 2001 加上 Tabstrip 之後的完整 flow logic
*----------------------------------------------------------------------*
PROCESS BEFORE OUTPUT.
*&SPWIZARD: PBO FLOW LOGIC FOR TABSTRIP 'TAB'
  MODULE tab_active_tab_set.
  CALL SUBSCREEN tab_sca
    INCLUDING g_tab-prog g_tab-subscreen.

  MODULE status_2001.
  MODULE modify_screen.
  MODULE initial.
  MODULE list_values.
  CALL SUBSCREEN sub1 INCLUDING sy-repid gv_screenid.

PROCESS AFTER INPUT.
  MODULE cancel AT EXIT-COMMAND.        " ★ E 類移到最前面
*&SPWIZARD: PAI FLOW LOGIC FOR TABSTRIP 'TAB'
  CALL SUBSCREEN tab_sca.
  MODULE tab_active_tab_get.

  FIELD fld1 MODULE check1 ON REQUEST.
  FIELD fld2 MODULE check2 ON INPUT.
  FIELD fld3 MODULE check3.
  MODULE user_command_2001.
動態控制頁籤:把某個頁籤變灰或隱藏,做法是在 PBO 用 LOOP AT SCREENscreen-name = 'TAB_FC2'screen-active = 0;或直接 SET PF-STATUS … EXCLUDING。另外可以用 tab-activetab = c_tab-tab2. 在程式中強制切到某一頁。
Tabstrip 精靈會自動建立三支 INCLUDE(TOP/PBO/PAI),存檔並啟動後可以逐一觀察,都不必修改。INCLUDE 寫在主程式前面或後面效果相同。

14. Table Control:觀念與精靈

第 12、13、20 節。Table Control 就是畫面上的「可編輯表格」。它跟 ALV Grid 的差別在於:Table Control 是 Dynpro 原生元件,欄位就是螢幕欄位,可以逐格控制屬性、逐格做 FIELD 檢查;而 ALV Grid 是 GUI Control(畫面外掛)。

比較Table ControlCL_GUI_ALV_GRID
本質Dynpro 元件GUI Control(Custom Container)
編輯天生可編輯,欄位級檢查最細要設 edit=X,檢查靠事件
排序/篩選/匯出要自己寫內建
資料量幾百到幾千列還好(只送可見列)上萬列較佳
逐列不同屬性非常容易(迴圈內改 screen)要用 style 表
建議單據明細維護(表頭+明細)報表、大量清單

什麼時候會觸動 PAI?(可用 /h Debug 實測)

  • 不會觸動 PAI:水平捲動、調整欄寬、移動欄位順序、選取行列。
  • 觸動 PAI:垂直捲動、儲存版面設定(Save layout)、按任何功能鍵。
可以放進 Table Control 的資料來源,必須是 Transparent TableStructure Type(DDIC 型別)。純程式內宣告的區域結構拉不進 Screen Painter,所以做 Table Control 之前,先在 SE11 建好一個 Structure——這是老師強調「設定較多,請善用精靈」的原因。

Table Control Wizard 的九個步驟

  1. 建立程式與螢幕,並在 Element list 加 OK_CODE
  2. 在 Layout 拉出 Table Control Wizard範圍不要拉太小,否則欄位塞不下)。
  3. Name of the table control:命名(例如 TC_MARD)。
  4. Table / Structure:指定 DDIC 來源(選 Dictionary),以及對應的內表名。
  5. Definition of Columns:選取要顯示在畫面上的欄位。
  6. Table control attributes:可否編輯、可否選取列、標題列等。
  7. Select additional table maintenance functions:勾選可捲動、插入列、刪除列、全選、取消全選——勾了精靈才會產生對應的 FORM。
  8. Set Includes:指定四支 INCLUDE(TOP/O01/I01/F01)。
  9. Finished:完成後回主程式,把資料撈進內表,再 CALL SCREEN
老師的話:「Table Control Wizard 所產生的代碼也重要,必須仔細的閱讀。我們手動新增時會用到。」 第 13 節就是要你不靠精靈再做一次(貼 Table Control 而不是 Table Control Wizard),才會真的懂。

Table Control 的核心變數

CONTROLS: tc_mard TYPE TABLEVIEW USING SCREEN 0100.
" tc_mard 的型別實際上是 DDIC 結構 CXTAB_CONTROL,重要欄位:

" tc_mard-lines        總列數(決定捲軸長度)→ PBO 一定要設
" tc_mard-top_line     目前顯示的第一列在內表中的索引 → 控制捲動位置
" tc_mard-current_line PAI 迴圈中「目前這一列」在內表中的索引 → MODIFY 用
" tc_mard-cols[]       欄位屬性內表(可改順序、寬度、是否可見)
" tc_mard-invisible    整個 TC 隱藏
" tc_mard-v_scroll / h_scroll  是否顯示捲軸

" 另兩個系統欄位:
" sy-loopc  PBO 迴圈中:畫面上「實際可見的列數」→ 捲動計算必備
" sy-stepl  迴圈中:目前是畫面上的第幾列(1..sy-loopc)→ 逐列改屬性用

15. Table Control 的 Flow Logic 與同步機制

Table Control 最難懂的地方在「畫面的可見列」與「內表的所有列」是兩回事。PBO 的 LOOP 負責把內表倒進畫面,PAI 的 LOOP 負責把畫面倒回內表。

*----------------------------------------------------------------------*
*  PBO:把內表倒進畫面
*----------------------------------------------------------------------*
PROCESS BEFORE OUTPUT.
  MODULE status_0100.

*&SPWIZARD: PBO FLOW LOGIC FOR TABLECONTROL 'TC_MARD'
  MODULE tc_mard_change_tc_attr.       " 設定 lines(捲軸長度)
* MODULE tc_mard_change_col_attr.      " 需要時:整欄屬性(永久性修改)

  LOOP AT   gt_mard
       INTO gs_mard                    " ★ 沒有 header line 的現代寫法
       WITH CONTROL tc_mard
       CURSOR tc_mard-current_line.
    MODULE tc_mard_get_lines.          " 記住 sy-loopc
    MODULE tc_mard_change_field_attr.  " 逐列屬性(暫時性修改)
  ENDLOOP.
*----------------------------------------------------------------------*
*  PAI:把畫面倒回內表
*----------------------------------------------------------------------*
PROCESS AFTER INPUT.
  MODULE exit_0100 AT EXIT-COMMAND.    " ★ E 類放最前面

*&SPWIZARD: PAI FLOW LOGIC FOR TABLECONTROL 'TC_MARD'
  LOOP AT gt_mard.                     " 注意:PAI 的 LOOP 不帶 INTO
    CHAIN.
      FIELD gs_mard-matnr.
      FIELD gs_mard-werks.
      FIELD gs_mard-lgort.
      FIELD gs_mard-labst.
      MODULE tc_mard_modify ON CHAIN-REQUEST.   " 有改才寫回內表
    ENDCHAIN.
    FIELD gs_mard-mark
      MODULE tc_mard_mark ON REQUEST.            " 勾選欄單獨處理
  ENDLOOP.

  MODULE tc_mard_user_command.
*&---------------------------------------------------------------------*
*& PBO 的兩個模組
*&---------------------------------------------------------------------*
MODULE tc_mard_change_tc_attr OUTPUT.
  " ★ 一定要設 lines,否則捲軸長度不對、捲不到最後一列
  DESCRIBE TABLE gt_mard LINES tc_mard-lines.
ENDMODULE.

MODULE tc_mard_get_lines OUTPUT.
  " sy-loopc = 這一輪畫面上實際顯示了幾列,捲動計算要用
  g_tc_mard_lines = sy-loopc.
ENDMODULE.

*&---------------------------------------------------------------------*
*& PAI 的寫回模組:TC 與內表同步的關鍵一行
*&---------------------------------------------------------------------*
MODULE tc_mard_modify INPUT.
  MODIFY gt_mard
    FROM gs_mard
    INDEX tc_mard-current_line.        " ★ 用 current_line,不是 sy-tabix
ENDMODULE.
Table Control 三大經典 bug:
(1) 忘記 DESCRIBE TABLE … LINES tc-lines → 捲軸不動、只看得到第一頁。
(2) MODIFY 時用了 sy-tabix 而不是 tc-current_line → 改 A 列卻寫到 B 列,最常在捲動後爆出來。
(3) PAI 沒有 MODIFY → 使用者輸入的值在下一次 PBO 被內表原值蓋回去,看起來像「改了又跳回來」。

永久性 vs 暫時性屬性修改

PBO 中 LOOP 之前tc-cols 屬於永久性修改(整欄一致);LOOP 之內screen-… 屬於暫時性修改(只影響當列)。善加利用,可以做到「不同列屬性不同」,例如第二、四列必填。

*&---------------------------------------------------------------------*
*& 永久性修改:整欄變唯讀 / 調整欄位順序
*&---------------------------------------------------------------------*
MODULE tc_mard_change_col_attr OUTPUT.
  LOOP AT tc_mard-cols ASSIGNING FIELD-SYMBOL(<ls_col>).
    CASE <ls_col>-screen-name.
      WHEN 'GS_MARD-MATNR' OR 'GS_MARD-WERKS' OR 'GS_MARD-LGORT'.
        <ls_col>-screen-input = 0.        " 主鍵欄整欄唯讀
      WHEN 'GS_MARD-LABST'.
        <ls_col>-index = 4.               " 調整這一欄的顯示順序
    ENDCASE.
  ENDLOOP.
ENDMODULE.

*&---------------------------------------------------------------------*
*& 暫時性修改:逐列不同(放在 PBO 的 LOOP 之內)
*&---------------------------------------------------------------------*
MODULE tc_mard_change_field_attr OUTPUT.
  LOOP AT SCREEN.
    " 已存在 DB 的列,主鍵不給改;新增列(NEW 旗標)才可以改
    IF gs_mard-newrow <> abap_true
       AND screen-name CS 'MATNR'.
      screen-input = 0.
    ENDIF.
    " 偶數列的數量欄設成必填(示範暫時性修改的威力)
    IF sy-stepl MOD 2 = 0 AND screen-name CS 'LABST'.
      screen-required = 1.
    ENDIF.
    MODIFY SCREEN.
  ENDLOOP.
ENDMODULE.

16. 完整範例:一支可跑的 Table Control 維護程式

把前面所有觀念組起來。這支 ZTC_MARD_MAINT 讓使用者用選擇畫面過濾庫存(MARD),在 Table Control 中編輯、插入、刪除、全選、排序,最後鎖定並存回資料庫。四支 INCLUDE + 一個螢幕 0100 + 一個 GUI Status 'S0100' + 一個 GUI Title 'T0100'。

16.1 SE11 先建結構

先建一個結構 ZSTC_MARD(因為 Table Control 只吃 DDIC 型別):

欄位型別說明
MARKCHAR1選取欄(給刪除/全選用)
NEWROWCHAR1是否為新增列(自己加的旗標)
MATNRMATNR物料
WERKSWERKS_D工廠
LGORTLGORT_D儲存地點
LABSTLABST非限制使用庫存
INSMEINSME品檢庫存
SPEMESPEME凍結庫存
ICONICON-NAME (CHAR4)紅綠燈欄位(Status Icon)

16.2 主程式

*&---------------------------------------------------------------------*
*& Report ZTC_MARD_MAINT
*& 用 Table Control 維護庫存(MARD)— 完整可跑範例
*&---------------------------------------------------------------------*
REPORT ztc_mard_maint.

INCLUDE ztc_mard_maint_top.     " 宣告
INCLUDE ztc_mard_maint_o01.     " PBO
INCLUDE ztc_mard_maint_i01.     " PAI
INCLUDE ztc_mard_maint_f01.     " FORM

*----------------------------------------------------------------------*
INITIALIZATION.
  s_werks-sign = 'I'. s_werks-option = 'EQ'. s_werks-low = '1000'.
  APPEND s_werks TO s_werks.

START-OF-SELECTION.
  PERFORM load_data.
  IF gt_mard IS INITIAL.
    MESSAGE '查無資料' TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.
  CALL SCREEN 0100.

16.3 INCLUDE ZTC_MARD_MAINT_TOP(宣告)

*&---------------------------------------------------------------------*
*& Include ZTC_MARD_MAINT_TOP
*&---------------------------------------------------------------------*

" ── 選擇畫面 ────────────────────────────────────────────────
SELECT-OPTIONS: s_matnr FOR gs_mard-matnr,      " 見下方 gs_mard 宣告
                s_werks FOR gs_mard-werks OBLIGATORY,
                s_lgort FOR gs_mard-lgort.

" ── Table Control 的工作區與內表 ────────────────────────────
" ★ 螢幕欄位名稱必須是 GS_MARD-MATNR、GS_MARD-WERKS …
DATA: gs_mard TYPE zstc_mard,
      gt_mard TYPE STANDARD TABLE OF zstc_mard WITH EMPTY KEY,
      gt_orig TYPE STANDARD TABLE OF zstc_mard WITH EMPTY KEY.  " 原始快照,用來比對異動

" ── Table Control 本體宣告 ──────────────────────────────────
*&SPWIZARD: DECLARATION OF TABLECONTROL 'TC_MARD' ITSELF
CONTROLS: tc_mard TYPE TABLEVIEW USING SCREEN 0100.

*&SPWIZARD: LINES OF TABLECONTROL 'TC_MARD'
DATA: g_tc_mard_lines TYPE sy-loopc.        " PBO 記錄可見列數

" ── 其他 ────────────────────────────────────────────────────
DATA: ok_code    TYPE sy-ucomm,
      gv_ok      TYPE sy-ucomm,
      gv_changed TYPE abap_bool,            " 是否有未存檔的異動
      gv_sortdir TYPE c LENGTH 1.           " 排序方向 A / D

CONSTANTS: gc_icon_ok   TYPE icon-name VALUE '@08@',   " 綠燈
           gc_icon_warn TYPE icon-name VALUE '@09@',   " 黃燈
           gc_icon_ng   TYPE icon-name VALUE '@0A@'.   " 紅燈

16.4 螢幕 0100 的 Flow Logic

*----------------------------------------------------------------------*
*  Screen 0100 (Normal)  Next screen = 0
*----------------------------------------------------------------------*
PROCESS BEFORE OUTPUT.
  MODULE status_0100.
  MODULE tc_mard_change_tc_attr.
  MODULE tc_mard_change_col_attr.

  LOOP AT   gt_mard
       INTO gs_mard
       WITH CONTROL tc_mard
       CURSOR tc_mard-current_line.
    MODULE tc_mard_get_lines.
    MODULE tc_mard_change_field_attr.
  ENDLOOP.

PROCESS AFTER INPUT.
  MODULE exit_0100 AT EXIT-COMMAND.          " ★ Function Type = E

  LOOP AT gt_mard.
    CHAIN.
      FIELD gs_mard-labst.
      FIELD gs_mard-insme.
      FIELD gs_mard-speme.
      MODULE tc_mard_modify ON CHAIN-REQUEST.
    ENDCHAIN.
    FIELD gs_mard-mark MODULE tc_mard_mark ON REQUEST.
  ENDLOOP.

  MODULE tc_mard_user_command.

16.5 INCLUDE ZTC_MARD_MAINT_O01(PBO)

*&---------------------------------------------------------------------*
*& Include ZTC_MARD_MAINT_O01
*&---------------------------------------------------------------------*
MODULE status_0100 OUTPUT.
  " 沒有異動時把 SAVE 灰掉
  DATA(lt_excl) = COND ttftit( WHEN gv_changed = abap_false
                               THEN VALUE #( ( 'SAVE' ) ) ).
  SET PF-STATUS 'S0100' EXCLUDING lt_excl.
  SET TITLEBAR  'T0100' WITH lines( gt_mard ).
ENDMODULE.

*----------------------------------------------------------------------*
MODULE tc_mard_change_tc_attr OUTPUT.
  DESCRIBE TABLE gt_mard LINES tc_mard-lines.   " ★ 捲軸長度
ENDMODULE.

*----------------------------------------------------------------------*
MODULE tc_mard_get_lines OUTPUT.
  g_tc_mard_lines = sy-loopc.                   " ★ 可見列數
ENDMODULE.

*----------------------------------------------------------------------*
MODULE tc_mard_change_col_attr OUTPUT.          " 永久性:主鍵整欄唯讀
  LOOP AT tc_mard-cols ASSIGNING FIELD-SYMBOL(<ls_col>).
    IF <ls_col>-screen-name CS 'MATNR'
    OR <ls_col>-screen-name CS 'WERKS'
    OR <ls_col>-screen-name CS 'LGORT'.
      <ls_col>-screen-input = 0.
    ENDIF.
  ENDLOOP.
ENDMODULE.

*----------------------------------------------------------------------*
MODULE tc_mard_change_field_attr OUTPUT.        " 暫時性:逐列不同
  " 新增列才允許輸入主鍵
  IF gs_mard-newrow = abap_true.
    LOOP AT SCREEN.
      IF screen-name CS 'MATNR' OR screen-name CS 'WERKS'
      OR screen-name CS 'LGORT'.
        screen-input = 1.
        MODIFY SCREEN.
      ENDIF.
    ENDLOOP.
  ENDIF.

  " 紅綠燈:庫存為負亮紅燈,為零亮黃燈,其餘綠燈
  gs_mard-icon = COND #( WHEN gs_mard-labst < 0 THEN gc_icon_ng
                         WHEN gs_mard-labst = 0 THEN gc_icon_warn
                         ELSE gc_icon_ok ).
ENDMODULE.

16.6 INCLUDE ZTC_MARD_MAINT_I01(PAI)

*&---------------------------------------------------------------------*
*& Include ZTC_MARD_MAINT_I01
*&---------------------------------------------------------------------*
MODULE exit_0100 INPUT.                 " Function Type = E,跳過所有檢查
  CASE ok_code.
    WHEN 'BACK' OR 'EXIT' OR 'CANCEL'.
      IF gv_changed = abap_true.
        CALL FUNCTION 'POPUP_TO_CONFIRM'
          EXPORTING
            titlebar              = '離開確認'
            text_question         = '資料尚未存檔,確定要離開嗎?'
            default_button        = '2'
            display_cancel_button = abap_false
          IMPORTING
            answer                = DATA(lv_answer)
          EXCEPTIONS
            OTHERS                = 1.
        IF lv_answer <> '1'.
          RETURN.                       " 使用者說不要,留在原畫面
        ENDIF.
      ENDIF.
      PERFORM unlock_all.
      LEAVE TO SCREEN 0.
  ENDCASE.
ENDMODULE.

*----------------------------------------------------------------------*
MODULE tc_mard_modify INPUT.            " ★ 畫面 → 內表 的同步
  gs_mard-newrow = gs_mard-newrow.      " 保留旗標
  MODIFY gt_mard FROM gs_mard INDEX tc_mard-current_line.
  gv_changed = abap_true.
ENDMODULE.

*----------------------------------------------------------------------*
MODULE tc_mard_mark INPUT.              " 勾選欄的同步
  MODIFY gt_mard FROM gs_mard INDEX tc_mard-current_line
    TRANSPORTING mark.
ENDMODULE.

*----------------------------------------------------------------------*
MODULE tc_mard_user_command INPUT.
  gv_ok = ok_code.
  CLEAR ok_code.

  " ★ 先交給 Table Control 的標準處理(插入 / 刪除 / 捲動 / 全選)
  PERFORM user_ok_tc USING    'TC_MARD'
                              'GT_MARD'
                              'MARK'
                     CHANGING gv_ok.

  CASE gv_ok.
    WHEN 'SAVE'.   PERFORM save_data.
    WHEN 'REFR'.   PERFORM load_data.
    WHEN 'SORTA'.  PERFORM sort_tc USING 'A'.
    WHEN 'SORTD'.  PERFORM sort_tc USING 'D'.
    WHEN 'TOP'.    tc_mard-top_line = 1.
    WHEN 'BOTM'.   tc_mard-top_line = nmax( val1 = 1
                                            val2 = lines( gt_mard ) - g_tc_mard_lines + 1 ).
    WHEN OTHERS.
  ENDCASE.
ENDMODULE.

16.7 INCLUDE ZTC_MARD_MAINT_F01(FORM)

*&---------------------------------------------------------------------*
*& Include ZTC_MARD_MAINT_F01
*&---------------------------------------------------------------------*
FORM load_data.
  SELECT matnr, werks, lgort, labst, insme, speme
    FROM mard
    WHERE matnr IN @s_matnr
      AND werks IN @s_werks
      AND lgort IN @s_lgort
    ORDER BY matnr, werks, lgort
    INTO CORRESPONDING FIELDS OF TABLE @gt_mard
    UP TO 1000 ROWS.

  gt_orig = gt_mard.                    " 保留快照,存檔時比對
  CLEAR: gv_changed.
  tc_mard-top_line = 1.
ENDFORM.

*----------------------------------------------------------------------*
FORM save_data.
  " 只挑真的有變動的列
  DATA(lt_upd) = VALUE tt_zstc_mard(
    FOR ls IN gt_mard
    WHERE ( matnr IS NOT INITIAL )
    ( COND #( WHEN NOT line_exists( gt_orig[ matnr = ls-matnr
                                             werks = ls-werks
                                             lgort = ls-lgort
                                             labst = ls-labst
                                             insme = ls-insme
                                             speme = ls-speme ] )
              THEN ls ) ) ).
  DELETE lt_upd WHERE matnr IS INITIAL.

  IF lt_upd IS INITIAL.
    MESSAGE '沒有需要儲存的異動' TYPE 'S'.
    RETURN.
  ENDIF.

  " ── 鎖定 ──
  LOOP AT lt_upd INTO DATA(ls_upd).
    CALL FUNCTION 'ENQUEUE_EMMARD'
      EXPORTING
        mode_mard      = 'E'
        mandt          = sy-mandt
        matnr          = ls_upd-matnr
        werks          = ls_upd-werks
        lgort          = ls_upd-lgort
        _scope         = '2'
      EXCEPTIONS
        foreign_lock   = 1
        system_failure = 2
        OTHERS         = 3.
    IF sy-subrc <> 0.
      MESSAGE |物料 { ls_upd-matnr } 已被 { sy-msgv1 } 鎖定| TYPE 'E'.
      RETURN.
    ENDIF.
  ENDLOOP.

  " ── 寫回 ──
  " ★ 實務上庫存數量絕不可以直接 UPDATE MARD,必須走 MIGO / BAPI_GOODSMVT_CREATE。
  "   這裡僅示範 Table Control 的存檔骨架,請換成自建 Z 表或 BAPI。
  TRY.
      MODIFY zstc_stock FROM TABLE @lt_upd.
      IF sy-subrc = 0.
        COMMIT WORK AND WAIT.
        gt_orig = gt_mard.
        CLEAR gv_changed.
        MESSAGE |已儲存 { lines( lt_upd ) } 筆| TYPE 'S'.
      ELSE.
        ROLLBACK WORK.
        MESSAGE '存檔失敗' TYPE 'E'.
      ENDIF.
    CATCH cx_sy_open_sql_db INTO DATA(lx_db).
      ROLLBACK WORK.
      MESSAGE lx_db->get_text( ) TYPE 'E'.
  ENDTRY.

  PERFORM unlock_all.
ENDFORM.

*----------------------------------------------------------------------*
FORM unlock_all.
  CALL FUNCTION 'DEQUEUE_ALL'.          " 最省事的全解鎖
ENDFORM.

*----------------------------------------------------------------------*
FORM sort_tc USING p_dir TYPE c.
  " Table Control 沒有內建排序,要自己排內表
  IF p_dir = 'A'.
    SORT gt_mard BY matnr ASCENDING werks ASCENDING lgort ASCENDING.
  ELSE.
    SORT gt_mard BY matnr DESCENDING werks DESCENDING lgort DESCENDING.
  ENDIF.
  tc_mard-top_line = 1.                 " 排完回到第一列
ENDFORM.
絕對不要直接 UPDATE MARD / MARC / MBEW。 在 S/4HANA 中庫存主檔仍由 MM 模組維護,數量異動必須走 MIGO / BAPI_GOODSMVT_CREATE;物料主檔異動走 BAPI_MATERIAL_SAVEDATA。上面的範例只是拿 MARD 當「畫面上的資料來源」示範 Table Control 的骨架,實際存檔請改寫進自建的 Z 表或呼叫 BAPI。

17. Table Control 的標準 FORM:插入、刪除、捲動、全選

精靈產生的 TABLECONTROL_FORMS 這一段程式是 SAP 的標準寫法,用 Field Symbol + 動態 ASSIGN 做到「一套 FORM 服務所有 Table Control」。手動建 Table Control 時,把這一段複製過去就好。

17.1 總入口 USER_OK_TC

*&---------------------------------------------------------------------*
*&      Form  USER_OK_TC   " 攔截所有 <TC 名稱>_<動作> 格式的功能碼
*&---------------------------------------------------------------------*
FORM user_ok_tc USING    p_tc_name    TYPE dynfnam
                         p_table_name
                         p_mark_name
                CHANGING p_ok         TYPE sy-ucomm.

  DATA: l_ok     TYPE sy-ucomm,
        l_offset TYPE i.

  " 功能碼必須以 TC 名稱開頭,例如 TC_MARD_INSR
  SEARCH p_ok FOR p_tc_name.
  IF sy-subrc <> 0.
    EXIT.                               " 不是 TC 的功能碼,交回主流程
  ENDIF.
  l_offset = strlen( p_tc_name ) + 1.
  l_ok = p_ok+l_offset.

  CASE l_ok.
    WHEN 'INSR'.                        " 插入列
      PERFORM fcode_insert_row USING p_tc_name p_table_name.
      CLEAR p_ok.

    WHEN 'DELE'.                        " 刪除列
      PERFORM fcode_delete_row USING p_tc_name p_table_name p_mark_name.
      CLEAR p_ok.

    WHEN 'P--' OR                       " 第一頁
         'P-'  OR                       " 上一頁
         'P+'  OR                       " 下一頁
         'P++'.                         " 最後一頁
      PERFORM compute_scrolling_in_tc USING p_tc_name l_ok.
      CLEAR p_ok.

    WHEN 'MARK'.                        " 全選
      PERFORM fcode_tc_mark_lines USING p_tc_name p_table_name p_mark_name.
      CLEAR p_ok.

    WHEN 'DMRK'.                        " 取消全選
      PERFORM fcode_tc_demark_lines USING p_tc_name p_table_name p_mark_name.
      CLEAR p_ok.

*   WHEN 'L--' OR 'L-' OR 'R+' OR 'R++'.   " 水平捲動(精靈預設註解掉)
*   WHEN 'SASCEND' OR 'SDESCEND'.          " 欄位排序(要自己實作)
  ENDCASE.
ENDFORM.
口訣:Table Control 的功能碼=「TC 名稱 + 底線 + 動作」,例如 TC 叫 TC_MARD,插入列就是 TC_MARD_INSR、刪除是 TC_MARD_DELE、全選是 TC_MARD_MARK、下一頁是 TC_MARD_P+。在 SE41 建 GUI Status 時就照這個規則命名,USER_OK_TC 才攔得到。

17.2 插入列

*&---------------------------------------------------------------------*
*&      Form  FCODE_INSERT_ROW
*&---------------------------------------------------------------------*
FORM fcode_insert_row USING p_tc_name TYPE dynfnam
                            p_table_name.

  DATA: l_lines_name TYPE feld-name,
        l_selline    TYPE sy-stepl,
        l_lastline   TYPE i,
        l_line       TYPE i,
        l_table_name TYPE feld-name.

  FIELD-SYMBOLS: <tc>    TYPE cxtab_control,
                 <table> TYPE STANDARD TABLE,
                 <lines> TYPE i.

  ASSIGN (p_tc_name) TO <tc>.

  " 取得 TC 所對應的內表(加 [] 表示表體,不是 header line)
  l_table_name = |{ p_table_name }[]|.
  ASSIGN (l_table_name) TO <table>.

  " 取得畫面可見列數變數 G_<TC 名稱>_LINES
  l_lines_name = |G_{ p_tc_name }_LINES|.
  ASSIGN (l_lines_name) TO <lines>.

  " 游標在哪一列?
  GET CURSOR LINE l_selline.
  IF sy-subrc <> 0.                     " 游標不在表內 → 追加到最後
    l_selline = <tc>-lines + 1.
    IF l_selline > <lines>.
      <tc>-top_line = l_selline - <lines> + 1.
    ELSE.
      <tc>-top_line = 1.
    ENDIF.
  ELSE.                                 " 游標在表內 → 插在游標那一列
    l_selline  = <tc>-top_line + l_selline - 1.
    l_lastline = <tc>-top_line + <lines> - 1.
  ENDIF.
  l_line = l_selline - <tc>-top_line + 1.

  INSERT INITIAL LINE INTO <table> INDEX l_selline.
  <tc>-lines = <tc>-lines + 1.
  SET CURSOR LINE l_line.
ENDFORM.

17.3 刪除列(依 MARK 欄)

*&---------------------------------------------------------------------*
*&      Form  FCODE_DELETE_ROW
*&---------------------------------------------------------------------*
FORM fcode_delete_row USING p_tc_name TYPE dynfnam
                            p_table_name
                            p_mark_name.

  DATA l_table_name TYPE feld-name.
  FIELD-SYMBOLS: <tc>         TYPE cxtab_control,
                 <table>      TYPE STANDARD TABLE,
                 <wa>         TYPE any,
                 <mark_field> TYPE any.

  ASSIGN (p_tc_name) TO <tc>.
  l_table_name = |{ p_table_name }[]|.
  ASSIGN (l_table_name) TO <table>.

  DESCRIBE TABLE <table> LINES <tc>-lines.

  LOOP AT <table> ASSIGNING <wa>.
    ASSIGN COMPONENT p_mark_name OF STRUCTURE <wa> TO <mark_field>.
    IF <mark_field> = 'X'.
      DELETE <table> INDEX syst-tabix.
      IF sy-subrc = 0.
        <tc>-lines = <tc>-lines - 1.
      ENDIF.
    ENDIF.
  ENDLOOP.
ENDFORM.
SAP 這段標準碼在 LOOP 中 DELETE 同一張表。它之所以「能動」,是因為 DELETE 之後 sy-tabix 的後續處理剛好抵銷;但這是脆弱寫法。自己寫時建議改成一行:DELETE gt_mard WHERE mark = abap_true.,再重設 tc-lines,既安全又快。

17.4 垂直捲動

*&---------------------------------------------------------------------*
*&      Form  COMPUTE_SCROLLING_IN_TC   " 用標準 FM 算出新的 top_line
*&---------------------------------------------------------------------*
FORM compute_scrolling_in_tc USING p_tc_name
                                   p_ok.
  DATA: l_tc_new_top_line TYPE i,
        l_tc_name         TYPE feld-name,
        l_tc_lines_name   TYPE feld-name,
        l_tc_field_name   TYPE feld-name.

  FIELD-SYMBOLS: <tc>    TYPE cxtab_control,
                 <lines> TYPE i.

  ASSIGN (p_tc_name) TO <tc>.
  l_tc_lines_name = |G_{ p_tc_name }_LINES|.
  ASSIGN (l_tc_lines_name) TO <lines>.

  IF <tc>-lines = 0.                    " 沒有任何一列
    l_tc_new_top_line = 1.
  ELSE.
    CALL FUNCTION 'SCROLLING_IN_TABLE'
      EXPORTING
        entry_act      = <tc>-top_line
        entry_from     = 1
        entry_to       = <tc>-lines
        last_page_full = 'X'
        loops          = <lines>
        ok_code        = p_ok
        overlapping    = 'X'
      IMPORTING
        entry_new      = l_tc_new_top_line
      EXCEPTIONS
        OTHERS         = 0.
  ENDIF.

  " 捲動後把游標放回同一欄的第一列
  GET CURSOR FIELD l_tc_field_name AREA l_tc_name.
  IF syst-subrc = 0 AND l_tc_name = p_tc_name.
    SET CURSOR FIELD l_tc_field_name LINE 1.
  ENDIF.

  <tc>-top_line = l_tc_new_top_line.
ENDFORM.

17.5 全選 / 取消全選

*&---------------------------------------------------------------------*
*&      Form  FCODE_TC_MARK_LINES / FCODE_TC_DEMARK_LINES
*&---------------------------------------------------------------------*
FORM fcode_tc_mark_lines USING p_tc_name p_table_name p_mark_name.
  PERFORM set_all_marks USING p_tc_name p_table_name p_mark_name 'X'.
ENDFORM.

FORM fcode_tc_demark_lines USING p_tc_name p_table_name p_mark_name.
  PERFORM set_all_marks USING p_tc_name p_table_name p_mark_name space.
ENDFORM.

FORM set_all_marks USING p_tc_name p_table_name p_mark_name p_value.
  DATA l_table_name TYPE feld-name.
  FIELD-SYMBOLS: <tc>         TYPE cxtab_control,
                 <table>      TYPE STANDARD TABLE,
                 <wa>         TYPE any,
                 <mark_field> TYPE any.

  ASSIGN (p_tc_name) TO <tc>.
  l_table_name = |{ p_table_name }[]|.
  ASSIGN (l_table_name) TO <table>.

  LOOP AT <table> ASSIGNING <wa>.
    ASSIGN COMPONENT p_mark_name OF STRUCTURE <wa> TO <mark_field>.
    <mark_field> = p_value.
  ENDLOOP.
ENDFORM.

17.6 老師的 ZSORT 小範例:控制行號與退出鍵

示範 TOP_LINE 的效果(一開機就從第 2 列開始顯示),以及最基本的 BACK 處理。第一次設計時老師故意漏掉退出鍵,讓大家體會「按不出去」的痛。

*&---------------------------------------------------------------------*
*& Report ZSORT   " 用 VALUE #( ) 準備測試資料,並控制 TOP_LINE
*&---------------------------------------------------------------------*
REPORT zsort.

DATA: gt_makt TYPE STANDARD TABLE OF makt WITH EMPTY KEY,
      gs_makt TYPE makt.

*&SPWIZARD: DECLARATION OF TABLECONTROL 'TC_MAKT' ITSELF
CONTROLS: tc_makt TYPE TABLEVIEW USING SCREEN 0100.
DATA: g_tc_makt_lines TYPE sy-loopc,
      ok_code         TYPE sy-ucomm.

START-OF-SELECTION.
  " VALUE #( ) 的分組寫法:BASE 之外,也可用「共用欄位 ( 差異欄位 )」
  gt_makt = VALUE #(
      matnr = 'APPLE' ( spras = 'M' maktx = 'APPLE-M' )
                      ( spras = 'E' maktx = 'APPLE-E' )
      matnr = 'GOOGL' ( spras = 'M' maktx = 'GOOGL-M' )
                      ( spras = 'E' maktx = 'GOOGL-E' )
      matnr = 'FACEB' ( spras = 'M' maktx = 'FACEB-M' )
                      ( spras = 'E' maktx = 'FACEB-E' ) ).

  tc_makt-top_line = 2.                 " ★ 一進畫面就從第 2 列開始顯示
  CALL SCREEN 0100.

*----------------------------------------------------------------------*
MODULE status_0100 OUTPUT.
  SET PF-STATUS 'ZSORT'.                " 記得要有 BACK / EXIT!
ENDMODULE.

MODULE tc_makt_change_tc_attr OUTPUT.
  DESCRIBE TABLE gt_makt LINES tc_makt-lines.
ENDMODULE.

MODULE tc_makt_get_lines OUTPUT.
  g_tc_makt_lines = sy-loopc.
ENDMODULE.

MODULE tc_makt_modify INPUT.
  MODIFY gt_makt FROM gs_makt INDEX tc_makt-current_line.
ENDMODULE.

MODULE tc_makt_user_command INPUT.
  DATA(lv_ok) = ok_code.
  CLEAR ok_code.
  PERFORM user_ok_tc USING 'TC_MAKT' 'GT_MAKT' ' ' CHANGING lv_ok.
ENDMODULE.

MODULE user_command_0100 INPUT.
  CASE ok_code.
    WHEN 'BACK'. LEAVE TO SCREEN 0.     " ★ 第一次設計時漏掉這個,就出不去了
    WHEN OTHERS.
  ENDCASE.
  CLEAR ok_code.
ENDMODULE.

18. Custom Control:把 ALV、編輯器、圖片、HTML 掛上畫面

第 10 節。Custom Control 是一塊「保留區」,執行時由 CL_GUI_CUSTOM_CONTAINER 接管,然後在裡面放任何 GUI Control。作法固定三步:建 Container → 建 Control → 餵資料,而且要用 IF gv_init IS INITIAL. 保證只建一次(PBO 每次都會跑)。

18.1 文字編輯器 CL_GUI_TEXTEDIT

*&---------------------------------------------------------------------*
*& TOP:宣告
*&---------------------------------------------------------------------*
DATA: gv_init      TYPE abap_bool,
      go_container TYPE REF TO cl_gui_custom_container,
      go_editor    TYPE REF TO cl_gui_textedit.

" 編輯器的內容用 80 碼的字元表承接
TYPES: ty_line TYPE c LENGTH 80.
DATA  gt_text TYPE STANDARD TABLE OF ty_line WITH EMPTY KEY.

INITIALIZATION.
  gt_text = VALUE #( ( '請在此開始輸入' ) ).
*&---------------------------------------------------------------------*
*& Module STATUS_2001 OUTPUT   " PBO:建立 Container 與文字編輯器
*&---------------------------------------------------------------------*
MODULE status_2001 OUTPUT.
  SET PF-STATUS 'STATUS'.
  SET TITLEBAR  'TITLE'.

  IF gv_init IS INITIAL.
    " ★ container_name 必須等於 Layout 上 Custom Control 的名稱
    go_container = NEW cl_gui_custom_container( container_name = 'CUSCON' ).

    IF go_container IS BOUND.
      go_editor = NEW cl_gui_textedit(
        parent                     = go_container
        wordwrap_mode              = cl_gui_textedit=>wordwrap_at_fixed_position
        wordwrap_position          = 80
        wordwrap_to_linebreak_mode = cl_gui_textedit=>true ).

      " 隱藏編輯器自己的工具列與狀態列(要更像 SAP 原生畫面時)
      go_editor->set_toolbar_mode( 0 ).
      go_editor->set_statusbar_mode( 0 ).
    ENDIF.
    gv_init = abap_true.
  ENDIF.

  go_editor->set_text_as_r3table( table = gt_text ).
ENDMODULE.

*&---------------------------------------------------------------------*
*& PAI:把使用者打的字取回來(★ 一定要先 flush,否則拿到舊值)
*&---------------------------------------------------------------------*
MODULE get_editor_text INPUT.
  cl_gui_cfw=>flush( ).
  go_editor->get_text_as_r3table( IMPORTING table = gt_text ).
ENDMODULE.
GUI Control 的黃金定律:所有 get_* 之前一定要 cl_gui_cfw=>flush( ),因為 Control 的值住在前端(SAP GUI),不 flush 就拿不到使用者最新輸入的內容。這是 Custom Control 最常見的「值抓不到」原因。

18.2 掛 ALV Grid(CL_GUI_ALV_GRID)

DATA: go_cont TYPE REF TO cl_gui_custom_container,
      go_grid TYPE REF TO cl_gui_alv_grid.

MODULE status_0100 OUTPUT.
  SET PF-STATUS 'S0100'.

  IF go_grid IS NOT BOUND.
    go_cont = NEW cl_gui_custom_container( container_name = 'ALV_AREA' ).
    go_grid = NEW cl_gui_alv_grid( i_parent = go_cont ).

    DATA(ls_layout) = VALUE lvc_s_layo( cwidth_opt = abap_true
                                        zebra      = abap_true
                                        sel_mode   = 'A'
                                        grid_title = '庫存明細' ).

    go_grid->set_table_for_first_display(
      EXPORTING
        i_structure_name = 'ZSTC_MARD'
        is_layout        = ls_layout
      CHANGING
        it_outtab        = gt_mard ).
  ELSE.
    " 資料變了只要 refresh,不要重建物件
    go_grid->refresh_table_display(
      EXPORTING is_stable = VALUE lvc_s_stbl( row = abap_true col = abap_true ) ).
  ENDIF.
ENDMODULE.
只是要「全螢幕看一張表」時,不需要 Custom Control,也不需要建 Screen——直接 cl_salv_table=>factory( ) 兩行搞定。要編輯、事件、多容器分割時才用 CL_GUI_ALV_GRID + Custom Control。另外 CL_GUI_DOCKING_CONTAINER 可以「不畫 Screen 就把 ALV 掛在選擇畫面旁邊」,CL_GUI_SPLITTER_CONTAINER 可以把一塊 Custom Control 切成上下/左右多格。

18.3 圖片 CL_GUI_PICTURE

DATA: go_pic_cont TYPE REF TO cl_gui_custom_container,
      go_picture  TYPE REF TO cl_gui_picture.

MODULE show_picture OUTPUT.
  IF go_picture IS NOT BOUND.
    go_pic_cont = NEW cl_gui_custom_container( container_name = 'PIC_AREA' ).
    go_picture  = NEW cl_gui_picture( parent = go_pic_cont ).

    " 圖片先用 SMW0 上傳成 MIME 物件,再用 DP 產生一個暫時 URL
    DATA lv_url TYPE cndp_url.
    CALL FUNCTION 'DP_PUBLISH_WWW_URL'
      EXPORTING
        objid    = 'ZLOGO'              " SMW0 上傳時給的物件名稱
        lifetime = cndp_lifetime_transaction
      IMPORTING
        url      = lv_url
      EXCEPTIONS
        OTHERS   = 5.

    IF sy-subrc = 0.
      go_picture->load_picture_from_url( url = lv_url ).
      go_picture->set_display_mode(
        display_mode = cl_gui_picture=>display_mode_fit_center ).
    ENDIF.
  ENDIF.
ENDMODULE.
第 11 節:上傳文件 SMW0(講義有處誤植為 SWM0)。步驟:SMW0 → 選 Binary data for WebRFC applications → 指定 Package → Create → 給物件名稱與描述 → 上傳本機檔案。前提是 SAP 主機必須由 BASIS 提前配置(MIME repository / ICM)。上傳後就能用 DP_PUBLISH_WWW_URL 取得 URL 給 CL_GUI_PICTURE 或 SmartForms 使用。(依講義步驟整理)

18.4 HTML CL_GUI_HTML_VIEWER

DATA: go_html_cont TYPE REF TO cl_gui_custom_container,
      go_html      TYPE REF TO cl_gui_html_viewer.

MODULE show_html OUTPUT.
  IF go_html IS NOT BOUND.
    go_html_cont = NEW cl_gui_custom_container( container_name = 'HTML_AREA' ).
    go_html      = NEW cl_gui_html_viewer( parent = go_html_cont ).

    " 方式一:直接餵 HTML 內容
    DATA(lt_html) = VALUE w3htmltab(
      ( line = '<html><body style="font-family:Arial">' )
      ( line = '<h2>NCU SAP !</h2>' )
      ( line = |<p>目前使用者:{ sy-uname },日期:{ sy-datum DATE = USER }</p>| )
      ( line = '</body></html>' ) ).

    DATA lv_url TYPE c LENGTH 250.
    go_html->load_data( IMPORTING assigned_url = lv_url
                        CHANGING  data_table   = lt_html ).
    go_html->show_url( url = lv_url ).

    " 方式二:直接開外部網址
    " go_html->show_url( url = 'https://help.sap.com' ).
  ENDIF.
ENDMODULE.
Custom Control 裡的物件離開畫面時要釋放,否則同一個 session 反覆進出會累積控制項、拖慢 GUI:go_grid->free( ). go_container->free( ). CLEAR: go_grid, go_container, gv_init. 放在 EXIT 模組裡。

19. 圖示(ICON)與其他小技巧

第 16 節:控制 icon 的小範例。SAP 所有圖示都存在資料表 ICON 裡,可以用 SE16N 查(欄位 ID@xx@ 這種內碼,NAME 是 ICON_XXX 這種名稱)。

*&---------------------------------------------------------------------*
*& Report ZICON   " 在畫面上顯示圖示(第一次設計時漏掉退出鍵)
*&---------------------------------------------------------------------*
REPORT zicon.

DATA: gv_icon1 TYPE icon-name,          " 畫面上放一個 Status Icon 或字元欄位
      ok_code  TYPE sy-ucomm.

START-OF-SELECTION.
  CALL SCREEN 0100.

*----------------------------------------------------------------------*
MODULE status_0100 OUTPUT.
  SET PF-STATUS 'STA_100'.              " ★ 記得要含 CANCEL / BACK
* SET TITLEBAR 'xxx'.

  CALL FUNCTION 'ICON_CREATE'
    EXPORTING
      name                  = 'ICON_FAILURE'   " ★ 前面不可有空白!
      text                  = '失敗'
      info                  = '滑鼠移上去會顯示這段說明'
      add_stdinf            = 'X'
    IMPORTING
      result                = gv_icon1
    EXCEPTIONS
      icon_not_found        = 1
      outputfield_too_short = 2
      OTHERS                = 3.
  IF sy-subrc <> 0.
    MESSAGE '圖示建立失敗' TYPE 'S' DISPLAY LIKE 'E'.
  ENDIF.

  " 更簡單的做法:直接用型別池 ICON 的常數(不必呼叫 FM)
  " TYPE-POOLS icon.  →  gv_icon1 = icon_led_green.
ENDMODULE.

*----------------------------------------------------------------------*
MODULE user_command_0100 INPUT.
  CASE ok_code.
    WHEN 'CANCEL'. SET SCREEN 0.        " ← 第一次設計時就是漏了這段
  ENDCASE.
  CLEAR ok_code.
ENDMODULE.
講義原程式寫的是 name = ' ICON_FAILURE'前面多了一個空白,會直接觸發 ICON_NOT_FOUND。這種「看不出來的空白」在 ICON_CREATE、SET PF-STATUS、CALL TRANSACTION 都很常見,抓不到圖示時第一個先檢查它。

Parameter ID 無所不在

第 17 節。Dialog 畫面的欄位也能掛 Parameter ID(SET/GET parameter)——在 Element list 的欄位屬性裡填 Parameter ID,並勾 SET parameter / GET parameter,使用者上次輸入的值下次會自動帶出來。(詳見第 3 章 SPA/GPA 一節。)

" 程式中手動存取(Element list 沒勾時)
GET PARAMETER ID 'MAT' FIELD gs_mard-matnr.     " 進畫面時帶出
SET PARAMETER ID 'MAT' FIELD gs_mard-matnr.     " 離開時記住
" ID 必須先存在於資料表 TPARA,否則語法檢查會警告

20. S/4HANA 的定位:Dialog 程式還要不要寫?

這是 2025 年學 Module Pool 一定要先想清楚的問題。答案不是「不要寫」,而是「看情境選工具」。

情境建議做法理由
簡單的 Z 表維護SM30 / SE54 表維護產生器零程式碼,自動有鎖定、傳輸、變更記錄
查詢型報表SELECT-OPTIONS + CL_SALV_TABLE不必畫 Screen,維護成本最低
報表 + 少量編輯CL_GUI_ALV_GRID(可編輯 ALV)比 Table Control 好寫,內建排序篩選匯出
單據式維護(表頭 + 多明細 + 頁籤)且使用者只用 SAP GUIModule Pool + Table Control + TabstripDialog 仍是 GUI 上最完整的解
要在瀏覽器 / 手機上用RAP + Fiori Elements(CDS + Behavior Definition + OData V4)Dialog 不可能上 Fiori Launchpad
要擴充標準交易的畫面Screen Exit / BAdI 的 Subscreen本章第 12 節的跨程式 Subscreen 機制
雲端 ABAP(BTP / Public Cloud)只能 RAPDynpro 相關語法在 ABAP Cloud 中不在白名單內,語法檢查直接擋掉
ABAP Cloud 的硬限制:在 S/4HANA Cloud、BTP ABAP Environment,或在 on-premise 用「ABAP for Cloud Development」語言版本時,CALL SCREEN、Dynpro、Table Control、CL_GUI_* 全部不可用。所以新專案若有「未來要上雲」的規劃,Dialog 就不該再開新的。反之,S/4HANA on-premise 的 Classic ABAP 版本仍完整支援 Dialog,SAP 也還在用(VA01、ME21N 至今仍是 Dynpro)。
老師的實務觀察:有些公司會用 ABAP 開發 SAP 模組以外的功能。 遇到這種情境,你還要繼續精進 Screen 設計技巧——例如 ME21N 的結構就包含「左側導航欄 + 主階 HEADER + 子階 ITEM OVERVIEW + 子階再展開的 ITEM DETAIL」,那是 Subscreen + Tabstrip + Table Control 的綜合體。想學畫面設計,直接拆 ME21N 是最好的教材。

21. 除錯與檢查清單

症狀最可能的原因
按鈕按下去沒反應Pushbutton 沒填 FctCode;或畫面上沒有 OK_CODE 欄位;或 PAI 沒有 MODULE user_command
按了 A 功能卻執行 Bok_code 用完沒有 CLEAR,殘留舊值
錯誤訊息之後按不出去BACK/EXIT/CANCEL 的 Function Type 不是 E;或 AT EXIT-COMMAND 沒放在 PAI 最前面
使用者輸入的值改了又跳回原值PAI 沒有 MODIFY itab … INDEX tc-current_line
Table Control 捲不動 / 只看得到第一頁PBO 沒有 DESCRIBE TABLE … LINES tc-lines
捲動之後改到錯的列MODIFY 用了 sy-tabix 而不是 tc-current_line
插入列的位置不對沒有正確 ASSIGN G_<TC>_LINES(PBO 的 sy-loopc 沒存)
LOOP AT SCREEN 改了沒效果忘了 MODIFY SCREEN.;或比較值沒大寫
Custom Control 取不到使用者輸入沒有 cl_gui_cfw=>flush( )
進出畫面幾次後 GUI 變慢Container / Control 沒有 free( )
下拉選單沒東西VRM_SET_VALUES 的 id 不等於畫面欄位名(要大寫、要完整)
Subscreen 出不來Dynpro Type 不是 Subscreen;或 CALL SUBSCREEN 放進了 LOOP/CHAIN
頁籤點了不換頁PBO 沒設 tab-activetab;或 Function Type 選錯(本地捲動要 P)
單據被鎖住解不掉SM12 查並刪除;程式端要在 EXIT 模組 DEQUEUE
Debug 技巧:在畫面的命令欄輸入 /h 再按任何按鈕,就會停在 PAI 的第一行,可以逐步觀察 flow logic 的執行順序——這也是老師建議用來實測「哪些動作會觸動 PAI」的方法。另外 ST05(SQL Trace)可以抓出「PBO 每次都打 DB」這種效能地雷。

練習與自我檢核

  1. Type 1 vs Type M:不看筆記說出兩者在「副程式、顯示前、輸入後、螢幕編號、首末頁」五項的差別,並解釋為什麼老師說「都用 TYPE 1 寫程式比較方便」。
  2. 第 41 練習(螢幕設計到實作):建立一支空程式,在螢幕 2001 上貼「NCU SAP!」的 Text 與一個按鈕,按下就 LEAVE PROGRAM
    要點:畫面要有 OK_CODE 欄位、主程式要宣告同名變數、按鈕要填 FctCode、用 CALL SCREEN 2001 進入。
  3. 第 02 節(螢幕功能表):用 SE41 建 GUI Status 與 GUI Title,做出 MENU11/MENU12/MENU21/MENU22/ZCAS/EXIT 六個功能碼,並讓 ZCAS 拋 E 類訊息。
    延伸:驗證「兩個 PF-STATUS 中同名的 Function Code 會走同一段程式」。
  4. E 類按鈕:故意把 CANCEL 的 Function Type 留空白,再讓某個欄位檢查失敗,體驗「跳不出去」;然後改成 E 並把 AT EXIT-COMMAND 移到 PAI 最前面。
  5. 第 03 節(Radio / Box / Group):貼兩個以上 Radio 並 Define group;把 FIELD2、FIELD3 設 Group1 = 'SEL' 使其不可輸入,FIELD1 設為必輸。
    要點:LOOP AT SCREEN + MODIFY SCREEN,判斷值大寫。
  6. 第 04 節(維護 Z00_VBAK):用 Screen 顯示 Z00_VBAK,先用 TABLES 寫法、再改成自宣告工作區的寫法,比較「欄位抬頭是否自動出現」。
    思考:為什麼老師說「筆數大的表直接放 Screen 上是不妥的做法」?
  7. 第 05 節(合理性檢查):加入 FLD1/FLD2/FLD3 三個欄位,分別用 ON REQUEST、ON INPUT、無附加條件觸發 CHECK1~CHECK3,觀察三者觸發時機的差異。
    延伸:把三個欄位包成 CHAIN,改用 ON CHAIN-REQUEST,比較「檢查失敗時哪些欄位還能輸入」。
  8. 第 06 節(SE91):建立訊息類別 YMESS,寫出 MESSAGE e005(ymess) WITH …MESSAGE ID … TYPE … NUMBER … DISPLAY LIKE 'I' 兩種寫法,並說明 S/I/W/E 在「Screen 100 → Screen 200」情境下各自何時出現。
  9. 第 07 節(下拉選單):用 VRM_SET_VALUES 做出 TWD/USD 的 Listbox with key,再改成直接從 TCURC(或自建的 Z00_CURR)撈值。
    思考:什麼時候該改用 Search Help 而不是 Dropdown?
  10. 第 08 節(Subscreen):建立 2002/2003 兩個 Subscreen,用功能表 MENU11/MENU12 切換 gv_screenid
    要點:CALL SUBSCREEN 不可放進 LOOP/CHAIN;PBO 要帶 INCLUDING、PAI 不帶。
  11. Subscreen 七條鐵律:不看筆記默寫出來(不可放 LOOP/不可放 CHAIN/不必 OK_CODE/名稱不可重複/不會有 AT EXIT-COMMAND/不會有自己的 Titlebar 與 PF-STATUS 與 SET/LEAVE SCREEN/Dynpro Type 必須是 Subscreen)。
  12. 第 09 節(Tabstrip):用精靈建立 Tabstrip,觀察自動產生的三支 INCLUDE(常數 C_TAB、CONTROLS TAB、G_TAB 結構、TAB_ACTIVE_TAB_SET/GET)。
    延伸:分別用「本地捲動(Function Type = P,一頁籤一區)」與「伺服器捲動(Function Type 空白,共用一區)」各做一次,比較差異。
  13. 第 10 節(Custom Control):在 Custom Control 中放入 CL_GUI_TEXTEDIT,並在 PAI 用 flush + get_text_as_r3table 把內容取回內表。
    延伸:改掛 CL_GUI_ALV_GRID、CL_GUI_PICTURE、CL_GUI_HTML_VIEWER 各一次。
  14. 第 11 節(SMW0):用 SMW0 上傳一張公司 Logo,並用 DP_PUBLISH_WWW_URL + CL_GUI_PICTURE 顯示在畫面上。(需 BASIS 先配置。)
  15. 第 12 節(TC Wizard):建立 Z202202_41_TABLE,用 Table Control Wizard 以 SPFLI 為例做出可捲動的表格,並在主程式撈 1000 筆後 CALL SCREEN 2000。
    要點:精靈的第 7 步一定要勾「插入/刪除/全選/取消全選」,才會產生對應 FORM。
  16. 第 13 節(不靠精靈):再建一個 dynnr 3000,改用一般的 Table Control(不是 Wizard),從 Dictionary 選欄位貼進去,然後手動把 TOP/PBO/PAI/FORM 四段程式補齊。
  17. 第 14 節(驗證所學):打開標準交易 OB58(財務報表版本維護),用 F1 → 技術資訊找出它的程式與螢幕編號,再用 SE51 觀察它的 Layout 與 flow logic,指出它用了哪些本章學過的元件。
  18. 第 15 節(Z00_VBAK Table Control):把 Z00_VBAK 做成表控制項,支援新增、刪除、存檔,並加上 ENQUEUE/DEQUEUE。
    要點:先在 SE11 建一個含 MARK 欄的 Structure,Table Control 才吃得下。
  19. 第 16 節(ICON):完成 ZICON,用 ICON_CREATE 顯示 ICON_FAILURE,並找出講義原程式的錯誤(name 前面多一個空白)。
    延伸:改用型別池 icon 的常數(icon_led_green 等),並到 SE16N 查資料表 ICON 找出五個常用圖示的 ID。
  20. 第 18 節(Group / MODIF ID):完成 ZRADIO 與 ZMODIFYID,做出「選自製 → 開放物料欄位;選採購 → 開放採購單號;勾核取方塊 → 才出現備註」。
    要點:Modif ID 最多 3 碼、判斷值一律大寫、要有 USER-COMMAND 才會即時重繪、SCREEN-INPUT 與 SCREEN-ACTIVE 的差別。
  21. 第 20 節(PAI 觸發實測):用 /h 進 Debug,逐一測試「水平捲動、調欄寬、移動欄位、選取行列、垂直捲動、存版面」哪些會觸動 PAI,把結果寫下來。
  22. Table Control 功能碼:說出 TC 名稱為 TC_MARD 時,插入列、刪除列、下一頁、全選的功能碼各是什麼,並解釋 USER_OK_TC 是怎麼用 SEARCHSTRLEN 把動作切出來的。
  23. 三大 bug:不看筆記說出 Table Control 的三個經典 bug(沒設 lines/用 sy-tabix/沒 MODIFY)以及各自的症狀。
  24. 永久 vs 暫時:寫出一個範例,讓 Table Control 的「主鍵欄整欄唯讀」(永久性,改 tc-cols)而「偶數列的數量欄必填」(暫時性,LOOP 內改 screen)。
  25. 綜合實作:完成本章第 16 節的 ZTC_MARD_MAINT,並補上:(a) 排序按鈕、(b) 未存檔離開時的 POPUP_TO_CONFIRM、(c) 紅綠燈欄位、(d) ENQUEUE/DEQUEUE。
    加分:把存檔對象改成自建 Z 表,並比對 gt_orig 只送真正異動的列。
  26. 拆解 ME21N:打開 ME21N,指出畫面上哪些區塊是 Subscreen、哪些是 Tabstrip、哪些是 Table Control,並說明「左側導航欄 + HEADER + ITEM OVERVIEW + ITEM DETAIL」的四層結構分別對應本章哪一節的技術。
  27. S/4HANA 定位題:給定五個需求(Z 表維護、查詢報表、可編輯清單、單據式維護、要上手機),各自選出最合適的技術,並說明為什麼 ABAP Cloud 中不能用 Dynpro。

第 4-3 章 單據式表單設計:SmartForms(含 SAPscript / Adobe Forms 定位)

報表給人「看」,單據給人「印」。這一章把 SAP 的表單技術一次講清楚:從最老的 SAPscript(SE71)、目前 ERP 專案最常用的 SmartForms(SMARTFORMS),到 S/4HANA 官方主推的 Adobe Forms(SFP)與 Fiori Output Management。核心是老師的完整案例 Z00_EKKO_EKPO 採購單——自建兩個結構當接口、設計 Page/Window/Table/Template、寫 SMARTSTYLES 樣式、用 Program Lines 算合計、再用 SSF_FUNCTION_MODULE_NAME 動態取得函數名呼叫它。學完之後,你能做出一張可印、可預覽、可轉 PDF、可自動寄信的正式採購單,也知道遇到中文字型缺失、空白頁、金額不對齊、傳輸後函數名跑掉這些坑要怎麼救。

核心 TCODESMARTFORMSSMARTSTYLESSE71SFPSPADSE78SE73SO10SP01SP02NACEOPDSE11SE38SE37OAERI18NRSTXTRANSCOTSOSTSM59
關鍵物件SSF_FUNCTION_MODULE_NAME、SSFCTRLOP、SSFCOMPOP、SSFCRESCL、SSFCRESPD、CONVERT_OTF、CONVERT_OTF_2_PDF、READ_TEXT、SAVE_TEXT、CL_BCS、CL_DOCUMENT_BCS、CL_GUI_FRONTEND_SERVICES、SSF_OPEN/SSF_CLOSE、SSF_DOWNLOAD/SSF_UPLOAD、FP_JOB_OPEN/FP_FUNCTION_MODULE_NAME/FP_JOB_CLOSE、EKKO、EKPO、MAKT、T161/T161T、T001L、ADRC、USER_ADDR、STXH/STXL、TSP01

1. SAP 表單技術的演進與選用建議

SAP 從 R/2 時代到 S/4HANA,列印技術換過三代半。實務上你四種都會遇到,要能一眼分辨並知道該用哪個。

技術TCODE年代/定位優點缺點
SAPscript(Form / Layout Set)SE71(維護)
SE72(樣式)
SE78(圖檔)
I18N(多語入口)
最老。SAP 標準單據(發票、送貨單)很多還是它執行快、資源少、跨語言穩、標準表單多編輯器像文字模式、版面靠「視窗+段落」硬排、Client 相依(要 RSTXTRAN 才能傳輸)、多語言要一份份複製
SmartFormsSMARTFORMS
SMARTSTYLES
4.6C 起。目前 ECC/S/4 客製單據的主力圖形化設計、Table 節點自動分頁、不用寫排版指令、Client 獨立、產生 ABAP 函數效能好、可直接輸出 XSF/HTML格式太細緻時很難調、複雜表格會卡、沒有互動式表單、設計本身很花時間
Adobe Forms(Interactive Forms by Adobe)SFPNW 7.0 起,S/4HANA 官方建議方向真正的 PDF、可互動(可填、可簽核、可離線)、可寫 JavaScript、版面所見即所得(LiveCycle Designer)、Fragment 可重用需要 ADS(Adobe Document Services),通常裝在 Java Stack/Cloud,沒裝就完全不能用;互動式功能要另外授權
Fiori Output Management(新輸出管理)OPD
App: Manage Output Items / Output Parameter Determination
S/4HANA 1610 起BRF+ 決定輸出參數(取代部分 NACE),表單本體用 Adobe Forms,支援 Email/Printer/XML/IDoc 一致管線只有部分 App 走新輸出管理,其餘仍走 NACE;學習曲線高
老師的選用口訣
・改標準單據 → 先看它原本是什麼(SE71 就 SAPscript、SMARTFORMS 就 Smart Form),能複製標準的就不要重寫
・全新客製單據、公司沒裝 ADS → SmartForms
・要真 PDF、要互動、要離線填單、S/4HANA 新專案 → Adobe Forms
・「ADOBE FORMS 可寫 Script 能互動,但需要額外的插件;SMARTFORMS 不需插件,但太複雜的格式很難調校。」
時間預估要講在前面:老師課堂統計,光是把這張採購單做出來,大多數學員都需要四小時以上。SmartForms 是很費時間的開發工作,估工時千萬不要用「一支報表」的標準去估,一張稍有格式要求的單據抓 2~5 天是正常的。

2. 動手前的三個前置設定(SPAD/RSCPSETEDITOR/SE78)

2-1 SPAD:輸出設備與紙張格式

Smart Form 要印,就一定要有輸出設備(Output Device)SPAD 是 Spool 管理的總入口。(依講義步驟整理)

  • SPAD → 切到 Full administration(完整管理)才看得到全部頁籤。
  • Devices / servers → Output devices:新增印表機。關鍵欄位是 Device Type(裝置類型)與 Host spool access method(存取方式,本機測試常用 LC,前端印表機用 F)。
  • 裝置類型的選擇是中文能不能印出來的關鍵:Windows 前端印表機用 SWINSAPWIN;繁中/簡中環境用 CNSAPWINCNHP4;要輸出 Unicode PDF 用 PDFUC(一般 PDF 是 PDF1)。
  • Device types → Page formats:按 Display → 修改 → Page Formats → Create 可以新增特殊紙張格式(例如三聯式發票、A4 橫印、連續報表紙)。
  • A4 橫印=在 Page Format 建一筆寬 297mm × 高 210mm 的格式,再建對應的 Format type(例如 X_65_255),Smart Form 的 Form Attributes 才選得到。
預設可用的紙張:DINA4(直)、DINA4 橫需要自建或用 LETTERX_PAPER 系列。Smart Form 的 Page 節點只能選「已存在於該裝置類型」的格式,選不到就是 SPAD 沒建。

2-2 RSCPSETEDITOR:取消 MS Word 編輯器

SmartForms 的文字節點有兩種編輯器:PC Editor(SAP 內建)MS Word 編輯器。SAP HANA 平台之後預設強制走 MS Word。

老師的原話:「取消 MS Word 編輯器(SAP HANA 之後已強制預設為 MS Word,雖然網路上有教如何修改預設,但勿試)。」
意思是:知道有 RSCPSETEDITOR 這支程式(SE38 執行,可切換文字編輯器預設值)就好,正式系統不要亂動——它是 client 全域設定,改了全公司的 SO10、SmartForms、SAPscript 編輯器都跟著變,出問題很難回頭。真的要改請走變更流程並由 Basis 執行。

為什麼會想關掉 MS Word?因為用了 MS Word 編輯器之後:

  • 字體大小、段落格式會被 Word 蓋掉,不好設,跟 SMARTSTYLES 的段落格式打架。
  • 插入欄位(&FIELD&)的灰底標示行為不一樣,容易插成純文字而印不出值。
  • 沒裝 Office 或版本不合的 client 端會開不起來。
替代做法:在文字節點的編輯畫面選單 Goto → Change Editor,可以「當次」切回 PC Editor(行編輯器)。這比改系統參數安全得多,也是老師實際上課用的方法。

2-3 SE78:上傳 Logo 與背景圖

  • SE78(Form Graphics / SAPscript Graphics)→ 樹狀選 GRAPHICS → BMAP → BMAP(Bitmap Images) → 匯入按鈕。
  • 只吃 .BMP.TIF黑白(B/W)檔案小、印得快,彩色(Color)檔案大,會明顯拖慢列印。
  • 命名建議 ZLOGO_公司代碼,之後在 Smart Form 的 Graphic 節點選 Object=GRAPHICS、ID=BMAP、Name=ZLOGO_1000。
  • 另一條路是 OAER(Business Document Navigator):Class Name = PICTURES、Class Type = OT,可以掛 JPG/GIF 等 SE78 不吃的格式,Smart Form 用 Graphic 節點的「Business Document」來源引用。
Logo 太大是列印變慢的頭號原因。300dpi 的彩色 BMP 動輒好幾 MB,每頁都要送一次到 Spool。建議:黑白 BMP、寬度不超過 600 px、檔案 < 100 KB。

3. SMARTSTYLES:樣式先做,表單才好做

SMARTSTYLES 是「為報表預先設定好樣式」,等同 Word 的樣式表。先做樣式、再做表單,否則你會在每個文字節點手動調字體,改一次要改幾十個地方。老師在課堂上建立的樣式叫 Z00_EKKO_EKPO(與表單同名,方便對應)。

3-1 樣式的三個組成

節點內容重點
Header Data(標題資料)Standard Paragraph(預設段落)、Tab Stop 預設值、First Page(第一頁樣式)一定要指定 Standard Paragraph,否則文字節點沒有預設格式,Active 會出錯
Paragraph Formats(段落格式)2 碼代號(如 HC、B1、L1、AS)。設定對齊、縮排、行距、段前段後、Tab、編號、外框對齊:LEFT/RIGHT/CENTER/BLOCK(左右對齊)
Character Formats(字元格式)2 碼代號(如 B1、B2、C1)。設定字體家族、大小、粗體、斜體、底線、上下標、條碼字元格式可以套在段落中的一部分文字上(<B1>粗體字</>)
老師的關鍵提醒:「段落不要設定字體大小,免得失去彈性。」
段落只管排版(對齊、縮排、行距),字體大小交給字元格式。這樣同一個段落可以印大標題也可以印小備註,改字級只改字元格式一處。

3-2 課堂案例用到的格式

代號類型用途設定
HC段落報表抬頭「採購單」對齊 CENTER(置中),段後間距 2mm
AS段落一般內文(Standard Paragraph)對齊 LEFT,行距 1.2 LN
AR段落數量/金額欄對齊 RIGHT(數字一定要靠右)
AC段落項次、單位、工廠對齊 CENTER
B1字元大標題字字型 16pt Bold
B2字元表格內文字字型 10pt

3-3 中文字型(最容易踩的坑)

  • Smart Style 的字體家族清單來自 SE73(SAPscript Font Maintenance)的 System font families
  • Unicode 系統:中文用 ANDALE 系列(ANDALE_J/ANDALE_K/ANDALE_S/ANDALE_T,T 是繁體)或 SAPFONT;非 Unicode 中文系統傳統上用 CNSONG(中易宋體)。
  • 選了字型,還要看裝置類型有沒有掛這個字型——SE73 → Printer fonts → 選 Device Type → 看字型清單。沒掛就印出方框或空白。
  • 轉 PDF 中文變亂碼/方框,九成是輸出設備的 Device Type 用了 PDF1(非 Unicode),改成 PDFUCCNSAPWIN 就好。
「螢幕預覽正常、印出來變方框」是最典型的字型問題:預覽走的是 SAPGUI 的字型,實體列印走的是 Device Type 掛的印表機字型。驗收一定要真的印一張出來,或至少用目標裝置轉一次 PDF。

3-4 條碼(Barcode)

  • 條碼在 SAP 是「特殊的字元格式」:SE73System bar codes 建條碼定義 → 在 SMARTSTYLES 的字元格式裡的 Barcode 欄位選它 → 文字節點把那段欄位套上這個字元格式。
  • 舊式條碼(BC_CD39、BC_EAN13…)是「印表機條碼」,靠 Device Type 的印表機指令產生,換印表機就可能印不出來。
  • 新式條碼(New Bar Code Technology):SE73 建立時選 New,可用 BC_C128(Code128)、BC_C39BC_EAN13BC_PDF417BC_QR(QR Code,需支援的 kernel)。新式條碼由 SAP 自己算出圖形(PDL barcode),跨印表機穩定得多,新專案一律用新式
  • 條碼欄位常需要「不做轉換常式」,插入欄位時記得加 (K) 選項:&ls_head-ebeln(K)&,否則 ALPHA 轉換會把前導零吃掉,掃出來的號碼跟系統不一致。

4. 案例準備:建立兩個結構 Z00_EKKO、Z00_EKPO

單據的資料一定分兩層:檔頭(一筆)用 STRUCTURE,檔身(多筆)用 INTERNAL TABLE。這是所有單據表單的鐵則。所以在 SE11 先建兩個結構當作 Form Interface 的型別。

4-1 檔頭結構 Z00_EKKO

欄位資料元素說明來源
EBELNEBELN採購編號EKKO-EBELN
BUKRSBUKRS公司代碼EKKO-BUKRS
BSARTESART文件類型EKKO-BSART(值域來自 T161
BATXTBATXT類型名稱T161T-BATXT(要用 SPRAS 過濾)
LIFNRELIFN廠商編號EKKO-LIFNR
MC_NAME1AD_MC_NAME廠商名稱(搜尋用大寫名)ADRC/VIEW USER_ADDR
老師找欄位的方法:不知道文件類型的中文叫什麼,就先看 DOMAIN 的值域表 T161,再找對應的文字表 T161T。SAP 的慣例是「代碼表 + T 結尾=文字表」(T001/T001T、T024/T024D…)。廠商簡稱可從 ADRCUSER_ADDR(View,內含大量 Business Partner 相關資訊)、USR_ADPR 取得。

4-2 檔身結構 Z00_EKPO

欄位資料元素說明對齊
EBELPEBELP項次靠中
MATNRMATNR物料編號靠左
MAKTXMAKTX物料名稱(MAKT 取)靠左
MENGEBSTMG採購數量靠右
MEINSBSTME單位靠中
WERKSEWERK工廠靠中
LGORTLGORT儲存位置(值域參考 T001L靠中
LGOBELGOBE位置說明(T001L-LGOBE)靠左
QUAN 型別一定要設定單位參考欄位! MENGE 是 QUAN(數量),SE11 的 Currency/Quantity Fields 頁籤必須填 Reference table = 自己(Z00_EKPO)、Reference field = MEINS。金額欄(CURR)同理要指向 WAERS。
沒設會有兩個後果:(1) 結構啟動不了(Active 報錯);(2) 就算過了,Smart Form 印出來的數量小數位會錯(QUAN 的小數位是跟著單位走的)。老師特別提醒:「注意有 QUAN 要設定單位。記得要 ACTIVE。」

如果不想建自訂結構,也可以用 APPEND 結構或直接在 Form Interface 用標準表型別(EKKO、EKPO)。但老師建議自建:表單需要的欄位常常來自五、六張表(EKKO+T161T+ADRC+MAKT+T001L),一個扁平結構把它們全部裝起來,Smart Form 端才單純

5. Form Attributes 與 Form Interface(表單的接口)

SMARTFORMS → 輸入 Form 名稱 Z00_EKKO_EKPO → Create。左邊樹狀由上而下有四個固定節點:Global Settings(Form Attributes/Form Interface/Global Definitions)與 Pages and Windows

5-1 Form Attributes(表單屬性)

頁籤設定
General Attributes說明文字、Style(指定你的 SMARTSTYLES,例如 Z00_EKKO_EKPO)、Output Format(Standard Output/XSF/XML for Smart Forms)
Output OptionsPage format(A4 / A4 橫 / LETTER)、字元寬度 Characters per inch、行距 Lines per inch、Style(可覆寫)
Language AttributesTranslate(是否納入翻譯)、語言相依設定。做多國表單這裡要開
Form Interface下一小節
Page format 一旦選錯(A4 選成 LETTER),所有 Window 的座標都要重排。第一步就把紙張定下來,這是老師強調「設定紙張,A4」放在最前面的原因。

5-2 Form Interface(表單接口)

這就是「Smart Form 產生出來的那支函數,長什麼樣子」。四個頁籤完全對應 SE37 的函數介面:

頁籤本案例填什麼說明
ImportI_EKKO TYPE Z00_EKKO檔頭一筆。Pass Value 建議打勾(值傳遞,比較安全)
Export(本案例不用)要把表單裡算出來的東西回傳給程式時才用
TablesI_EKPO TYPE Z00_EKPO檔身多筆。注意:這裡填的是「列型別」,不是表型別
Exceptions(可自訂)系統自動附加 FORMATTING_ERROR、INTERNAL_ERROR、SEND_ERROR、USER_CANCELED
Tables 參數是舊式介面(帶 header line 的 obsolete 參數種類),但 Smart Forms 的接口就是這樣設計的,只能照用。在呼叫端則不受影響:呼叫時傳一個正常的 STANDARD TABLE 進去即可,不需要 WITH HEADER LINE。
如果你想避開 Tables,也可以把內表包成 Import 參數(型別=一個 TABLE TYPE),Smart Form 一樣能 LOOP,這是比較現代的做法。

5-3 Global Definitions(全域定義)

五個頁籤,是 Smart Form 裡「寫程式」的地方:

頁籤作用本案例
Global Data宣告表單內共用的變數/工作區。等同 FORM 內的 DATAWA_EKPO TYPE Z00_EKPO(檔身迴圈工作區)
LV_MENGE TYPE BSTMG(數量合計)
LV_CNT TYPE I(筆數)
Types自訂 TYPES(結構、表型別)要用 REDUCE/VALUE 建暫存表時很好用
Field Symbols宣告 FIELD-SYMBOLS動態存取欄位時用
Initialization表單一開始執行一次的程式碼。要在 Input/Output parameters 欄位登記你會讀/寫哪些變數清空合計、算筆數、補預設值
Form Routines寫 FORM ... ENDFORM 子程序,供 Program Lines 節點 PERFORM把複雜邏輯搬出來,表單樹才乾淨
Currency/Quantity Fields當「值欄位」與「幣別/單位欄位」不在同一張表時,在這裡明講對應關係見第 12 節

Initialization 節點的內容(注意:Smart Form 內的 Program Lines 是經典 ABAP 片段,不能用 REPORT/FORM 之外的語句,但內嵌宣告與新語法在 7.40+ 完全可用):

" === Global Definitions → Initialization ===
" Input parameters : I_EKKO, I_EKPO
" Output parameters: LV_MENGE, LV_CNT, GV_TITLE

CLEAR: lv_menge, lv_cnt.

" 筆數
lv_cnt = lines( i_ekpo ).

" 用 REDUCE 一次算出數量合計(比在 Table 節點的 Calculations 更彈性)
lv_menge = REDUCE bstmg( INIT s = CONV bstmg( 0 )
                         FOR <ls> IN i_ekpo
                         NEXT s = s + <ls>-menge ).

" 表頭抬頭字串(字串範本)
gv_title = |採購單 { i_ekko-ebeln ALPHA = OUT } (共 { lv_cnt } 項)|.
Initialization 的 Input parameters / Output parameters 兩欄一定要填,否則變數在節點裡「看得到但值是空的」。這是初學者最常卡住的地方——Smart Form 的 Program Lines 是封閉作用域,沒登記就傳不進去、也傳不出來。

6. Pages and Windows:頁面與視窗

Smart Form 的版面模型很簡單:一份表單有多個 Page,每個 Page 上擺多個 Window,Window 裡才放內容節點。老師的分法是三段:表頭 → 表身(Main Window,會 loop)→ 表尾

6-1 Page 節點

屬性說明實務設定
Next Page這一頁印滿後跳到哪一頁單頁循環:%PAGE1 的 Next Page 填 %PAGE1(指向自己),這樣資料多就自動一直加頁
Page Counter → Mode頁碼行為:Increase(+1)/Hold(不變)/Initialize(歸零重算)續頁用 Increase;附件頁若要獨立編號用 Initialize
Page counter → Numbering type阿拉伯數字/羅馬數字/字母一般 Arabic numerals
Output Options本頁的 Page format、Print mode(S=單面/D=雙面/T=Tumble)、Print attributes雙面列印才設 D
Conditions本頁在什麼條件下才輸出例如「只有 COPYCOUNT > 1 才印的存查聯」
「首頁不一樣、續頁只要表格」怎麼做:建 %PAGE1(有 Logo、有廠商資訊、有 Main Window)與 %PAGE2(只有簡化表頭 + Main Window + 表尾)。%PAGE1 的 Next Page = %PAGE2,%PAGE2 的 Next Page = %PAGE2。這是最常見的兩頁式結構。

6-2 Window 的四種類型

類型特性用途
MAIN(主視窗)每頁最多一個;內容印不完會自動續到下一頁的 MAIN;同一份表單所有頁的 MAIN 寬度必須一致放明細 Table 節點
Secondary(次視窗)固定位置固定大小,內容超出就被截掉,不會續頁表頭資訊、廠商地址、表尾簽核欄、備註
Copies Window(副本視窗)可設定「只在正本印」「只在副本印」「客戶聯/會計聯/存查聯」的差異文字
Final Window(最終視窗)整份表單處理完之後才輸出——此時總金額、總頁數才算得出來總計金額、「共 x 頁」、大寫金額
MAIN Window 的三條硬規則
1. 一頁只能有一個 MAIN。
2. 所有頁面的 MAIN 寬度必須相同,高度可以不同——寬度不同會在 Active 時報錯。
3. 一個 Page 若沒有 MAIN Window,表單處理到它就停住,不會再往下一頁(因為續頁機制是靠 MAIN 觸發的)。這就是「最後一頁印不出來」「資料只印第一頁」的最常見原因。

6-3 精確調整位置

三個視窗(表頭 Secondary、MAIN、表尾 Secondary)拖曳只能粗調,精確調整要在 Window 的 Output Options 頁籤直接輸入數值(依講義步驟整理)

  • Left Margin(左邊界)、Upper Margin(上邊界)、Window Width(寬)、Window Height(高)。
  • 單位建議統一用 MM(也可用 CM、TW、PT、CH、LN)。A4 直式可用範圍約 寬 190mm × 高 277mm(左右各留 10mm)。
  • 建議配置:表頭 Y=10 H=45、MAIN Y=57 H=180、表尾 Y=240 H=40。三者相加不要超過紙高,重疊會蓋字
  • Window 的 Conditions 頁籤可設「只在第一頁顯示」(SFSY-PAGE = 1)、「只在最後一頁顯示」(配合 Final Window)。

7. Template 與 Table:兩個長得像、用途完全不同的節點

這是 SmartForms 最容易搞混的一組。兩個都畫格子,差別在「列數固不固定」

比較項Template(範本)Table(表格)
列數固定,設計時就決定幾行幾列動態,隨內表筆數增減
是否會 LOOP不會(自帶 Data 頁籤的 LOOP)
是否自動跨頁不會(放 MAIN 也不會續頁),而且會自動重印 Header
結構只有格子Header / Main Area / Footer 三段
典型用途表頭欄位框、簽核欄、固定格式的收據格明細清單
放哪個 WindowSecondary Window(表頭/表尾)MAIN Window
一句話記法Template=固定格子(表頭表尾),Table=會長高的格子(明細)。老師的採購單就是這樣配:表頭 Secondary Window 放兩個 Template(報表抬頭、採購單頭),MAIN Window 放一個 Table。

7-1 Template 的操作要點

  • 建立:對 Window 按右鍵 → Create → Template
  • Table Painter(表格繪製器):按 Details 鈕可以逐行逐格輸入精確尺寸——這是老師強調的「每個 TEMPLATE 可以按 Details 鈕,做精確的尺寸設定」。
  • Line Type(%LTYPE1、%LTYPE2…):定義「一種列的欄寬組合」。同一個 Template 可以有多種 Line Type,例如 %LTYPE1 是抬頭(一整欄),%LTYPE2 是明細(六欄)。
  • 每個格子(Cell)要指定 Line 與 Column,Template 才知道文字要放哪一格。老師:「記得標明 Line 與 Column」——漏填是「東西不見了」的頭號原因。
  • Pattern(框線):在 Table Painter 用 Select Pattern 一次套用外框/內框/無框。

課堂案例的表頭四個欄位(在 Template 的格子裡各放一個 Text 節點):

格位內容(& 括起來的就是欄位)
L1 C1單據編號 &I_EKKO-EBELN&
L1 C2公司代碼 &I_EKKO-BUKRS&
L2 C1單據類型 &I_EKKO-BSART& &I_EKKO-BATXT&
L2 C2供應廠商 &I_EKKO-LIFNR& &I_EKKO-MC_NAME1&

7-2 Table 節點的四個頁籤

頁籤設定內容
DataInternal TableI_EKPO INTO WA_EKPO(來源內表 → 工作區)。可加 WHERE 條件、Sort Criteria(排序欄位,勾 Event on Sort Begin/End 可做小計)
Calculations自動計算欄。填 Operation(TOTAL/MAX/MIN/CNT)/Field name(來源欄位)/Target field name(放結果的全域變數)/Time(At the end 之類)。本案例:TOTAL、WA_EKPO-MENGELV_MENGE
Table(Table Painter)定義 Line Type 與欄寬
Table Line(Line Types)逐列定義:%LTYPE1(表頭列)、%LTYPE2(明細列)的欄數與寬度

Table 節點樹狀下面固定有三個資料夾:

  • Header:表格抬頭。跨頁時會自動重印(可在 Header 資料夾的屬性關掉 At Start of Table only)。放六個 Text 節點:項次/物料編號/採購數量/單位/工廠/儲存位置。
  • Main Area:明細列。每個格子的 Text 節點要取 WA_EKPO 的欄位(老師:「Main Area 每個 CELL 格位都有一個 TEXT,而且要取 WA_EKPO」——這是最常寫錯的地方,寫成 I_EKPO-MENGE 會印不出東西)。
  • Footer:表格結尾。合計列,數量合計取 &LV_MENGE&

課堂為了簡化,明細只顯示六欄(省略物料名稱與位置說明):

#欄位資料元素抬頭對齊
1WA_EKPO-EBELPEBELP項次靠中
2WA_EKPO-MATNRMATNR物料編號靠左
3WA_EKPO-MENGEBSTMG採購數量靠右
4WA_EKPO-MEINSBSTME單位靠中
5WA_EKPO-WERKSEWERK工廠靠中
6WA_EKPO-LGORTLGORT儲存位置靠中
數字沒對齊是驗收被退件的常見原因。解法有兩個,要一起用
1. 段落格式設 RIGHT(靠右)
2. 插入欄位時指定輸出長度並靠右:&WA_EKPO-MENGE(13)&&WA_EKPO-MENGE(R)&
只設段落靠右,遇到不同長度的數字仍會因為千分位與小數位而看起來歪。

8. Text 節點與「插入欄位」的完整規則

Text 節點是表單裡最多的節點。它有四種 Text Type:

Text Type說明使用場合
Text element直接在節點裡打字(PC Editor)絕大多數情況
Text module引用 SmartForms 的 文字模組(SMARTFORMS 的 Text Module 物件,Client 獨立、可翻譯條款、注意事項、公司抬頭,多張表單共用
Include textSAPscript 標準長文(SO10/單據長文)採購單抬頭文字、項目文字、客戶備註
Dynamic text執行期才決定要讀哪一段長文(名稱由變數給)依單據動態抓長文

8-1 Include Text 的四個關鍵欄位

Include Text 節點要填 Text Name / Text Object / Text ID / Language,這四個就是 STXH(長文抬頭表)的主鍵,也是 READ_TEXT 的四個參數。

參數意義採購單常見值
OBJECT文字物件(屬於哪個模組)EKKO(採購抬頭)、EKPO(採購項目)、TEXT(SO10 通用文字)、KNA1、VBBK
ID文字類型F01(Header text)、F02(Header note)、F03ST(SO10 標準文字)
NAME文字名稱=單據鍵抬頭:EBELN;項目:EBELN && EBELP(10+5=15 碼)
LANGUAGE語言SY-LANGU

找不到 ID/OBJECT 時的標準做法:到單據的長文編輯畫面(例如 ME23N 的 Texts 頁籤)→ 選單 Goto → Header,畫面會直接顯示 Text Name/Language/Text ID/Text Object。

" === 在程式端自己讀長文(Smart Form 之外也常用)===
DATA lt_line TYPE STANDARD TABLE OF tline WITH EMPTY KEY.

DATA(lv_name) = CONV thead-tdname( ls_head-ebeln ).

TRY.
    CALL FUNCTION 'READ_TEXT'
      EXPORTING
        id                      = 'F01'          " 文字 ID
        language                = sy-langu
        name                    = lv_name        " 採購單號
        object                  = 'EKKO'         " 文字物件
      TABLES
        lines                   = lt_line
      EXCEPTIONS
        id                      = 1
        language                = 2
        name                    = 3
        not_found               = 4
        object                  = 5
        reference_check         = 6
        wrong_access_to_archive = 7
        OTHERS                  = 8.

    IF sy-subrc = 0.
      " 用 REDUCE 把多行長文串成一個字串
      DATA(lv_text) = REDUCE string(
                        INIT t = ``
                        FOR <ls_line> IN lt_line
                        NEXT t = COND #( WHEN t IS INITIAL
                                         THEN CONV string( <ls_line>-tdline )
                                         ELSE |{ t }{ cl_abap_char_utilities=>newline }{ <ls_line>-tdline }| ) ).
    ENDIF.
  CATCH cx_root INTO DATA(lx_err).
    MESSAGE lx_err->get_text( ) TYPE 'S' DISPLAY LIKE 'W'.
ENDTRY.
長文是 Client 相依的(STXH/STXL 有 MANDT)。SO10 建立的標準文字不會跟著 Request 自動傳輸,必須用 RSTXTRAN 把它掛進傳輸請求,或在目標環境重建。「開發機好好的,測試機文字不見了」九成是這個原因。
相對地,SmartForms 的 Text Module 是 Repository 物件、Client 獨立、會跟著 Request 走——能用 Text Module 就不要用 SO10

8-2 插入欄位(Including Fields)的規則

這一段來自 SAP 官方文件,老師特別整段收錄,因為「畫報表是苦差事,若遇到要求細緻的表,一定要仔細閱讀原廠的文件」

  • 欄位必須用 & 前後包起來&I_EKKO-EBELN&
  • 插入方式三選一:(1) 從左邊 Field List 拖曳(Drag&Drop)到游標處;(2) PC Editor 工具列的 Include field(另有 Change field/Delete field);(3) 直接打字後做 local check
  • 成功的唯一判準:欄位會顯示成灰底。 老師原話:「一定要變灰色,才表示有成功哦」「手冊上有些欄位的插入方法是錯誤的,記得只要是欄位一定要是灰色的,就是了」。
  • 你自己的欄位必須先定義成 Global DataForm Interface 參數,才插得進去。
  • 名稱規則:不得含空白;不得含 '+()(這些是格式化選項的保留字元);最長 130 字元但只有前 32 字元有辨識力;不分大小寫;結構欄位用 減號 連接(KNA1-NAME1)。
  • 如果 Text 節點引用了 Text module 或 Include text,被引用文字裡的欄位也會一起被複製進來並解析

8-3 系統欄位 SFSY

表單內可用的欄位字串是 SFSY(不是 SY!)。規則:可以顯示任何 SFSY 欄位;不可以在 Program Lines 裡指派值給 SFSY;條件式可以查詢所有 SFSY 欄位,但 SFSY-FORMPAGES 只能在 Final Window 內查詢、SFSY-JOBPAGES 完全不能查詢

欄位意義
&SFSY-DATE&日期(格式依使用者主資料)
&SFSY-TIME&時間 HH:MM:SS
&SFSY-PAGE&目前頁碼(格式由 Page 節點的 Page counter 決定)
&SFSY-FORMPAGES&本份表單總頁數 → 「第 x 頁/共 y 頁」
&SFSY-JOBPAGES&整個列印請求(多份表單合併)的總頁數
&SFSY-WINDOWNAME&目前 Window 名稱
&SFSY-PAGENAME&目前 Page 名稱
&SFSY-XSF&輸出格式是 XSF/HTML 時為 'X'。用來在條件式裡隱藏「只有畫面才需要」的元素(例如按鈕)
&SFSY-COPYCOUNT&1=正本、2=第一份副本、3=第二份副本…
&SFSY-COPYCOUNT0&0=正本、1=第一份副本…(差 1)
SFSY-SUBRC可在 Program Lines 查詢。目前只對 include text/text module 有效:0=找到並印出,4=找不到
SFSY-USERNAME執行列印的使用者登入名稱
SFSY-FORMPAGES/SFSY-JOBPAGES 很吃記憶體:為了算出總頁數,系統必須把所有輸出頁面留在主記憶體中,直到整份表單/整個列印工作結束。大量列印(例如一次印五千張出貨單)用「第 x 頁/共 y 頁」可能直接把記憶體吃爆。大批次列印請避開 JOBPAGES。

8-4 欄位的格式化選項(Formatting Options)

選項寫在欄位名稱後面,大寫字母,部分可組合。數值欄位的評估順序:(<length>) → < → L → C → R → F;字元欄位:C → +<offset> 與 (<length>) → R → F單一欄位輸出上限 255 字元。

語法作用
&f+<offset>&跳過前 n 個字元(僅字元欄位)。offset 大於值長度則不顯示
&f(<length>)&設定輸出長度
&f(*)&用 DDIC 定義的輸出長度
&f(S)&不印正負號
&f(<)&負號印在數字左邊(台灣報表必用,SAP 預設負號在右邊)
&f(.N)&限制小數位數為 N
&f(EN)&以固定指數 N 顯示
&f(T)&不印千分位(DEC/CURR/INT/QUAN)
&f(Z)&去除前導零
&f(I)&初始值不顯示(0 或空白就印空的)
&f(K)&停用 DDIC 的轉換常式(條碼、要原始值時用)
&f(R)&靠右對齊(必須同時指定輸出長度才有效
&f(F*)&左側空白補上填充字元(例如 &f(F0)& 補零)
&f(C)&壓縮空白,效果同 ABAP 的 CONDENSE
&f(L)&日本地區日期格式(JPDAT),只在日文版用

常用組合實例:&WA_EKPO-MENGE(13.3<)&=寬 13、三位小數、負號在左;&I_EKKO-EBELN(Z)&=採購單號去前導零;&I_EKKO-LIFNR(K)&=廠商編號印原始值。

9. 其他節點:Address / Graphic / Folder / Alternative / Loop / Command / Program Lines

節點用途關鍵設定
Address(地址)各國郵政格式自動排版地址,不用自己拼字串Address type:Organization(給 ADRNR,例如廠商 LFA1-ADRNR)/Person(ADRNP)/Workplace。可設 Number of lines、Priority(哪幾行優先保留)、Recipient language
Graphic(圖形)Logo、印章、浮水印、背景Object=GRAPHICS、ID=BMAP、Name=SE78 上傳的名稱;Color/Black&White;Resolution;可選 Background picture(滿版背景)
Folder(資料夾)把數個節點群組,並可整組設條件、整組不分頁(Page Protection)Output Options 勾 Page Protection=這一組要嘛整組印在同一頁,要嘛整組移到下一頁
Alternative(替代)IF-ELSE。底下自動產生 TRUE / FALSE 兩個資料夾Conditions 頁籤寫條件;比在每個節點各寫一次條件清楚得多
Loop(迴圈)對內表迴圈,但不畫格子(Table 才畫格子)Data 頁籤:Internal Table … INTO …、WHERE、Sort Criteria。適合「一筆一段文字」的排版
Command(命令)下列印控制指令四大功能:Go to New Page(強制換頁)、Reset paragraph numberingSpool control(新增 spool request/改 spool 屬性)、Print control(送印表機指令)
Program Lines(程式行)在表單中間寫 ABAP必填 Input parameters / Output parameters;不能寫 CALL SCREEN、COMMIT WORK 等會破壞輸出流程的語句
Complex SectionLoop + Program Lines + Template 的組合節點較少用,能用 Table 就用 Table
強制換頁的標準做法:在 MAIN Window 內插入 Command 節點 → 勾 Go to New Page → 指定 New page = %PAGE2。再把 Command 節點的 Conditions 設成「每 10 筆換一頁」或「客戶代碼改變時換頁」,就能做出「一個客戶一張單」的批次列印。

Program Lines 節點的典型內容(放在 Table 的 Main Area 之前,用來做逐列加工):

" === Program Lines:明細列前的加工 ===
" Input  parameters: WA_EKPO, I_EKKO
" Output parameters: GV_ROW_COLOR, GV_AMT_TXT, LV_MENGE

" 1) 交錯底色(配合 Table Line 的兩種 Line Type 使用)
gv_row_color = COND char1( WHEN sy-tabix MOD 2 = 0 THEN 'X' ELSE space ).

" 2) 數量超量標紅字用的旗標
gv_over = COND char1( WHEN wa_ekpo-menge > 1000 THEN abap_true ELSE abap_false ).

" 3) 單位描述(用 T006A 取中文單位)
SELECT SINGLE msehl FROM t006a
  WHERE spras = @sy-langu
    AND msehi = @wa_ekpo-meins
  INTO @gv_unit_txt.

" 4) 若沒有物料名稱就補一段預設文字
gv_maktx = COND maktx( WHEN wa_ekpo-maktx IS INITIAL
                       THEN |(無物料主檔說明)|
                       ELSE wa_ekpo-maktx ).
Program Lines 裡不要下 SELECT 進迴圈! 上面第 3 點的寫法是為了示範語法;真的要用,請在 Initialization 一次把 T006A 全撈進一個 HASHED TABLE,Program Lines 只做 lt_t006a[ msehi = wa_ekpo-meins ] 查表。1000 筆明細=1000 次 DB 存取,列印會慢到被使用者投訴。

10. 產生的函數模組:為什麼一定要用 SSF_FUNCTION_MODULE_NAME

Smart Form 按 Active(啟動)時,系統會產生一支真正的函數模組,名稱長得像 /1BCDWB/SF00000009。你的程式其實是在呼叫這支函數。

老師的第一版程式(08-1)是直接寫死函數名稱——這是教學用的第一步,正式程式絕對不能這樣寫

" 反例(錯誤示範):寫死產生的函數名(僅供理解用,正式程式禁用)
CALL FUNCTION '/1BCDWB/SF00000009'   " ← 每張報表的函數都不相同
  EXPORTING i_ekko = ls_head
  TABLES    i_ekpo = lt_item
  EXCEPTIONS formatting_error = 1
             internal_error   = 2
             send_error       = 3
             user_canceled    = 4
             OTHERS           = 5.
為什麼寫死一定會出事:
1. 這個編號是各系統各自產生的流水號。表單傳輸到 QAS/PRD 之後,那邊重新產生的函數名幾乎一定不同(也可能相同,但你不能賭)。
2. 只要有人重新 Active 一次表單,函數就可能重新產生新編號
3. 系統升級、SPAU 調整之後也會重生。
→ 結果就是「開發機好好的,上線就 dump:CALL_FUNCTION_NOT_FOUND」。
正確做法:一律用 SSF_FUNCTION_MODULE_NAME,用「表單名稱」換「函數名稱」。
" 正解:動態取得函數名稱
DATA lv_fm_name TYPE rs38l_fnam.

CALL FUNCTION 'SSF_FUNCTION_MODULE_NAME'
  EXPORTING
    formname           = 'Z00_EKKO_EKPO'   " ← 表單名稱(這個才是穩定的)
*   variant            = ' '
*   direct_call        = ' '
  IMPORTING
    fm_name            = lv_fm_name
  EXCEPTIONS
    no_form            = 1
    no_function_module = 2
    OTHERS             = 3.

IF sy-subrc <> 0.
  MESSAGE |找不到表單 Z00_EKKO_EKPO 或其函數尚未產生(RC={ sy-subrc })| TYPE 'E'.
ENDIF.

" 之後就用變數呼叫
CALL FUNCTION lv_fm_name
  EXPORTING i_ekko = ls_head
  TABLES    i_ekpo = lt_item
  EXCEPTIONS formatting_error = 1
             internal_error   = 2
             send_error       = 3
             user_canceled    = 4
             OTHERS           = 5.
把表單名稱做成 PARAMETERS 或 TVARVC 常數,不要寫死在程式裡;這樣同一支列印程式可以切換「正式版/簡化版/英文版」三張表單,維運只要改變式。

11. 完整的呼叫程式(現代 ABAP 改寫版)

講義的原程式 Z202202_60 用了 SELECT *INTO CORRESPONDING FIELDS OF TABLEREAD TABLE … MODIFY 這些舊寫法。以下是 S/4HANA 1809 以後的現代寫法,邏輯完全等價但更快也更安全。

*&---------------------------------------------------------------------*
*& Report Z00_SF_PO_PRINT ── 採購單 SmartForm 列印(現代版)
*&---------------------------------------------------------------------*
REPORT z00_sf_po_print.

CONSTANTS gc_formname TYPE tdsfname VALUE 'Z00_EKKO_EKPO'.

PARAMETERS: p_ebeln TYPE ekko-ebeln OBLIGATORY DEFAULT '4500000001',
            p_prev  TYPE abap_bool  AS CHECKBOX DEFAULT 'X'.   " 預覽

DATA: gs_head TYPE z00_ekko,
      gt_item TYPE STANDARD TABLE OF z00_ekpo WITH EMPTY KEY.

*----------------------------------------------------------------------*
START-OF-SELECTION.
  PERFORM get_data.
  IF gt_item IS INITIAL.
    MESSAGE |採購單 { p_ebeln ALPHA = OUT } 查無項目資料| TYPE 'S' DISPLAY LIKE 'E'.
    RETURN.
  ENDIF.
  PERFORM print_form.

*&---------------------------------------------------------------------*
FORM get_data.

  " ── 檔頭:明列欄位、@ 逸出主機變數 ──────────────────────────
  SELECT SINGLE ebeln, bukrs, bsart, lifnr
    FROM ekko
    WHERE ebeln = @p_ebeln
    INTO @DATA(ls_ekko).

  IF sy-subrc <> 0.
    MESSAGE |採購單 { p_ebeln ALPHA = OUT } 不存在| TYPE 'E'.
  ENDIF.

  gs_head = CORRESPONDING #( ls_ekko ).

  " ── 單據類型名稱(T161T,一定要帶語言)────────────────────
  SELECT SINGLE batxt
    FROM t161t
    WHERE spras = @sy-langu
      AND bstyp = 'F'
      AND bsart = @gs_head-bsart
    INTO @gs_head-batxt.

  " ── 廠商名稱(LFA1 → ADRC)──────────────────────────────
  SELECT SINGLE l~adrnr, a~name1
    FROM lfa1 AS l
    INNER JOIN adrc AS a ON a~addrnumber = l~adrnr
    WHERE l~lifnr = @gs_head-lifnr
    INTO @DATA(ls_vendor).
  gs_head-mc_name1 = ls_vendor-name1.

  " ── 檔身:排除刪除註記 ────────────────────────────────────
  SELECT ebelp, matnr, menge, meins, werks, lgort
    FROM ekpo
    WHERE ebeln = @p_ebeln
      AND loekz = @space
    ORDER BY ebelp                     " 一定要排序,SELECT 不保證順序
    INTO CORRESPONDING FIELDS OF TABLE @gt_item.

  CHECK gt_item IS NOT INITIAL.        " FOR ALL ENTRIES 空表陷阱防護

  " ── 物料名稱:一次撈成 SORTED TABLE,避免迴圈內 SELECT ──────
  SELECT matnr, maktx
    FROM makt
    FOR ALL ENTRIES IN @gt_item
    WHERE matnr = @gt_item-matnr
      AND spras = @sy-langu
    INTO TABLE @DATA(lt_makt).

  " ── 儲存位置說明 ─────────────────────────────────────────
  SELECT werks, lgort, lgobe
    FROM t001l
    FOR ALL ENTRIES IN @gt_item
    WHERE werks = @gt_item-werks
      AND lgort = @gt_item-lgort
    INTO TABLE @DATA(lt_lgort).

  " ── 用 FIELD-SYMBOL 就地補值(不必 MODIFY)──────────────────
  LOOP AT gt_item ASSIGNING FIELD-SYMBOL(<ls_item>).
    <ls_item>-maktx = VALUE #( lt_makt[ matnr = <ls_item>-matnr ]-maktx OPTIONAL ).
    <ls_item>-lgobe = VALUE #( lt_lgort[ werks = <ls_item>-werks
                                         lgort = <ls_item>-lgort ]-lgobe OPTIONAL ).
  ENDLOOP.

ENDFORM.
老師原程式的三個陷阱,改寫時一定要處理:
1. SELECT SINGLE BATXT FROM T161T WHERE BSART = … 沒有帶 SPRAS!T161T 的主鍵是 SPRAS+BSTYP+BSART,不帶語言會隨機抓到英文或德文,也可能因為SELECT SINGLE 對非唯一鍵沒有排序保證而每次結果不同。
2. FOR ALL ENTRIES 前面必須確定驅動內表不是空的(原程式有做 IF LT_EKPO IS NOT INITIAL,很好,要保留);空表會變成「無 WHERE 條件」全表掃描。
3. SELECT * FROM EKPO 在 S/4HANA 上 EKPO 有 300+ 欄位,全撈是嚴重浪費。一律明列欄位。

接著是列印段,先用最單純的方式(讓 SAP 跳出列印對話框):

*&---------------------------------------------------------------------*
FORM print_form.

  DATA: ls_ctrl   TYPE ssfctrlop,      " 控制參數
        ls_output TYPE ssfcompop.      " 輸出選項

  " 取得動態函數名
  CALL FUNCTION 'SSF_FUNCTION_MODULE_NAME'
    EXPORTING  formname           = gc_formname
    IMPORTING  fm_name            = DATA(lv_fm)
    EXCEPTIONS no_form            = 1
               no_function_module = 2
               OTHERS             = 3.
  IF sy-subrc <> 0.
    MESSAGE |表單 { gc_formname } 尚未啟動或不存在| TYPE 'E'.
  ENDIF.

  ls_ctrl-no_dialog = COND #( WHEN p_prev = abap_true THEN abap_false ELSE abap_true ).
  ls_ctrl-preview   = p_prev.          " 螢幕預覽
  ls_ctrl-langu     = sy-langu.

  ls_output-tddest  = 'LOCL'.          " 輸出裝置
  ls_output-tdimmed = abap_true.       " 立即列印
  ls_output-tdnewid = abap_true.       " 產生新的 spool request
  ls_output-tdcopies = 1.

  CALL FUNCTION lv_fm
    EXPORTING
      control_parameters = ls_ctrl
      output_options     = ls_output
      user_settings      = abap_false  " ← 不要用使用者預設印表機
      i_ekko             = gs_head
    TABLES
      i_ekpo             = gt_item
    EXCEPTIONS
      formatting_error   = 1
      internal_error     = 2
      send_error         = 3
      user_canceled      = 4
      OTHERS             = 5.

  CASE sy-subrc.
    WHEN 0.  MESSAGE |採購單 { p_ebeln ALPHA = OUT } 已送出列印| TYPE 'S'.
    WHEN 4.  MESSAGE '使用者取消列印' TYPE 'S' DISPLAY LIKE 'W'.
    WHEN OTHERS.
      MESSAGE |列印失敗,RC = { sy-subrc }| TYPE 'E'.
  ENDCASE.

ENDFORM.
USER_SETTINGS = 'X' 是預設值,也是最常見的「印到別人印表機」元凶。 它會用使用者主資料(SU3)裡的預設印表機覆蓋你在 OUTPUT_OPTIONS 設的 TDDEST。批次列印一律設 user_settings = abap_false,才會照程式指定的印表機走。

12. 輸出控制:CONTROL_PARAMETERS 與 OUTPUT_OPTIONS 完整對照

12-1 CONTROL_PARAMETERS(型別 SSFCTRLOP)

欄位作用
NO_DIALOG'X'=不顯示列印對話框。背景執行必設
PREVIEW'X'=顯示列印預覽
GETOTF'X'=不真的印,把 OTF 資料回傳到 JOB_OUTPUT_INFO-OTFDATA。轉 PDF、寄信都靠它
NO_OPEN / NO_CLOSE多份表單合併到同一個 Spool 時使用,配合 SSF_OPEN/SSF_CLOSE
DEVICE'PRINTER'(預設)/'TELEFAX'/'MAIL'/'SCREEN'
LANGU表單輸出語言
REPLANGU1/2/3找不到指定語言時的替代語言
STARTPAGE指定從哪一頁開始輸出
GETOTF = 'X' 時,NO_DIALOG 也要一起設 'X',否則背景執行會卡在對話框,Job 一直不結束。而且 GETOTF 開了就不會產生 Spool——想「既存 Spool 又拿 OTF」要跑兩次,或先拿 OTF 再自己用 RSPO_SR_OPEN/WRITE 建 Spool。

12-2 OUTPUT_OPTIONS(型別 SSFCOMPOP)

欄位作用
TDDEST輸出裝置(印表機代號,如 LOCL、LP01)
TDIMMED'X'=立即列印(Print immediately)
TDDELETE'X'=列印後刪除 Spool
TDNEWID'X'=建立新的 Spool Request(不要跟前一份併在一起)
TDCOPIES份數
TDNOPRINT'X'=只產生 Spool 不真的印
TDFINALSpool 是否標記為已完成
TDTITLESpool 標題(在 SP01 顯示,強烈建議帶單號方便追查)
TDLIFETIMESpool 保留天數(預設 8)
TDCOVER / TDCOVTITLE是否印封面頁與封面標題
TDARMOD1=只印、2=只歸檔、3=印+歸檔(ArchiveLink)
TDPRINTERDevice type('SWIN'、'CNSAPWIN'、'PDFUC'…)
XSF / XSFFORMAT / XSFOUTMODE / XSFOUTDEVXSF/HTML 輸出用

12-3 多份表單合併成一個 Spool

" 一次印 100 張採購單,但只產生「一個」Spool Request
DATA: ls_ctrl   TYPE ssfctrlop,
      ls_output TYPE ssfcompop.

ls_ctrl-no_dialog = abap_true.
ls_output-tddest  = 'LOCL'.
ls_output-tdnewid = abap_true.
ls_output-tdimmed = abap_false.

" 1) 開啟一次列印工作
CALL FUNCTION 'SSF_OPEN'
  EXPORTING
    output_options     = ls_output
    control_parameters = ls_ctrl
    user_settings      = abap_false
  EXCEPTIONS
    formatting_error   = 1
    internal_error     = 2
    send_error         = 3
    user_canceled      = 4
    OTHERS             = 5.

" 2) 每份表單都設 no_open / no_close
ls_ctrl-no_open  = abap_true.
ls_ctrl-no_close = abap_true.

LOOP AT gt_po_list INTO DATA(ls_po).
  PERFORM get_data USING ls_po-ebeln.

  CALL FUNCTION lv_fm
    EXPORTING control_parameters = ls_ctrl
              output_options     = ls_output
              user_settings      = abap_false
              i_ekko             = gs_head
    TABLES    i_ekpo             = gt_item
    EXCEPTIONS OTHERS            = 5.
ENDLOOP.

" 3) 關閉列印工作,此時才真的產生 Spool
CALL FUNCTION 'SSF_CLOSE'
  IMPORTING  job_output_info = DATA(ls_job_info)
  EXCEPTIONS OTHERS          = 3.

MESSAGE |已產生 Spool: { ls_job_info-spoolids[ 1 ] }| TYPE 'S'.
批次列印一定要用 SSF_OPEN/SSF_CLOSE。不用的話一百張單就會產生一百個 Spool Request,SP01 完全沒辦法管理,印表機也會一直重新暖機。

12-4 幣別/數量欄位在不同表時的對應

SAP 官方規則:值欄位與幣別/單位欄位在同一張表時,Smart Forms 會自動辨識並正確格式化;不在同一張表時,你必須明講。做法(在 Global Definitions → Currency/Quantity Fields 頁籤):

  1. 在 Global Data 定義一個與「存幣別的那張表」同型別的工作區,例如 WA_CURTAB;以及輸出值欄位的工作區 WA_VALTAB
  2. 在 Currency/Quantity Fields 填:Field name = WA_VALTAB-VALUEReference field = WA_CURTAB-CURRENCY
  3. Table/Loop 節點的 Data 頁籤用 WA_VALTAB 當工作區。
  4. 在輸出該欄位之前插入一個 Program Lines 節點,把幣別/單位指派WA_CURTAB-CURRENCY(值可在表單裡 SELECT,或由 Form Interface 傳進來)。
" === Program Lines:放在輸出金額欄位之前 ===
" Input parameters : I_EKKO
" Output parameters: WA_CURTAB

" 讓 WA_VALTAB-VALUE 依採購單的幣別格式化(小數位數才會對)
wa_curtab-currency = i_ekko-waers.
金額小數位印錯幾乎都是這個問題。JPY(0 位)、TWD(2 位)、KWD(3 位)的小數位數不同,SAP 內部一律存兩位,靠幣別欄位換算。沒掛幣別參考,日圓 1000 會印成 10.00

13. 轉 PDF、下載、寄 E-mail(完整樣板)

這是 SmartForms 在實務上最常被要求的加值功能:不要印紙,直接寄 PDF 給廠商。流程固定四步:GETOTF 取 OTF → CONVERT_OTF 轉 PDF → 二進位轉 SOLIX → CL_BCS 寄出

13-1 取得 OTF 並轉 PDF

*&---------------------------------------------------------------------*
*& 取 OTF → 轉 PDF(回傳 XSTRING 與 SOLIX 表)
*&---------------------------------------------------------------------*
FORM build_pdf CHANGING cv_pdf     TYPE xstring
                        ct_solix   TYPE solix_tab
                        cv_bytes   TYPE i.

  DATA: ls_ctrl   TYPE ssfctrlop,
        ls_output TYPE ssfcompop,
        ls_result TYPE ssfcrescl,          " job_output_info
        lt_otf    TYPE STANDARD TABLE OF itcoo WITH EMPTY KEY,
        lt_pdf    TYPE STANDARD TABLE OF tline WITH EMPTY KEY.

  CALL FUNCTION 'SSF_FUNCTION_MODULE_NAME'
    EXPORTING  formname           = gc_formname
    IMPORTING  fm_name            = DATA(lv_fm)
    EXCEPTIONS no_form            = 1
               no_function_module = 2
               OTHERS             = 3.
  IF sy-subrc <> 0.
    MESSAGE |取不到表單函數| TYPE 'E'.
  ENDIF.

  " ── 關鍵三個開關 ──────────────────────────────────
  ls_ctrl-no_dialog = abap_true.     " 不跳對話框
  ls_ctrl-getotf    = abap_true.     " 要 OTF,不要真的印
  ls_ctrl-preview   = abap_false.
  ls_ctrl-langu     = sy-langu.

  ls_output-tddest   = 'LOCL'.       " 仍要給裝置,決定字型與紙張
  ls_output-tdnoprev = abap_true.

  CALL FUNCTION lv_fm
    EXPORTING
      control_parameters = ls_ctrl
      output_options     = ls_output
      user_settings      = abap_false
      i_ekko             = gs_head
    IMPORTING
      job_output_info    = ls_result       " ← OTF 在這裡
    TABLES
      i_ekpo             = gt_item
    EXCEPTIONS
      formatting_error   = 1
      internal_error     = 2
      send_error         = 3
      user_canceled      = 4
      OTHERS             = 5.

  IF sy-subrc <> 0.
    MESSAGE |產生表單失敗 RC={ sy-subrc }| TYPE 'E'.
  ENDIF.

  lt_otf = ls_result-otfdata.

  " ── OTF → PDF ─────────────────────────────────────
  CALL FUNCTION 'CONVERT_OTF'
    EXPORTING
      format                = 'PDF'
      max_linewidth         = 132
    IMPORTING
      bin_filesize          = cv_bytes
      bin_file              = cv_pdf          " XSTRING,最好用的形式
    TABLES
      otf                   = lt_otf
      lines                 = lt_pdf
    EXCEPTIONS
      err_max_linewidth     = 1
      err_format            = 2
      err_conv_not_possible = 3
      err_bad_otf           = 4
      OTHERS                = 5.

  IF sy-subrc <> 0.
    MESSAGE |OTF 轉 PDF 失敗 RC={ sy-subrc }| TYPE 'E'.
  ENDIF.

  " ── XSTRING → SOLIX(寄信附件用)────────────────────
  ct_solix = cl_bcs_convert=>xstring_to_solix( iv_xstring = cv_pdf ).

ENDFORM.
另一支常見的函數是 CONVERT_OTF_2_PDF(只吃 OTF 表回傳 tline),以及 CONVERT_OTFSPOOLJOB_2_PDFCONVERT_ABAPSPOOLJOB_2_PDF已經有 Spool ID 時直接把 Spool 轉 PDF,適合「重印歷史單據」的需求)。

13-2 下載 PDF 到本機

FORM download_pdf USING iv_pdf TYPE xstring.

  DATA lt_bin TYPE STANDARD TABLE OF x255 WITH EMPTY KEY.

  DATA(lv_len) = xstrlen( iv_pdf ).
  lt_bin = cl_bcs_convert=>xstring_to_solix( iv_pdf ).

  TRY.
      cl_gui_frontend_services=>gui_download(
        EXPORTING
          bin_filesize = lv_len
          filename     = |C:\\temp\\PO_{ gs_head-ebeln ALPHA = OUT }.pdf|
          filetype     = 'BIN'
        CHANGING
          data_tab     = lt_bin ).

      MESSAGE '檔案已下載' TYPE 'S'.

    CATCH cx_root INTO DATA(lx).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.

ENDFORM.
背景執行(SM36 排程)不能用 CL_GUI_FRONTEND_SERVICES——沒有 GUI,會 dump。背景要存檔請用 OPEN DATASET … FOR OUTPUT IN BINARY MODE 寫到應用伺服器(AL11 看得到),或用 ArchiveLink/GOS 掛到單據上。
" === 背景可用:寫到應用伺服器 ===
DATA(lv_path) = |/usr/sap/tmp/PO_{ gs_head-ebeln }_{ sy-datum }.pdf|.

TRY.
    OPEN DATASET lv_path FOR OUTPUT IN BINARY MODE.
    IF sy-subrc = 0.
      TRANSFER gv_pdf TO lv_path.
      CLOSE DATASET lv_path.
      MESSAGE |已寫入 { lv_path }| TYPE 'S'.
    ELSE.
      MESSAGE |無法開啟 { lv_path }| TYPE 'E'.
    ENDIF.
  CATCH cx_sy_file_open cx_sy_file_authority INTO DATA(lx_file).
    MESSAGE lx_file->get_text( ) TYPE 'E'.
ENDTRY.

13-3 寄 E-mail(CL_BCS 完整樣板)

*&---------------------------------------------------------------------*
*& 把 PDF 當附件寄給廠商(CL_BCS,現代寫法)
*&---------------------------------------------------------------------*
FORM send_mail USING it_solix TYPE solix_tab
                     iv_bytes TYPE i
                     iv_to    TYPE ad_smtpadr.

  TRY.
      " 1) 建立信件物件
      DATA(lo_send) = cl_bcs=>create_persistent( ).

      " 2) 內文(HTML 或純文字)
      DATA(lt_body) = VALUE bcsy_text(
        ( line = |<p>您好,</p>| )
        ( line = |<p>附件為採購單 <b>{ gs_head-ebeln ALPHA = OUT }</b>,| )
        ( line = |請確認交期並回簽。</p>| )
        ( line = |<p>此信件由系統自動發送,請勿直接回覆。</p>| ) ).

      DATA(lo_doc) = cl_document_bcs=>create_document(
        i_type    = 'HTM'
        i_text    = lt_body
        i_subject = CONV so_obj_des( |採購單 { gs_head-ebeln ALPHA = OUT }| ) ).

      " 3) 掛 PDF 附件(附件名稱最多 50 字元)
      lo_doc->add_attachment(
        i_attachment_type    = 'PDF'
        i_attachment_subject = CONV so_obj_des( |PO_{ gs_head-ebeln ALPHA = OUT }| )
        i_attachment_size    = CONV so_obj_len( iv_bytes )
        i_att_content_hex    = it_solix ).

      lo_send->set_document( lo_doc ).

      " 4) 收件人
      DATA(lo_recipient) = cl_cam_address_bcs=>create_internet_address( iv_to ).
      lo_send->add_recipient( i_recipient = lo_recipient
                              i_express   = abap_true ).

      " 5) 寄件人(不指定就用目前使用者)
      lo_send->set_sender(
        cl_cam_address_bcs=>create_internet_address( 'noreply@company.com' ) ).

      " 6) 立即送出、要求狀態回報
      lo_send->set_send_immediately( abap_true ).
      lo_send->set_status_attributes( i_requested_status = 'N'   " 只在失敗時通知
                                      i_status_mail      = 'N' ).

      DATA(lv_sent) = lo_send->send( i_with_error_screen = abap_true ).

      IF lv_sent = abap_true.
        COMMIT WORK.                      " ← CL_BCS 一定要 COMMIT 才會真的寄出
        MESSAGE |已寄送至 { iv_to }| TYPE 'S'.
      ELSE.
        ROLLBACK WORK.
        MESSAGE '郵件送出失敗' TYPE 'E'.
      ENDIF.

    CATCH cx_bcs INTO DATA(lx_bcs).
      ROLLBACK WORK.
      MESSAGE |寄信失敗:{ lx_bcs->get_text( ) }| TYPE 'E'.
  ENDTRY.

ENDFORM.
寄信的三個必踩坑
1. 忘了 COMMIT WORK:CL_BCS 是把信排進佇列,不 COMMIT 就永遠不會送。
2. SCOT 沒設定 SMTP 節點:程式回傳成功但信寄不出去,去 SOST 看狀態會顯示「Waiting」。SCOT 還要排程 RSCONN01 這支發送程式。
3. 附件名稱超過 50 字元會被截斷;中文附件名在某些郵件伺服器會亂碼,建議用英數。

13-4 把 PDF 掛回單據(GOS 附件)

" 把產生的 PDF 存成 GOS 附件掛在採購單上(ME23N 的迴紋針可看到)
DATA(ls_object) = VALUE borident( objkey  = gs_head-ebeln
                                  objtype = 'BUS2012' ).   " 採購訂單

DATA(lt_content) = cl_bcs_convert=>xstring_to_solix( gv_pdf ).

TRY.
    cl_binary_relation=>create_link(
      is_object_a = ls_object
      is_object_b = VALUE borident(
                      objkey  = cl_gos_document_service=>create_attachment(
                                  is_object   = ls_object
                                  iv_filename = |PO_{ gs_head-ebeln }.pdf| )
                      objtype = 'MESSAGE' )
      ip_reltype  = 'ATTA' ).
  CATCH cx_obl_parameter_error cx_obl_model_error INTO DATA(lx_gos).
    MESSAGE lx_gos->get_text( ) TYPE 'S' DISPLAY LIKE 'W'.
ENDTRY.
GOS 附件的標準做法還有 SO_DOCUMENT_INSERT_API1BINARY_RELATION_CREATE_COMMIT,或直接用 ArchiveLink(OUTPUT_OPTIONS-TDARMOD = 3)歸檔。專案上要先確認公司用的是 GOS 還是 ArchiveLink。

14. NACE 輸出決定與 SPOOL 管理

正式專案裡,很少讓使用者「執行一支 Z 程式來印單據」,而是在單據存檔時自動觸發列印。這靠的是 Output Determination(輸出決定)

14-1 NACE 的五層設定

層次內容說明
1. ApplicationEF=採購訂單、V1=銷售、V2=送貨、V3=帳單、M1/M2=MM 收貨NACE 第一個畫面就是選它
2. Output Type(輸出類型)NEU(採購單)、BA00(訂單確認)、LD00(送貨單)、RD00(發票)可複製標準的改成 Z*
3. Processing Routines(處理程式)Medium(1=印表機、5=外部傳送/EMail、A=ALE、6=EDI)+ ProgramFORM routineForm / PDF-FormSmartForm 填在「Form」旁的 Smart Form 欄位(不是 SAPscript 的 Form 欄位)
4. Access Sequence / Condition Table決定「什麼條件下要出這個輸出」例如「採購組織+文件類型」
5. Condition Record(MN04/MN05 等)實際的輸出主資料:印表機、份數、時點(1=立即、3=手動、4=批次)沒建 Condition Record,單據存檔就不會出輸出

NACE 呼叫的驅動程式(Print Program)介面是固定的——SAP 標準採購單用 SAPFM06P + FORM routine ENTRY_NEU。客製時複製成 Z 程式:

*&---------------------------------------------------------------------*
*& NACE 驅動程式的固定介面(複製 SAPFM06P 的 ENTRY_NEU)
*&---------------------------------------------------------------------*
FORM entry_neu USING ent_retco TYPE sy-subrc
                     ent_screen TYPE c.

  " NACE 會把處理中的輸出記錄放在 NAST 結構
  DATA(lv_ebeln) = CONV ekko-ebeln( nast-objky ).

  PERFORM get_data USING lv_ebeln.

  DATA: ls_ctrl   TYPE ssfctrlop,
        ls_output TYPE ssfcompop.

  " ent_screen = 'X' 代表使用者按了「預覽」
  ls_ctrl-no_dialog = COND #( WHEN ent_screen = 'X' THEN abap_false ELSE abap_true ).
  ls_ctrl-preview   = ent_screen.

  " 印表機/份數由 NAST 決定,不要自己寫死
  ls_output-tddest   = nast-ldest.
  ls_output-tdcopies = nast-anzal.
  ls_output-tdimmed  = nast-dimme.
  ls_output-tddelete = nast-delet.
  ls_output-tdtitle  = |PO { lv_ebeln }|.

  CALL FUNCTION 'SSF_FUNCTION_MODULE_NAME'
    EXPORTING formname = gc_formname
    IMPORTING fm_name  = DATA(lv_fm)
    EXCEPTIONS OTHERS  = 3.

  CALL FUNCTION lv_fm
    EXPORTING control_parameters = ls_ctrl
              output_options     = ls_output
              user_settings      = abap_false
              i_ekko             = gs_head
    TABLES    i_ekpo             = gt_item
    EXCEPTIONS formatting_error  = 1
               internal_error    = 2
               send_error        = 3
               user_canceled     = 4
               OTHERS            = 5.

  " 【重要】一定要回傳 return code,否則 NAST 狀態會一直停在「未處理」
  ent_retco = sy-subrc.

ENDFORM.
NACE 驅動程式最常見的錯:忘了設 ent_retco。回傳 0 才會把 NAST 標記為「已成功處理(綠燈)」,非 0 會變紅燈並可在 ME23N 的 Messages 重送。忘了設,單據會一直重複列印或永遠顯示未處理。

14-2 SPOOL 相關 TCODE

TCODE用途
SP01Spool Request 總覽(看內容、重印、刪除、看輸出狀態)
SP02只看自己的 Spool
SPADSpool 管理:輸出裝置、裝置類型、紙張格式、字元集
SPICSpool 一致性檢查
SP12TemSe 管理(Spool 實體資料的儲存區)
TSP01 / TSP02 / TST01Spool 的資料表,程式要查 Spool ID 時用
除錯時最好用的一招:SP01 選一筆 Spool → Goto → Display Requests → Settings → 勾 Output Text(Raw/OTF),可以看到原始 OTF 指令,直接看出是哪一個 Window、哪一段文字沒印出來。

15. 傳輸、多語言與跨系統搬運

  • Smart Form 與 Smart Style 都是 Repository 物件、Client 獨立,會跟著 Request(工作台請求)走。建立時要指定 Package(Z 開頭)與 Request。
  • 產生的函數模組不傳輸——它是目標系統在第一次啟動表單時自己產生的。這正是必須用 SSF_FUNCTION_MODULE_NAME 的根本原因。
  • SO10 標準文字是 Client 相依,要用 RSTXTRAN 掛進傳輸請求。
  • SE78 的圖檔也是 Repository 物件會傳,但檔案很大時建議在各環境分別上傳。
  • 沒有傳輸連線時(例如要把表單給客戶):用 SSF_DOWNLOAD 匯出成 XML,在目標系統用 SSF_UPLOAD 匯入;也可以在 SMARTFORMS 選單 Utilities → Download form / Upload form。Smart Style 對應的是 SSFS_DOWNLOAD / SSFS_UPLOAD
" 檢查表單在目前系統是否存在且可用(上線前的自我檢查程式)
DATA(lt_forms) = VALUE stringtab( ( `Z00_EKKO_EKPO` ) ( `Z00_DELIVERY` ) ( `Z00_INVOICE` ) ).

LOOP AT lt_forms INTO DATA(lv_form).

  CALL FUNCTION 'SSF_FUNCTION_MODULE_NAME'
    EXPORTING  formname           = CONV tdsfname( lv_form )
    IMPORTING  fm_name            = DATA(lv_fm)
    EXCEPTIONS no_form            = 1
               no_function_module = 2
               OTHERS             = 3.

  DATA(lv_msg) = SWITCH string( sy-subrc
                   WHEN 0 THEN |OK  → { lv_fm }|
                   WHEN 1 THEN |NG 表單不存在|
                   WHEN 2 THEN |NG 表單存在但函數未產生(請重新 Active)|
                   ELSE        |NG 未知錯誤| ).

  WRITE: / lv_form, lv_msg.
ENDLOOP.

15-1 多語言表單

  • Smart Form 的原始語言在 Form Attributes 決定。翻譯走 SE63(Translation → ABAP Objects → Other Long Texts → SSF)。
  • 執行時語言由 CONTROL_PARAMETERS-LANGU 決定;找不到就用 REPLANGU1/2/3 的替代語言。
  • 「要給廠商看的語言」通常不是登入語言,而是廠商主資料的語言(LFA1-SPRAS)或客戶的 KNA1-SPRAS。不要直接用 SY-LANGU。
  • Text Module 可以逐語言維護,是多語表單的最佳實務;不要在文字節點裡硬寫中文再複製一份英文表單。
" 依廠商主資料的語言列印,找不到才用登入語言
SELECT SINGLE spras FROM lfa1
  WHERE lifnr = @gs_head-lifnr
  INTO @DATA(lv_vendor_langu).

ls_ctrl-langu = COND spras( WHEN lv_vendor_langu IS NOT INITIAL
                            THEN lv_vendor_langu
                            ELSE sy-langu ).
ls_ctrl-replangu1 = 'E'.        " 找不到就退回英文
ls_ctrl-replangu2 = '1'.        " 再退回繁體中文

16. 常見坑總整理(老師與現場經驗)

症狀原因解法
欄位印不出來,印成一串 &I_EKKO-EBELN& 文字插入欄位時沒有變成灰底,被當成純文字刪掉重插:拖曳 Field List,或用 PC Editor 的 Include field。灰色才算成功
明細只印第一筆Main Area 的 Text 節點取了 I_EKPO-xxx 而不是工作區 WA_EKPO-xxx全部改成 Table 節點 Data 頁籤指定的工作區
只印第一頁,後面資料不見該 Page 沒有 MAIN Window,或 Next Page 沒設每個要續頁的 Page 都要有 MAIN,Next Page 指向自己或續頁
頁面重複/無限迴圈印個不停Page 的 Next Page 互相指來指去,或 Command 節點的換頁條件永遠成立檢查 Next Page 鏈;換頁條件加上「不是最後一筆」的判斷
最後多出一張空白頁MAIN Window 高度太大,或表尾 Template 的高度超過剩餘空間;也可能是 Table Footer 被擠到下一頁縮小 MAIN 高度;表尾改放 Final Window;Folder 加 Page Protection
Active 時報「Main window width differs」不同 Page 的 MAIN 寬度不一致把所有 Page 的 MAIN 寬度改成完全相同的數值
總計是 0 或少一筆合計欄位放在一般 Window,此時明細還沒跑完把總計搬到 Table FooterFinal Window
「共 y 頁」印成 0 或空白SFSY-FORMPAGES 只能在 Final Window 使用把「第 x 頁/共 y 頁」放進 Final Window
中文變方框/問號Device Type 沒掛中文字型;或用了 PDF1 而非 PDFUCSE73 檢查字型;換 CNSAPWIN/PDFUC;Smart Style 改用 ANDALE_T/CNSONG
金額/數量對不齊只設段落靠右,沒設輸出長度段落 RIGHT + &field(13.2)& 一起用
日圓金額多兩位小數值欄位與幣別欄位不同表,沒在 Currency/Quantity Fields 建對應見第 12-4 節
採購單號前面多一堆 0沒做 ALPHA 轉換輸出欄位加 (Z),或在 Program Lines 用 |{ ebeln ALPHA = OUT }|
QAS/PRD dump:CALL_FUNCTION_NOT_FOUND寫死了 /1BCDWB/SFxxxxxxxx改用 SSF_FUNCTION_MODULE_NAME
測試機文字不見SO10 標準文字是 Client 相依,沒傳RSTXTRAN,或改用 SmartForms Text Module
印到別人的印表機USER_SETTINGS = 'X' 蓋掉了 TDDESTuser_settings = abap_false
背景 Job 一直不結束NO_DIALOG 沒設,卡在列印對話框ls_ctrl-no_dialog = abap_true
列印非常慢Logo 是高解析彩色 BMP;或 Program Lines 在迴圈內 SELECTLogo 換黑白小檔;資料一次撈好放 HASHED TABLE
條碼掃不出來用舊式印表機條碼、或 ALPHA 轉換吃掉前導零改用新式條碼 BC_C128;欄位加 (K)
預覽正常、實體列印跑版預覽用 SAPGUI 字型,列印用 Device Type 字型驗收一定要用目標印表機實印或轉 PDF 檢查

17. S/4HANA 補充:Adobe Forms 與 Fiori 列印

SmartForms 在 S/4HANA 仍完全支援,既有客製不需要改寫。但 SAP 對新開發的建議方向是 Adobe Forms,S/4HANA 的新輸出管理(Output Management)也是以 Adobe Forms 為基礎。

17-1 Adobe Forms 的呼叫流程

結構和 SmartForms 很像,但多了「開工作/關工作」的包裝:

*&---------------------------------------------------------------------*
*& Adobe Forms(SFP)標準呼叫樣板
*&---------------------------------------------------------------------*
DATA: ls_outputparams TYPE sfpoutputparams,
      ls_docparams    TYPE sfpdocparams,
      ls_formoutput   TYPE fpformoutput.

" 1) 開啟列印工作
ls_outputparams-nodialog = abap_true.       " 不跳對話框
ls_outputparams-preview  = abap_false.
ls_outputparams-getpdf   = 'X'.             " ← 要 PDF 二進位
ls_outputparams-dest     = 'LOCL'.

CALL FUNCTION 'FP_JOB_OPEN'
  CHANGING   ie_outputparams = ls_outputparams
  EXCEPTIONS cancel          = 1
             usage_error     = 2
             system_error    = 3
             internal_error  = 4
             OTHERS          = 5.
IF sy-subrc <> 0.
  MESSAGE |FP_JOB_OPEN 失敗 RC={ sy-subrc }| TYPE 'E'.
ENDIF.

" 2) 由表單名稱取得產生的函數名(等同 SmartForms 的 SSF_FUNCTION_MODULE_NAME)
CALL FUNCTION 'FP_FUNCTION_MODULE_NAME'
  EXPORTING  i_name     = 'Z00_PO_ADOBE'
  IMPORTING  e_funcname = DATA(lv_fp_fm)
  EXCEPTIONS OTHERS     = 3.

" 3) 呼叫表單
ls_docparams-langu   = sy-langu.
ls_docparams-country = 'TW'.

CALL FUNCTION lv_fp_fm
  EXPORTING  /1bcdwb/docparams  = ls_docparams
             i_ekko             = gs_head
             i_ekpo             = gt_item      " Adobe 可直接吃內表參數
  IMPORTING  /1bcdwb/formoutput = ls_formoutput
  EXCEPTIONS usage_error        = 1
             system_error       = 2
             internal_error     = 3
             OTHERS             = 4.

" 4) 關閉列印工作
CALL FUNCTION 'FP_JOB_CLOSE'
  EXCEPTIONS usage_error    = 1
             system_error   = 2
             internal_error = 3
             OTHERS         = 4.

" ls_formoutput-pdf 就是 XSTRING 格式的 PDF,可直接寄信或下載
IF ls_formoutput-pdf IS NOT INITIAL.
  DATA(lt_solix) = cl_bcs_convert=>xstring_to_solix( ls_formoutput-pdf ).
ENDIF.
比較SmartFormsAdobe Forms
維護 TCODESMARTFORMSSFP
取函數名SSF_FUNCTION_MODULE_NAMEFP_FUNCTION_MODULE_NAME
工作包裝SSF_OPEN/SSF_CLOSE(選用)FP_JOB_OPEN/FP_JOB_CLOSE(必要)
拿 PDFGETOTF → CONVERT_OTFGETPDF → FORMOUTPUT-PDF(一步到位)
介面/內容分離介面與版面綁在一起Interface 與 Form 是兩個獨立物件,一個介面可給多張表單用
設計工具SAP GUI 內建的樹狀編輯器Adobe LiveCycle Designer(所見即所得)
前置需求ADS(Adobe Document Services)必須先安裝設定
老師的講義寫「Adobe Form T-CODE: SPF 沒安裝沒辦法試」——正確 TCODE 是 SFP(Form Builder),沒有 ADS 時能建立表單但無法產生/預覽 PDF,會出 ADS_ERROR。要測試可以請 Basis 確認 SM59 有沒有 ADS 這個 HTTP 目的地,或執行標準程式 FP_TEST_00FP_PDF_TEST_00 做連線檢測。

17-2 Fiori/新輸出管理

  • S/4HANA 1610 起,部分應用(銷售訂單、交貨單、帳單、採購單)改走 Output Control(新輸出管理):用 BRF+ 決定 Output Type、Channel(Printer/Email/XML/IDoc)、Receiver、Form Template。
  • 設定入口:OPD(Output Parameter Determination)、Fiori App「Manage Output Items」「Output Parameter Determination」、以及 SPRO 的 Cross-Application Components → Output Control。
  • 表單本體只支援 Adobe Forms(Form Template)Fragment新輸出管理不支援 SmartForms/SAPscript
  • NACE 並沒有被移除,很多模組(MM 的收貨單、PP 的工單)仍走 NACE。專案上要先確認該單據走的是哪一套,設定的地方完全不同。
  • Fiori 前端列印走 Print Queue(App「Maintain Print Queues」)+ SAP Cloud Print Manager,跟傳統 SPAD 的印表機是兩條路。
不要在 S/4HANA 上把 SmartForms 全部改寫成 Adobe Forms。 遷移成本高、風險大,SAP 也明確表示 SmartForms 繼續支援。合理策略是:既有的維持、新的用 Adobe、走新輸出管理的單據只能用 Adobe

17-3 S/4HANA 取數要注意的表格變化

單據列印程式常需要抓會計或庫存資料,S/4HANA 的資料模型改了:
BSEG/BKPF/BSIS/BSAS…→ ACDOCA(Universal Journal)。舊表多半變成相容 View,直接查會慢。
MSEG/MKPF → MATDOC;MB5B 之類的邏輯建議改用 CDS View。
庫存 MARD/MCHB → NSDM_V_MARD 等相容 View,實體資料在 MATDOC。
・新開發建議直接用 CDS View(例如 I_PurchaseOrderItemI_Product)取數,欄位語意清楚也自帶關聯。
" S/4HANA 建議:用標準 CDS View 取採購單資料(欄位語意化、自帶文字關聯)
SELECT po~PurchaseOrder,
       po~CompanyCode,
       po~PurchaseOrderType,
       po~Supplier,
       item~PurchaseOrderItem,
       item~Material,
       item~OrderQuantity,
       item~PurchaseOrderQuantityUnit,
       item~Plant,
       item~StorageLocation,
       txt~ProductName
  FROM I_PurchaseOrder      AS po
  INNER JOIN I_PurchaseOrderItem AS item
          ON item~PurchaseOrder = po~PurchaseOrder
  LEFT OUTER JOIN I_ProductDescription AS txt
          ON  txt~Product  = item~Material
          AND txt~Language = @sy-langu
  WHERE po~PurchaseOrder = @p_ebeln
    AND item~PurchasingDocumentDeletionCode = @space
  ORDER BY item~PurchaseOrderItem
  INTO TABLE @DATA(lt_po_cds).

" 再對映到表單接口結構
gt_item = CORRESPONDING #( lt_po_cds MAPPING ebelp = purchaseorderitem
                                              matnr = material
                                              maktx = productname
                                              menge = orderquantity
                                              meins = purchaseorderquantityunit
                                              werks = plant
                                              lgort = storagelocation ).

18. SAPscript 的最低限度認識(改標準單據時會遇到)

老師說「因為有了 SMARTFORM,所以不會也無所謂」——但實務上你改 SAP 標準單據時還是會撞到它,所以留一個最小知識集:

項目內容
TCODESE71 Form/SE72 Style/SE73 Font/SE78 Graphic/SO10 標準文字/I18N 國際化入口
結構Header → Paragraph Formats → Character Formats → WindowsPagesPage WindowsText Elements
呼叫流程OPEN_FORM → START_FORM →(多次)WRITE_FORM → END_FORM → CLOSE_FORM
文字元素/E 元素名稱 定義,程式用 WRITE_FORM ELEMENT = '…' 呼叫
常用行格式/: 控制指令(IF、CASE、DEFINE、NEW-PAGE、PROTECT、BOX、POSITION、SIZE)、/* 註解、* 預設段落、/= 續行
致命差異Client 相依,傳輸要 RSTXTRAN;改標準表單要先複製成 Z(SE71 → Utilities → Copy from Client)
要知道某張標準單據用的是 SAPscript 還是 Smart Form,最快的方法:NACE → 選 Application → Output Types → Processing Routines,看填的是 Form(SAPscript)還是 Smart Form 欄位。

練習與自我檢核

  1. 【09-3 課堂作業】仿作 Z00_EKKO_EKPO。 自行設計一張採購單(欄位可簡化,不計美觀,只要求設計重點),必須真的有資料印出來。
    驗收方式:老師會用程式執行你的表單,因此至少要交出第 08-1 節那種「直接呼叫函數名」的版本;08-2 的動態函數名版本請務必自己練成,那才是正式程式的寫法。
    提示:(1) SE11 建 Z00_EKKO / Z00_EKPO 並記得 Active、QUAN 掛單位;(2) Form Interface 的 Import 放檔頭、Tables 放檔身;(3) Global Data 宣告 WA_EKPO 與 LV_MENGE;(4) 表頭 Secondary Window 用 Template,明細用 MAIN Window 的 Table;(5) 欄位一定要是灰色的
  2. 為什麼「直接寫 CALL FUNCTION '/1BCDWB/SF00000009'」在開發機會成功、上線就 dump?請說出 SSF_FUNCTION_MODULE_NAME 的兩個 EXCEPTION(NO_FORM=1、NO_FUNCTION_MODULE=2)分別代表什麼情況。
    答案要點:產生的函數名是各系統自行編號的流水號,傳輸不會帶過去;重新 Active 也會換號。1=表單根本不存在,2=表單存在但函數還沒產生(需重新啟動表單)。
  3. Template 與 Table 的差別是什麼? 表頭的四個欄位(單據編號、公司代碼、單據類型、供應廠商)應該用哪一個?明細清單應該用哪一個?為什麼明細一定要放在 MAIN Window?
    答案要點:Template 固定列數不會 LOOP 也不會跨頁;Table 動態列數、自帶 Header/Main Area/Footer、會自動續頁並重印表頭。續頁機制由 MAIN Window 觸發,沒有 MAIN 的頁面不會往下翻。
  4. Table 的 Main Area 裡,格子要取 I_EKPO-MENGE 還是 WA_EKPO-MENGE?取錯會發生什麼事?
  5. 合計數量 LV_MENGE 有兩種算法:(a) Table 節點的 Calculations 頁籤設 TOTAL;(b) Program Lines 用 REDUCE。各寫一次,並說明「合計要顯示在哪個節點才不會是 0」。
    答案要點:要放在 Table Footer 或 Final Window,放在一般 Window 時明細還沒跑完。
  6. 寫一個欄位輸出,要求:採購數量寬 13 位、小數 3 位、負號在左、不要千分位。
    答案:&WA_EKPO-MENGE(13.3<T)&
  7. 「第 x 頁/共 y 頁」要用哪兩個系統欄位?為什麼 SFSY-FORMPAGES 只能放在 Final Window?大量列印時為什麼不建議用 SFSY-JOBPAGES
  8. 把採購單轉成 PDF 寄給廠商,請依序寫出四個關鍵動作與對應的物件/函數。
    答案要點:CONTROL_PARAMETERS-GETOTF='X' 與 NO_DIALOG='X' → job_output_info-otfdata → CONVERT_OTF(FORMAT='PDF')→ cl_bcs_convert=>xstring_to_solix → CL_BCS/CL_DOCUMENT_BCS 送出 → COMMIT WORK
  9. 使用者反映「單據都印到會計部的印表機」。程式明明有設 OUTPUT_OPTIONS-TDDEST = 'LP01',為什麼?
    答案:USER_SETTINGS 預設 'X',會用 SU3 的使用者預設印表機蓋掉。設 abap_false。
  10. 採購單抬頭長文(ME23N 的 Texts)要印在表單上,Include Text 節點的 Text Object / Text ID / Text Name 各要填什麼?如果改用程式端的 READ_TEXT,四個參數又是什麼?為什麼這段文字傳到測試機會不見?
  11. 你的表單在開發機預覽中文完全正常,印出來全是方框。列出三個要檢查的地方。
    答案要點:(1) SPAD 輸出設備的 Device Type 是否支援中文(CNSAPWIN/PDFUC);(2) SE73 該 Device Type 是否掛了中文字型;(3) Smart Style 的字元格式選的字體家族(ANDALE_T/CNSONG)在該裝置有沒有對應。
  12. 一次列印 500 張出貨單,要求:只產生一個 Spool Request、背景執行不跳對話框、不使用使用者預設印表機。請寫出關鍵的三個設定與兩支函數。
    答案要點:SSF_OPEN/SSF_CLOSE 包住迴圈,中間每次呼叫設 no_open = no_close = 'X';ls_ctrl-no_dialog = abap_true;user_settings = abap_false。
  13. S/4HANA 上有人要求你「把所有 SmartForms 改寫成 Adobe Forms」,你會怎麼回應?請說出 Adobe Forms 的前置需求、兩個 TCODE 差異、以及「什麼情況非改不可」。
    答案要點:需要 ADS;SMARTFORMS vs SFP、SSF_FUNCTION_MODULE_NAME vs FP_FUNCTION_MODULE_NAME;只有走新輸出管理(Output Control / BRF+)的單據非用 Adobe Forms 不可,其餘 SmartForms 繼續支援。

第 4-4 章 綜合技巧:BDC 批次匯入 / 郵件 / FTP / 檔案處理 / 背景作業

這一章是「介面程式」的總集篇。企業導入 SAP 之後,最常被要求的不是報表,而是「把外面的資料弄進來、把裡面的結果送出去」:MES 產生 CSV 放到 FTP、SAP 定時抓下來、逐筆用 CO11N 報工、把成功失敗寫成紀錄表、印成 PDF 再寄給相關人,整串排成背景作業每小時跑一次。老師用一支真實的工時上傳程式 ZPPI0002 把 FTP、Excel 上傳、BDC、寄信、Spool 轉 PDF 全部串起來。學完這一章,你能自己寫出一支「無人值守、有紀錄、有通知、可重跑」的介面程式,也知道在 S/4HANA 上哪些寫法該換掉。

核心 TCODESHDBSM35SM36SM37SM59SM49SM69AL11FILESCOTSOSTSBWPSO23SP01SU3SE38SE37SE93SM62SM64SJOBREPOCO11NMIGOLTMC
關鍵物件BDCDATA、BDCMSGCOLL、CTU_PARAMS、BDC_OPEN_GROUP、BDC_INSERT、BDC_CLOSE_GROUP、FORMAT_MESSAGE、MESSAGE_TEXT_BUILD、CL_GUI_FRONTEND_SERVICES、GUI_UPLOAD、GUI_DOWNLOAD、OPEN DATASET、CL_ABAP_CHAR_UTILITIES、CL_ABAP_CONV_CODEPAGE、CL_FDT_XL_SPREADSHEET、FTP_CONNECT、FTP_COMMAND、FTP_R3_TO_SERVER、FTP_SERVER_TO_R3、FTP_DISCONNECT、HTTP_SCRAMBLE、SCMS_BINARY_TO_TEXT、SX_TABLE_LINE_WIDTH_CHANGE、CL_BCS、CL_DOCUMENT_BCS、CL_CAM_ADDRESS_BCS、CL_BCS_CONVERT、SO_DOCUMENT_SEND_API1、SO_DLI_LIST_READ_XDL、CONVERT_ABAPSPOOLJOB_2_PDF、JOB_OPEN、JOB_SUBMIT、JOB_CLOSE、BP_EVENT_RAISE、TBTCO、TBTCP、TSP01、AUFK、AFRU、ZPPWORKMA0001

1. 這一章的地圖:一支程式串起五件事

講義開頭老師就說了:「這支工時回報上傳,應該算寫得非常好,有 FTP、有發 MAIL、有 EXCEL 上傳,也是先放暫存檔 T_XLS,然後才用 BDC 做完整的檢查,連訊息也回寫得很清楚。」這句話其實就是介面程式的標準骨架,值得抄下來當口訣:

  1. 取得來源:從 FTP 抓檔,或從 PC 上傳 Excel/CSV。兩條路走完都倒進同一張暫存內表。
  2. 暫存 + 正規化:全部先進 T_XLS 這種「寬鬆型別(全部字元欄)」的暫存表,再做去引號、ALPHA 補零、日期轉換。
  3. 驗證:查主檔存不存在(工單查 AUFK)、必填欄位、數值可不可轉。錯的先標 STATUS = 'E',不要丟掉。
  4. 過帳:只處理 STATUS = SPACE 的那些,用 BDC 呼叫 CO11N,訊息收回來寫進 MSG
  5. 留痕與通知:整批結果寫進自建紀錄表 ZPPWORKMA0001、顯示 ALV、必要時把 Spool 轉 PDF 寄給群組。
老師口訣「先落地、再驗證、最後才過帳;錯的不要刪,標起來。」 介面程式最怕的是「跑一半死掉、不知道跑到哪」。所有資料先落到暫存表/紀錄表,就算中途中斷也能查、能重跑。
講義這一份原本的課程索引還列了 BC414「SAPscript: Form Design and Text Management」的十一個小節(Introduction / SAPscript Overview / Form Elements / Graphical Editor and Line Editor / Symbols and Control Statements / Print Program / Styles / The Next Generation / Modifications / Maintenance / Appendix)。表單設計的內容請看第 4-3 章 SmartForms;本章專注在第 01 節「工時上傳」的綜合技巧。

2. 範例主程式 ZPPI0002 的骨架與選擇畫面

主程式只做「流程編排」,所有細節都在 INCLUDE 裡。這是介面程式很好的分層:主程式一眼看得懂在做什麼。

REPORT zppi0002 NO STANDARD PAGE HEADING
                LINE-SIZE  255
                LINE-COUNT 65
                MESSAGE-ID zpp01.        " 自建訊息類別,訊息集中管理

INCLUDE zppi0002top.    " 宣告 + 選擇畫面
INCLUDE zppi0002f01.    " 所有 FORM

*--- F4:讓使用者挑 PC 上的檔案
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_files.
  PERFORM get_pc_file CHANGING p_files.

START-OF-SELECTION.

  CLEAR gt_xls.

  " 勾了寄信卻沒填收件群組 → 擋下來
  IF p_email = abap_true AND p_group IS INITIAL.
    MESSAGE s000 WITH TEXT-e01.
    STOP.
  ENDIF.

  IF p_upftp = abap_true.
    PERFORM get_data_from_ftp.      " 走 FTP
    PERFORM adjust_t_xls.           " 正規化 + 驗證(FTP/PC 共用同一支)
  ELSE.
    PERFORM upload_excel.           " 走 PC Excel
    PERFORM adjust_t_xls.
  ENDIF.

  PERFORM process_co11n_rtn.        " BDC 過帳
  PERFORM modify_log_table.         " 回寫紀錄表 ZPPWORKMA0001
  PERFORM display_alv_rtn.          " ALV 呈現結果

  IF p_email = abap_true.
    PERFORM z_print_spool_pdf.      " Spool → PDF
    PERFORM z_send_pdf_email.       " 寄出
  ENDIF.

END-OF-SELECTION.

選擇畫面用 RADIOBUTTON GROUP 分成「FTP 來源」和「PC 來源」兩組,並用 MODIF ID 把欄位分群,方便在 AT SELECTION-SCREEN OUTPUT 動態灰掉不相干的欄位。

*&--- INCLUDE ZPPI0002TOP :選擇畫面
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-s01.

" BDC 顯示模式:A=全部畫面 E=只顯示錯誤 N=完全不顯示
PARAMETERS: p_mode TYPE ctu_params-dismode DEFAULT 'N'.

" --- 來源一:FTP
PARAMETERS: p_upftp  RADIOBUTTON GROUP g1 DEFAULT 'X' USER-COMMAND uc1,
            p_fpath  TYPE rlgrap-filename MODIF ID ftp DEFAULT '/CO01/',      " 收檔路徑
            p_fbakup TYPE rlgrap-filename MODIF ID ftp DEFAULT '/CO01_BAK/'.  " 備份路徑

PARAMETERS: p_ip   TYPE char20      MODIF ID key DEFAULT '10.10.10.180',
            p_user TYPE scms_ftpus  MODIF ID key DEFAULT 'SAPERP',
            p_pw   TYPE scms_ftppw  MODIF ID key.     " 密碼欄位型別自帶隱藏顯示

" --- 來源二:PC Excel/CSV
PARAMETERS: p_uppc  RADIOBUTTON GROUP g1,
            p_files TYPE rlgrap-filename MODIF ID pcf.

" --- 通知
PARAMETERS: p_email AS CHECKBOX,
            p_group TYPE sood-objnam MODIF ID mal.   " SBWP 通訊群組名稱

SELECTION-SCREEN END OF BLOCK b1.

*--- 依 radio button 動態開關欄位
AT SELECTION-SCREEN OUTPUT.
  LOOP AT SCREEN INTO DATA(ls_scr).
    CASE ls_scr-group1.
      WHEN 'FTP' OR 'KEY'.
        ls_scr-input = COND #( WHEN p_upftp = abap_true THEN 1 ELSE 0 ).
      WHEN 'PCF'.
        ls_scr-input = COND #( WHEN p_uppc  = abap_true THEN 1 ELSE 0 ).
      WHEN 'MAL'.
        ls_scr-input = COND #( WHEN p_email = abap_true THEN 1 ELSE 0 ).
      WHEN OTHERS.
        CONTINUE.
    ENDCASE.
    MODIFY SCREEN FROM ls_scr.
  ENDLOOP.
不要把 FTP 帳密寫死在程式裡。講義的範例把 IP/帳號/密碼直接當 DEFAULT 寫在 PARAMETERS(甚至密碼 '???0517'),這在教學可以,正式系統絕對不行。正確做法:存在自建設定表(密碼欄位加密)、或用 SM59 的 RFC destination 保管、或走 Secure Store(CL_ABAP_SSF / SSFS)。程式碼被傳輸到 QAS/PRD 時,Request 裡就會躺著明碼密碼。

暫存表 T_XLS 在講義裡是 DATA: BEGIN OF t_xls OCCURS 0 ... WITH HEADER LINE。S/4HANA 應改寫成型別 + 內表:

*--- 暫存表:所有欄位刻意用字元型,因為外部檔案什麼髒資料都可能進來
TYPES: BEGIN OF ty_xls,
         aufnr  TYPE char12,   " 工單
         lmnga  TYPE char15,   " 產出量
         meinh  TYPE char03,   " 產出量單位
         xmnga  TYPE char15,   " 壞品數量
         grund  TYPE char04,   " 無效工時原因代碼
         ism01  TYPE char15,   " 工時 1
         ile01  TYPE char03,   " 工時 1 單位
         ism02  TYPE char15,
         ile02  TYPE char03,
         ism03  TYPE char15,
         ile03  TYPE char03,
         ism04  TYPE char15,
         ile04  TYPE char03,
         budat  TYPE char08,   " 過帳日期(外部給的字串)
         vornr  TYPE char04,   " 作業
         status TYPE char01,   " ' '=待處理 'S'=成功 'E'=失敗
         msg    TYPE char220,  " 回寫訊息
       END OF ty_xls.

TYPES ty_t_xls TYPE STANDARD TABLE OF ty_xls WITH EMPTY KEY.

DATA gt_xls TYPE ty_t_xls.
為什麼暫存表全用字元欄?因為外部檔案的「1,234.5」「2025/09/01」「 A0001 」丟進 TYPE pTYPE d 會直接 dump(CX_SY_CONVERSION_NO_NUMBER)。先用字元收下來,轉換時包 TRY...CATCH,轉不成功就標 'E' 並寫「轉碼失敗」,程式不會死。這正是講義 MODIFY_ZPPWORKMA0001 裡包 TRY/CATCH 的用意。

3. BDC 是什麼:三種批次輸入作法

BDC=Batch Data Communication(批次輸入)。原理很直白:把「使用者在畫面上敲鍵盤」的整個過程錄成資料,再讓程式重播一遍。它不呼叫任何 API,而是真的去跑那支交易的畫面邏輯(PBO/PAI),所以所有標準檢查、Enhancement、User Exit 都會照跑——這是 BDC 最大的優點,也是它慢的原因。

作法怎麼做執行時機錯誤處理適用
SHDB 錄製TCODE SHDB 錄一次操作,再由系統產生程式範本只是「產生程式碼」的工具取得畫面編號與欄位名的最快方式
CALL TRANSACTION USING程式內組 BDCDATA,直接呼叫交易同步(線上就跑完)MESSAGES INTO 收訊息,自己判斷筆數不多、要即時知道結果、要把訊息回寫給使用者
SESSION(批次輸入群組)BDC_OPEN_GROUPBDC_INSERTBDC_CLOSE_GROUP非同步,之後在 SM35 執行SM35 自動保留錯誤 session,可補跑大量資料、夜間跑、需要人工介入修正
LSMW / LTMC設定式工具,底層仍可能用 BDC/BAPI/IDoc專案期一次性搬資料工具本身管導入期主檔/期初資料遷移
兩者的關鍵差異CALL TRANSACTION 出錯了資料就在那邊,你得自己記錄、自己重跑;SESSION 出錯的那筆會留在 SM35 的 session 裡,管理員可以用「顯示錯誤(Display errors only)」模式一筆一筆修好再送出——這是 SESSION 唯一無可取代的價值。

4. SHDB 錄製與 BDCDATA 結構

不會寫 BDC 的人,第一步永遠是 SHDB(依講義步驟整理)

  1. SHDBNew recording,輸入錄製名稱(例:Z_CO11N)與要錄的交易碼(CO11N)。
  2. Start recording,系統就會把你接下來的每一個畫面、每一個欄位、每一次按鍵記下來。像真的使用者一樣把單開完、按存檔。
  3. 回到 SHDB 清單,選這筆錄製 → Program,系統直接吐出一支含 bdc_dynpro / bdc_field 的完整程式範本。
  4. 把裡面固定的值換成內表欄位,就完成了 90%。

錄製結果就是一張 BDCDATA 表。它只有四個欄位,非常好記:

欄位長度意義
PROGRAM40畫面所屬的程式名(例:SAPLCORU_S
DYNPRO4畫面編號(例:0100
DYNBEGIN1'X' 表示「這一列是新畫面的開頭」
FNAM132欄位名(結構-欄位,例 AFRUD-AUFNR)或控制欄位
FVAL132欄位值(一律是「使用者會敲進去的那個字串」)

三個特殊控制欄位一定要背:

  • BDC_OKCODE:這個畫面按的功能碼。'=BU' 存檔、'/00' Enter、'=BACK''/EBACK' 返回、'/EE' 離開、'=P+' 下一頁、'=P-' 上一頁。前面加 /E 代表用 系統功能碼(相當於在指令列打)。
  • BDC_CURSOR:游標停在哪個欄位。看起來沒用,其實在 Table Control、在「輸入後才展開子畫面」的場合非常關鍵——很多欄位是游標移過去才會觸發 PAI 檢查。
  • BDC_SUBSCR:子畫面(Subscreen)區域。值是「子畫面程式 + 畫面編號」,用來指定接下來的欄位屬於哪一塊子畫面。錄製時系統會自動產生,不要手動刪掉

兩個 helper FORM 是 BDC 的標準配備。講義用的是舊寫法(WITH HEADER LINE 的全域 BDCDATA),現代寫法改成把內表當參數傳:

*&--- 開一個新畫面
FORM bdc_dynpro USING iv_program TYPE bdcdata-program
                      iv_dynpro  TYPE bdcdata-dynpro
             CHANGING ct_bdc     TYPE ty_t_bdcdata.

  APPEND VALUE bdcdata( program  = iv_program
                        dynpro   = iv_dynpro
                        dynbegin = abap_true ) TO ct_bdc.

ENDFORM.

*&--- 塞一個欄位值
FORM bdc_field USING iv_fnam TYPE bdcdata-fnam
                     iv_fval TYPE any
            CHANGING ct_bdc  TYPE ty_t_bdcdata.

  DATA lv_fval TYPE bdcdata-fval.

  lv_fval = |{ iv_fval }|.
  CONDENSE lv_fval.                      " BDC 的值前面不可以有多餘空白

  APPEND VALUE bdcdata( fnam = iv_fnam
                        fval = lv_fval ) TO ct_bdc.

ENDFORM.
如果你用的是 ABAP 7.40 以上,其實連 FORM 都可以省掉,直接用 VALUE #( ) 一次把整張 BDCDATA 寫出來,可讀性反而更好(見下一節)。老師的舊寫法要保留是因為錄製產生的範本長那樣,你要看得懂。

5. CALL TRANSACTION USING:MODE / UPDATE / MESSAGES INTO

講義的 CO11N 報工就是 CALL TRANSACTION。先看資料怎麼組——注意 SAPLCORU_S0100 畫面、OKCODE '=BU'(Save),以及四組工時是「有值才塞」:

*&--- 組 CO11N 的 BDC 資料(現代寫法:一次 VALUE 完成)
FORM build_bdc_co11n USING    is_xls TYPE ty_xls
                     CHANGING ct_bdc TYPE ty_t_bdcdata.

  CLEAR ct_bdc.

  " 過帳日期:外部字串 YYYYMMDD → 依使用者格式的外部顯示字串
  DATA(lv_budat) = conv_date_to_external( is_xls-budat ).

  ct_bdc = VALUE #(
    ( program = 'SAPLCORU_S' dynpro = '0100' dynbegin = abap_true )
    ( fnam = 'BDC_OKCODE'  fval = '=BU' )        " 存檔
    ( fnam = 'AFRUD-RUECK' fval = '' )           " 確認號碼留空 → 用工單/作業
    ( fnam = 'AFRUD-AUFNR' fval = is_xls-aufnr )
    ( fnam = 'AFRUD-VORNR' fval = is_xls-vornr )
    ( fnam = 'AFRUD-LMNGA' fval = is_xls-lmnga )
    ( fnam = 'AFRUD-MEINH' fval = is_xls-meinh )
    ( fnam = 'AFRUD-XMNGA' fval = is_xls-xmnga )
    ( fnam = 'AFRUD-GRUND' fval = is_xls-grund )
    ( fnam = 'AFRUD-BUDAT' fval = lv_budat ) ).

  " 工時 1~4:有值才送,避免把空值蓋掉預設值
  IF is_xls-ism01 IS NOT INITIAL.
    APPEND VALUE #( fnam = 'AFRUD-ISM01' fval = is_xls-ism01 ) TO ct_bdc.
    APPEND VALUE #( fnam = 'AFRUD-ILE01' fval = is_xls-ile01 ) TO ct_bdc.
  ENDIF.
  IF is_xls-ism02 IS NOT INITIAL.
    APPEND VALUE #( fnam = 'AFRUD-ISM02' fval = is_xls-ism02 ) TO ct_bdc.
    APPEND VALUE #( fnam = 'AFRUD-ILE02' fval = is_xls-ile02 ) TO ct_bdc.
  ENDIF.
  IF is_xls-ism03 IS NOT INITIAL.
    APPEND VALUE #( fnam = 'AFRUD-ISM03' fval = is_xls-ism03 ) TO ct_bdc.
    APPEND VALUE #( fnam = 'AFRUD-ILE03' fval = is_xls-ile03 ) TO ct_bdc.
  ENDIF.
  IF is_xls-ism04 IS NOT INITIAL.
    APPEND VALUE #( fnam = 'AFRUD-ISM04' fval = is_xls-ism04 ) TO ct_bdc.
    APPEND VALUE #( fnam = 'AFRUD-ILE04' fval = is_xls-ile04 ) TO ct_bdc.
  ENDIF.

ENDFORM.

接著呼叫。CALL TRANSACTION 的三個附加語法必須清楚:

附加意義
MODE(畫面顯示)'A'All screens:全部畫面都顯示,一步一步跑。除錯用
'E'Errors only:正常時不顯示,出錯時停在那個畫面。找錯最好用
'N'No display:完全不顯示。背景作業一定要用這個
'P'不顯示,但允許中斷點(可在被呼叫的交易裡下斷點除錯)
UPDATE(更新模式)'S'Synchronous 同步:更新完成才回來,sy-subrc 才真正可信
'A'Asynchronous 非同步:丟給更新工作程序就回來,快但無法確認結果
'L'Local update:在同一個工作程序內更新(不進 update task),速度快
MESSAGES INTOBDCMSGCOLL 內表把交易產生的所有訊息(S/W/E/A/X)收下來
FORM call_co11n CHANGING cs_xls TYPE ty_xls.

  DATA: lt_bdc TYPE ty_t_bdcdata,
        lt_msg TYPE STANDARD TABLE OF bdcmsgcoll WITH EMPTY KEY.

  PERFORM build_bdc_co11n USING cs_xls CHANGING lt_bdc.

  CALL TRANSACTION 'CO11N'
       USING            lt_bdc
       MODE             p_mode          " A / E / N
       UPDATE           'L'             " Local update
       MESSAGES INTO    lt_msg.

  DATA(lv_subrc) = sy-subrc.

  COMMIT WORK AND WAIT.                 " local update 之後要自己 commit

  PERFORM eval_bdc_messages USING lt_msg lv_subrc CHANGING cs_xls.

ENDFORM.

如果要一次控制更多細節,改用 OPTIONS FROM 傳一個 CTU_PARAMS 結構,這是比 MODE/UPDATE 更完整的介面:

DATA(ls_opt) = VALUE ctu_params(
    dismode  = p_mode        " 顯示模式 A/E/N/P
    updmode  = 'S'           " 更新模式 S/A/L
    cattmode = ' '           " CATT 模式
    defsize  = abap_true     " 用預設畫面大小,避免因螢幕解析度不同而失敗
    racommit = abap_true     " 被呼叫交易內的 COMMIT WORK 不中止 CALL TRANSACTION
    nobinpt  = abap_false    " abap_true 時 sy-binpt 不設為 X,交易表現得像線上
    nobiend  = abap_false ). " 資料用完後仍留在批次輸入模式

CALL TRANSACTION 'CO11N'
     USING         lt_bdc
     OPTIONS FROM  ls_opt
     MESSAGES INTO lt_msg.
三個一定要記的陷阱:
  • defsize = 'X' 幾乎一定要開。不開的話,BDC 會用「目前使用者的畫面大小」跑;不同人的 SAP GUI 視窗大小不同,Table Control 可見列數就不同,同一支程式在你電腦上跑得過、在別人電腦上就掛掉。
  • UPDATE 'A'(非同步)時 sy-subrc 不可信。更新失敗會變成 SM13 的 update terminate,你的程式卻以為成功了。要確定結果就用 'S'
  • 被呼叫的交易裡如果有 COMMIT WORK,預設會直接結束 CALL TRANSACTION,後面的畫面就不跑了。要繼續就設 racommit = 'X'
S/4HANA 語法檢查:新版 ABAP 對「純呼叫交易」的形式(CALL TRANSACTION 'XX',不帶 USING)要求明示 WITH AUTHORITY-CHECKWITHOUT AUTHORITY-CHECK,否則語法檢查會出警告。CALL TRANSACTION ... USING bdcdata 這種 BDC 形式本身就會做交易權限檢查(除非用 AUTHORITY-CHECK 相關附加關掉),但呼叫者仍必須擁有該 TCODE 的 S_TCODE 與底層物件權限。BDC 不會幫你繞過權限。

6. 錯誤訊息:從 BDCMSGCOLL 變成看得懂的中文字

BDCMSGCOLL 收回來的是「訊息代號」,不是文字。要變成人看得懂的句子有三條路:

FORM eval_bdc_messages USING it_msg   TYPE bdcmsgcoll_tab
                             iv_subrc TYPE sy-subrc
                    CHANGING cs_xls   TYPE ty_xls.

  DATA lv_text TYPE string.

  cs_xls-status = 'S'.
  cs_xls-msg    = TEXT-s01.        " 「過帳成功」

  LOOP AT it_msg INTO DATA(ls_msg).

    " 作法一(最推薦):MESSAGE ... INTO,語法內建、不用呼叫 FM
    MESSAGE ID     ls_msg-msgid
            TYPE   ls_msg-msgtyp
            NUMBER ls_msg-msgnr
            WITH   ls_msg-msgv1 ls_msg-msgv2 ls_msg-msgv3 ls_msg-msgv4
            INTO   lv_text.

    " 只要出現一筆 E/A/X 就算失敗,且以第一筆錯誤為主
    IF ls_msg-msgtyp CA 'EAX'.
      cs_xls-status = 'E'.
      cs_xls-msg    = |{ ls_msg-msgid }{ ls_msg-msgnr } { lv_text }|.
      EXIT.
    ENDIF.

  ENDLOOP.

  " CALL TRANSACTION 本身回傳非 0,但訊息表卻是空的 → 也要當失敗
  IF iv_subrc <> 0 AND cs_xls-status <> 'E'.
    cs_xls-status = 'E'.
    cs_xls-msg    = |CALL TRANSACTION failed, RC={ iv_subrc }|.
  ENDIF.

ENDFORM.

另外兩個常見的函數模組,在需要「拿到字串但不想動 sy-msg*」時很好用:

*--- 作法二:FORMAT_MESSAGE(最常見,可指定語言)
DATA lv_msg TYPE char200.

CALL FUNCTION 'FORMAT_MESSAGE'
  EXPORTING
    id        = ls_msg-msgid
    lang      = sy-langu          " '2' 表示用登入語言;也可寫死 'ZH'
    no        = ls_msg-msgnr
    v1        = ls_msg-msgv1
    v2        = ls_msg-msgv2
    v3        = ls_msg-msgv3
    v4        = ls_msg-msgv4
  IMPORTING
    msg       = lv_msg
  EXCEPTIONS
    not_found = 1
    OTHERS    = 2.

*--- 作法三:MESSAGE_TEXT_BUILD(傳整個 SY 結構最方便)
DATA lv_out TYPE natxt.

CALL FUNCTION 'MESSAGE_TEXT_BUILD'
  EXPORTING
    msgid               = sy-msgid
    msgnr               = sy-msgno
    msgv1               = sy-msgv1
    msgv2               = sy-msgv2
    msgv3               = sy-msgv3
    msgv4               = sy-msgv4
  IMPORTING
    message_text_output = lv_out.
老師的紀錄表設計值得抄ZPPWORKMA0001 除了業務欄位,還放了 ORA(原始整列字串)、STATUSMSGERDATERTIMERNAM把「原始那一行」整串存下來是關鍵:日後客戶說「我那筆明明有傳」,你直接調出原始行就能斷案。
*&--- 回寫紀錄表:先刪同鍵、再整批寫入(可重跑)
FORM modify_log_table.

  DATA lt_log TYPE STANDARD TABLE OF zppworkma0001.

  " 1) 同一批的鍵值先刪掉,讓程式可以重跑而不會殘留舊結果
  DELETE FROM zppworkma0001
    FOR ALL ENTRIES IN @gt_xls
    WHERE aufnr = @gt_xls-aufnr
      AND vornr = @gt_xls-vornr.

  LOOP AT gt_xls ASSIGNING FIELD-SYMBOL(<ls_xls>).

    DATA(ls_log) = VALUE zppworkma0001( ).

    TRY.
        " 字元 → 型別欄位的轉換都可能爆,包起來
        ls_log = CORRESPONDING #( <ls_xls> ).

      CATCH cx_sy_conversion_no_number cx_sy_conversion_error INTO DATA(lx_conv).
        CLEAR ls_log.
        ls_log-aufnr  = <ls_xls>-aufnr.
        ls_log-vornr  = <ls_xls>-vornr.
        ls_log-status = 'E'.
        ls_log-msg    = |轉碼失敗:{ lx_conv->get_text( ) }|.
        <ls_xls>-status = 'E'.
        <ls_xls>-msg    = '轉碼失敗'.
    ENDTRY.

    ls_log-ora   = |{ <ls_xls> }|.       " 原始整列,出事時的證據
    ls_log-erdat = sy-datlo.
    ls_log-ertim = sy-timlo.
    ls_log-ernam = sy-uname.
    APPEND ls_log TO lt_log.

  ENDLOOP.

  MODIFY zppworkma0001 FROM TABLE lt_log.
  COMMIT WORK AND WAIT.

ENDFORM.
FOR ALL ENTRIES 空表陷阱gt_xls 若為空,上面的 DELETE ... FOR ALL ENTRIES 會變成「沒有 WHERE 條件」,整張表被清空!凡是 FOR ALL ENTRIES,前面一定要加 IF gt_xls IS NOT INITIAL. 這條規矩對 SELECT 一樣適用(SELECT 空表會抓回全表)。

7. SESSION 方式:BDC_OPEN_GROUP / BDC_INSERT / BDC_CLOSE_GROUP 與 SM35

資料量大(幾千、幾萬筆)或需要人工修正時,改用 Session。三支函數而已:

REPORT zbdc_session_demo.

CONSTANTS gc_group TYPE apqi-groupid VALUE 'ZCO11N_UPLOAD'.

DATA lt_bdc TYPE ty_t_bdcdata.

START-OF-SELECTION.

  " 1) 開群組
  CALL FUNCTION 'BDC_OPEN_GROUP'
    EXPORTING
      client              = sy-mandt
      group               = gc_group          " SM35 上看到的名稱
      user                = sy-uname          " 執行者
      keep                = abap_true         " 成功後仍保留 session(方便查)
      holddate            = sy-datum          " 在此日期前不自動處理
    EXCEPTIONS
      client_invalid      = 1
      destination_invalid = 2
      group_invalid       = 3
      group_is_locked     = 4
      holddate_invalid    = 5
      internal_error      = 6
      queue_error         = 7
      running             = 8
      system_lock_error   = 9
      user_invalid        = 10
      OTHERS              = 11.

  IF sy-subrc <> 0.
    MESSAGE |BDC_OPEN_GROUP failed RC={ sy-subrc }| TYPE 'E'.
  ENDIF.

  " 2) 每一筆資料塞一個交易
  LOOP AT gt_xls INTO DATA(ls_xls) WHERE status = space.

    PERFORM build_bdc_co11n USING ls_xls CHANGING lt_bdc.

    CALL FUNCTION 'BDC_INSERT'
      EXPORTING
        tcode            = 'CO11N'
      TABLES
        dynprotab        = lt_bdc
      EXCEPTIONS
        internal_error   = 1
        not_open         = 2
        queue_error      = 3
        tcode_invalid    = 4
        printing_invalid = 5
        posting_invalid  = 6
        OTHERS           = 7.

  ENDLOOP.

  " 3) 關群組(此時才真的產生 SM35 的 session)
  CALL FUNCTION 'BDC_CLOSE_GROUP'
    EXCEPTIONS
      not_open    = 1
      queue_error = 2
      OTHERS      = 3.

  MESSAGE |批次輸入群組 { gc_group } 已建立,請至 SM35 執行| TYPE 'S'.

SM35 的三種執行模式,跟 CALL TRANSACTION 的 MODE 是對應的:

  • Process/Foreground(前景):一個畫面一個畫面走,用來看 BDC 到底卡在哪。
  • Display errors only(僅顯示錯誤):正常畫面自動跑過,出錯才停下來讓你改。日常補跑最常用。
  • Background(背景):完全不顯示,交給背景作業跑。
要讓 session 自己在背景被消化,排一支標準程式 RSBDCSUB 的背景作業,指定 group 名稱即可。SM35 的 Log 按鈕可以看到每一筆的訊息,錯誤 session 會保留,修完再送。相關資料表:APQI(群組表頭)、APQD(資料)。
Session 沒有「即時回饋」。你的程式跑完只知道「已建立群組」,不知道成功幾筆。如果業務要求「上傳完馬上告訴我哪幾筆錯」,就必須用 CALL TRANSACTION;或用 Session 但另外寫一支程式去讀 SM35 的 log(BDC_... 相關 FM/APQI)回報,複雜度高很多。

8. BDC 的九個坑(老師特別強調)

#解法
1日期格式:BDC 的值等同「使用者敲進去的字」。同一支程式,A 使用者 SU3 設 DD.MM.YYYY、B 設 YYYY/MM/DD,結果就不同WRITE lv_date TO lv_char 或 FM CONVERSION_EXIT_DATUM_OUTPUT 轉成「目前使用者的外部格式」再送;或在背景作業指定一個格式固定的技術帳號
2小數點格式:SU3 的 Decimal notation 有 1,234,567.89 / 1.234.567,89 / 1 234 567,89 三種數量金額也要用 WRITE ... TO 轉外部格式;或全公司統一 SU3 設定並寫進上線 checklist
3SU3 使用者參數:Parameter ID 會自動帶預設值進畫面欄位(例如工廠 WRK、公司代碼 BUK)BDC 要明確送出每一個欄位值,不要依賴預設值;或替背景作業建立參數乾淨的專用帳號
4前導零 / ALPHA:Excel 的 1000123 和資料庫的 001000123 對不起來丟進 BDC 前先跑 CONVERSION_EXIT_ALPHA_INPUT;顯示回使用者時用 ..._OUTPUT
5Excel 帶來的引號與空白:CSV 匯出常變成 "1000123"講義用 TRANSLATE t_xls-aufnr USING '" ' 把雙引號換成空白,再 CONDENSE。現代寫法用 REPLACE ALL OCCURRENCES OF '"' IN lv_x WITH space
6Table Control 捲動:畫面一次只顯示 N 列,第 N+1 列必須先翻頁見下方程式;核心是每 N 筆送一次 BDC_OKCODE = '=P+',並用 BDC_CURSOR 定位
7畫面大小:Table Control 可見列數隨 GUI 視窗大小改變defsize = 'X',或改用「每頁固定第 1 列輸入」的定位欄位(很多交易有 Position 欄)
8Enhancement / User Exit 跳出額外畫面:正式機有客製檢查,開發機沒有錄製一定要在「有客製的環境」錄;上線前用 MODE 'A' 在 QAS 完整跑一次
9SAP 升級後畫面改版:欄位改名、畫面編號改變 → BDC 全部失效這是 BDC 的原罪。所以 S/4HANA 一律「先找 BAPI,找不到才 BDC」
*&--- Table Control 捲動的標準寫法(以每頁可輸入 lc_rows 列為例)
CONSTANTS lc_rows TYPE i VALUE 5.        " 該畫面每頁可輸入列數

DATA(lv_idx) = 0.

LOOP AT lt_item INTO DATA(ls_item).

  DATA(lv_row) = lv_idx MOD lc_rows + 1.   " 本頁的第幾列(1..lc_rows)

  IF lv_row = 1.
    " 每頁的第一列 → 先開新畫面
    APPEND VALUE bdcdata( program  = 'SAPMV45A'
                          dynpro   = '4001'
                          dynbegin = abap_true ) TO lt_bdc.
    " 游標放在本頁第一列,確保系統認得目前的捲動位置
    APPEND VALUE bdcdata( fnam = 'BDC_CURSOR'
                          fval = |VBAP-POSNR({ lv_row WIDTH = 2 ALIGN = RIGHT PAD = '0' })| ) TO lt_bdc.
  ENDIF.

  APPEND VALUE bdcdata(
    fnam = |RV45A-MABNR({ lv_row WIDTH = 2 ALIGN = RIGHT PAD = '0' })|
    fval = ls_item-matnr ) TO lt_bdc.
  APPEND VALUE bdcdata(
    fnam = |VBAP-KWMENG({ lv_row WIDTH = 2 ALIGN = RIGHT PAD = '0' })|
    fval = |{ ls_item-kwmeng }| ) TO lt_bdc.

  lv_idx = lv_idx + 1.

  " 本頁塞滿了,或已是最後一筆 → 決定按什麼鍵
  IF lv_idx MOD lc_rows = 0.
    APPEND VALUE bdcdata( fnam = 'BDC_OKCODE' fval = '=P+' ) TO lt_bdc.   " 翻下一頁
  ENDIF.

ENDLOOP.

" 最後補一次 Enter 與存檔
APPEND VALUE bdcdata( fnam = 'BDC_OKCODE' fval = '/00'  ) TO lt_bdc.
APPEND VALUE bdcdata( fnam = 'BDC_OKCODE' fval = '=SICH' ) TO lt_bdc.
Table Control 欄位名的寫法結構-欄位(列號),列號固定兩位、右靠、補零,例如 VBAP-KWMENG(03)。這是 BDC 新手最常寫錯的地方(寫成 (3) 就找不到欄位)。字串範本 WIDTH = 2 ALIGN = RIGHT PAD = '0' 剛好可以生出來。

9. BDC vs BAPI:S/4HANA 的選用原則

這是本章最重要的觀念題。老師的立場很明確:能用 BAPI 就不要用 BDC。但 BDC 沒有死,它在 S/4HANA 上仍有無可取代的場合。

比較BDC(CALL TRANSACTION)BAPI / Function ModuleOData / RAP
執行對象畫面(PBO/PAI)函數介面服務介面
效能慢(每筆都跑一次畫面)
檢查完整度最完整(含所有 Exit、客製)只有該 BAPI 實作的檢查依 Behavior Definition
升級穩定度,畫面一改就壞好,SAP 保證介面相容
能否背景執行可(MODE 'N')
Fiori 畫面完全不能錄原生支援
回傳訊息BDCMSGCOLLBAPIRET2Message container
S/4HANA 三個必須知道的變化:
  • Fiori/SAP GUI for HTML 的畫面錄不了 BDC。SHDB 只能錄傳統 SAP GUI Dynpro。如果那支交易在 S/4HANA 只剩 Fiori App,就沒有 BDC 這條路,只能找 OData 或 BAPI。
  • 很多經典交易在 S/4HANA 被廢止或改版(例如客戶/供應商主檔統一成 Business Partner,XD01/MK01 已不建議使用,改用 BPBAPI_BUPA_* / CVI)。舊系統轉過來的 BDC 程式一定要重驗。
  • 資料遷移改用 Migration Cockpit(LTMC / Fiori「Migrate Your Data」),LSMW 在 S/4HANA 已不受支援(SAP Note 2287723)。期初資料不要再寫 BDC。

以 CO11N 報工為例,S/4HANA 的建議寫法是改用 BAPI,錯誤處理乾淨得多:

*&--- 以 BAPI 取代 CO11N 的 BDC(生產訂單確認)
DATA lt_return TYPE STANDARD TABLE OF bapiret2.

DATA(lt_conf) = VALUE bapi_pp_timeticket_tab(
  ( orderid   = ls_xls-aufnr            " 工單
    operation = ls_xls-vornr            " 作業
    yield     = ls_xls-lmnga            " 產出量
    scrap     = ls_xls-xmnga            " 壞品量
    un_yield  = ls_xls-meinh
    postg_date = lv_budat
    ex_finish  = abap_true
    conf_activity1 = ls_xls-ism01
    un_activity1   = ls_xls-ile01 ) ).

CALL FUNCTION 'BAPI_PRODORDCONF_CREATE_TT'
  EXPORTING
    post_wrong_entries = '0'          " 有錯就整批不寫
  TABLES
    timetickets        = lt_conf
    return             = lt_return.

IF line_exists( lt_return[ type = 'E' ] ) OR line_exists( lt_return[ type = 'A' ] ).
  CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
  ls_xls-status = 'E'.
  ls_xls-msg    = lt_return[ type = 'E' ]-message.
ELSE.
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = abap_true.
  ls_xls-status = 'S'.
  ls_xls-msg    = '報工成功'.
ENDIF.
找 BAPI 的三個方法:(1) BAPI 交易碼看 Business Object Repository;(2) SE37BAPI_* 加關鍵字模糊搜尋;(3) 在該交易的畫面做一次操作,用 ST05 追 SQL 找到主表,再用 SE84 找相關函數群組。找不到 BAPI 時再回頭考慮 BDC。

10. 前景檔案處理:GUI_UPLOAD / GUI_DOWNLOAD / XLSX

「前景」指的是使用者坐在 SAP GUI 前面。這時候可以動使用者的 PC 檔案,用的是 CL_GUI_FRONTEND_SERVICES(或它包裝的 GUI_UPLOAD / GUI_DOWNLOAD)。

*&--- 選檔對話框(講義的 GET_PC_FILE 現代化)
FORM get_pc_file CHANGING cv_file TYPE rlgrap-filename.

  DATA: lt_ftab   TYPE filetable,
        lv_rc     TYPE i,
        lv_action TYPE i.

  cl_gui_frontend_services=>file_open_dialog(
    EXPORTING
      window_title            = '請選擇上傳檔案'
      initial_directory       = 'C:\Temp'
      multiselection          = abap_false
      default_extension       = 'csv'
      file_filter             = |CSV (*.csv)\|*.csv\|Excel (*.xlsx)\|*.xlsx\|All (*.*)\|*.*\||
    CHANGING
      file_table              = lt_ftab
      rc                      = lv_rc
      user_action             = lv_action
    EXCEPTIONS
      file_open_dialog_failed = 1
      cntl_error              = 2
      error_no_gui            = 3
      not_supported_by_gui    = 4
      OTHERS                  = 5 ).

  IF sy-subrc <> 0
     OR lv_action = cl_gui_frontend_services=>action_cancel.
    RETURN.
  ENDIF.

  IF line_exists( lt_ftab[ 1 ] ).
    cv_file = lt_ftab[ 1 ]-filename.
  ENDIF.

ENDFORM.
*&--- 讀 CSV / TXT:GUI_UPLOAD + 自行切欄
FORM upload_csv USING iv_file TYPE string.

  TYPES: BEGIN OF ty_line,
           line TYPE string,
         END OF ty_line.

  DATA lt_raw TYPE STANDARD TABLE OF ty_line.

  cl_gui_frontend_services=>gui_upload(
    EXPORTING
      filename                = iv_file
      filetype                = 'ASC'          " ASC=文字 BIN=二進位
      has_field_separator     = abap_false     " 自己切比較可控
      codepage                = '4110'         " UTF-8;BIG5 用 '8300'
    CHANGING
      data_tab                = lt_raw
    EXCEPTIONS
      file_open_error         = 1
      file_read_error         = 2
      no_batch                = 3              " ← 背景執行會噴這個!
      gui_refuse_filetransfer = 4
      invalid_type            = 5
      OTHERS                  = 19 ).

  IF sy-subrc <> 0.
    MESSAGE |檔案讀取失敗 RC={ sy-subrc }| TYPE 'E'.
  ENDIF.

  " 第 1 列是標題,從第 2 列開始
  LOOP AT lt_raw INTO DATA(ls_raw) FROM 2.

    " 先清掉 Excel 匯出常見的雙引號與 BOM
    DATA(lv_line) = ls_raw-line.
    REPLACE ALL OCCURRENCES OF '"' IN lv_line WITH ``.
    REPLACE ALL OCCURRENCES OF cl_abap_char_utilities=>byte_order_mark_utf8
            IN lv_line WITH ``.

    SPLIT lv_line AT ','
      INTO DATA(lv_aufnr) DATA(lv_lmnga) DATA(lv_meinh) DATA(lv_xmnga)
           DATA(lv_grund) DATA(lv_ism01) DATA(lv_ile01) DATA(lv_ism02)
           DATA(lv_ile02) DATA(lv_ism03) DATA(lv_ile03) DATA(lv_ism04)
           DATA(lv_ile04) DATA(lv_budat) DATA(lv_vornr).

    APPEND VALUE ty_xls( aufnr = condense( lv_aufnr )
                         lmnga = condense( lv_lmnga )
                         meinh = condense( lv_meinh )
                         xmnga = condense( lv_xmnga )
                         grund = condense( lv_grund )
                         ism01 = condense( lv_ism01 )
                         ile01 = condense( lv_ile01 )
                         ism02 = condense( lv_ism02 )
                         ile02 = condense( lv_ile02 )
                         ism03 = condense( lv_ism03 )
                         ile03 = condense( lv_ile03 )
                         ism04 = condense( lv_ism04 )
                         ile04 = condense( lv_ile04 )
                         budat = condense( lv_budat )
                         vornr = condense( lv_vornr ) ) TO gt_xls.

  ENDLOOP.

ENDFORM.
編碼與 BOM 是 CSV 的兩大殺手(尤其中文)。
  • codepage 傳錯,中文會變亂碼。常見 SAP codepage:1100 Latin-1、4110 UTF-8、4103 UTF-16LE、8300 繁體中文 BIG5。實際可用值請查 TCP00 / TCP0C
  • Excel 另存 CSV UTF-8 時會在檔頭放 BOMEF BB BF),第一個欄位會多三個看不見的字元,比對永遠不相等。用 cl_abap_char_utilities=>byte_order_mark_utf8 清掉。
  • 反過來,SAP 產生給 Excel 開的 UTF-8 CSV,要主動加上 BOM,否則 Excel 用 ANSI 解讀,中文變亂碼。
*&--- 下載:讓 Excel 打得開的 UTF-8 CSV(含 BOM、Tab 分隔)
FORM download_csv USING iv_file TYPE string.

  DATA lt_out TYPE STANDARD TABLE OF string.

  " 第一列前面掛 BOM,Excel 才會用 UTF-8 開
  APPEND |{ cl_abap_char_utilities=>byte_order_mark_utf8 }工單{ cl_abap_char_utilities=>horizontal_tab }| &&
         |作業{ cl_abap_char_utilities=>horizontal_tab }| &&
         |狀況{ cl_abap_char_utilities=>horizontal_tab }訊息| TO lt_out.

  LOOP AT gt_xls INTO DATA(ls_xls).
    APPEND |{ ls_xls-aufnr }{ cl_abap_char_utilities=>horizontal_tab }| &&
           |{ ls_xls-vornr }{ cl_abap_char_utilities=>horizontal_tab }| &&
           |{ ls_xls-status }{ cl_abap_char_utilities=>horizontal_tab }| &&
           |{ ls_xls-msg }| TO lt_out.
  ENDLOOP.

  cl_gui_frontend_services=>gui_download(
    EXPORTING
      filename                = iv_file
      filetype                = 'ASC'
      write_field_separator   = abap_false      " 已自己放 TAB
      codepage                = '4110'
      trunc_trailing_blanks   = abap_true
    CHANGING
      data_tab                = lt_out
    EXCEPTIONS
      file_write_error        = 1
      no_batch                = 2
      gui_refuse_filetransfer = 3
      OTHERS                  = 24 ).

  IF sy-subrc <> 0.
    MESSAGE |檔案下載失敗 RC={ sy-subrc }| TYPE 'E'.
  ENDIF.

ENDFORM.

講義用 TEXT_CONVERT_XLS_TO_SAP 讀 Excel。這支在舊 .xls 上能用,但對 .xlsx 常常失敗、還會受本機 Excel 版本影響。S/4HANA 建議改用 CL_FDT_XL_SPREADSHEET(標準內建,純 ABAP 解析,不需要前端 Excel):

*&--- 讀 XLSX(S/4HANA 建議做法,純 ABAP、可在背景使用)
FORM upload_xlsx USING iv_xstring TYPE xstring.

  TRY.
      DATA(lo_xl) = NEW cl_fdt_xl_spreadsheet(
                        document_name = 'upload.xlsx'
                        xdocument     = iv_xstring ).

      " 取得所有工作表名稱
      lo_xl->if_fdt_doc_spreadsheet~get_worksheet_names(
        IMPORTING worksheet_names = DATA(lt_sheets) ).

      " 讀第一張工作表,回傳一個動態內表的參考
      DATA(lo_data) = lo_xl->if_fdt_doc_spreadsheet~get_itab_from_worksheet(
                          worksheet_name = lt_sheets[ 1 ] ).

      ASSIGN lo_data->* TO FIELD-SYMBOL(<lt_sheet>).

      LOOP AT <lt_sheet> ASSIGNING FIELD-SYMBOL(<ls_row>) FROM 2.
        " <ls_row> 是動態結構,用 ASSIGN COMPONENT 取欄位
        ASSIGN COMPONENT 1 OF STRUCTURE <ls_row> TO FIELD-SYMBOL(<lv_c1>).
        ASSIGN COMPONENT 2 OF STRUCTURE <ls_row> TO FIELD-SYMBOL(<lv_c2>).
        APPEND VALUE ty_xls( aufnr = <lv_c1> vornr = <lv_c2> ) TO gt_xls.
      ENDLOOP.

    CATCH cx_fdt_excel_core INTO DATA(lx_xl).
      MESSAGE lx_xl->get_text( ) TYPE 'E'.
  ENDTRY.

ENDFORM.
讀 Excel 的四種方案比較
  • TEXT_CONVERT_XLS_TO_SAP:講義用的,簡單但只穩定支援舊 .xls,且需要 GUI。
  • ALSM_EXCEL_TO_INTERNAL_TABLE:老牌,但上限 9999 列 / 欄寬有限、需要 GUI、逐格回傳很慢。
  • CL_FDT_XL_SPREADSHEET標準內建、可在背景跑,讀 .xlsx 首選。缺點是只能讀不能寫。
  • abap2xlsx(開源 ZCL_EXCEL):讀寫都行、可設格式與公式,但要另外安裝。
最省事的做法其實是:請對方給 CSV,不要給 Excel。

11. 應用伺服器檔案:OPEN DATASET / AL11

背景作業裡絕對不能用 GUI_UPLOAD / GUI_DOWNLOAD / CL_GUI_FRONTEND_SERVICES——沒有 SAP GUI,會噴 no_batch 例外或 CNTL_ERROR。背景要動檔案,只能動應用伺服器上的檔案,用 OPEN DATASET,用 AL11 瀏覽。

*&--- 寫應用伺服器檔案(文字模式、UTF-8)
FORM write_app_server_file USING iv_path TYPE string.

  DATA lv_msg TYPE string.

  TRY.
      OPEN DATASET iv_path
           FOR OUTPUT
           IN TEXT MODE ENCODING UTF-8
           WITH SMART LINEFEED            " 依 OS 自動決定換行字元
           MESSAGE lv_msg.

      IF sy-subrc <> 0.
        MESSAGE |開檔失敗:{ lv_msg }| TYPE 'E'.
      ENDIF.

      LOOP AT gt_xls INTO DATA(ls_xls).
        DATA(lv_line) = |{ ls_xls-aufnr },{ ls_xls-vornr },{ ls_xls-status },{ ls_xls-msg }|.
        TRANSFER lv_line TO iv_path.
      ENDLOOP.

      CLOSE DATASET iv_path.

    CATCH cx_sy_file_open cx_sy_file_authority cx_sy_file_io INTO DATA(lx_file).
      MESSAGE lx_file->get_text( ) TYPE 'E'.
  ENDTRY.

ENDFORM.

*&--- 讀應用伺服器檔案
FORM read_app_server_file USING iv_path TYPE string.

  DATA lv_line TYPE string.

  OPEN DATASET iv_path FOR INPUT IN TEXT MODE ENCODING UTF-8.
  IF sy-subrc <> 0.
    MESSAGE |檔案不存在:{ iv_path }| TYPE 'E'.
  ENDIF.

  DO.
    READ DATASET iv_path INTO lv_line.
    IF sy-subrc <> 0.
      EXIT.
    ENDIF.
    " ...解析 lv_line...
  ENDDO.

  CLOSE DATASET iv_path.

  " 處理完把檔案移到備份目錄(同一台機器才能 rename)
  DATA(lv_bak) = |{ iv_path }.{ sy-datum }{ sy-uzeit }.bak|.
  " DELETE DATASET iv_path.  " 或用外部指令 mv

ENDFORM.
模式語法用途
文字(Unicode)IN TEXT MODE ENCODING UTF-8 / DEFAULT / NON-UNICODE一般 CSV/TXT,逐列讀寫
二進位IN BINARY MODEPDF、XLSX、圖片,配 TYPE xstring
舊式文字IN LEGACY TEXT MODE CODE PAGE '8300'要產生 BIG5 等非 Unicode 編碼給舊系統吃
附加FOR APPENDING寫入 log 檔不覆蓋
路徑不要寫死。FILE(邏輯檔案名稱)定義邏輯路徑,程式用 FILE_GET_NAME 取實體路徑;這樣 DEV/QAS/PRD 的目錄不同也不必改程式。AL11 可以看到伺服器上的目錄結構與 DIR_HOMEDIR_TRANS 等別名。
*--- 用邏輯檔案名取得實體路徑(跨環境安全)
DATA lv_phys TYPE string.

CALL FUNCTION 'FILE_GET_NAME'
  EXPORTING
    logical_filename = 'ZPP_WORKTIME_IN'      " FILE 交易碼裡定義
    parameter_1      = |{ sy-datum }|
  IMPORTING
    file_name        = lv_phys
  EXCEPTIONS
    file_not_found   = 1
    OTHERS           = 2.
*&--- 編碼轉換:xstring <-> string(S/4HANA 1809 = 7.53 起可用)
DATA(lv_xstr) = iv_binary.

" UTF-8 位元組 → 字串
DATA(lo_in)  = cl_abap_conv_codepage=>create_in( codepage = 'UTF-8' ).
DATA(lv_str) = lo_in->convert( source = lv_xstr ).

" 字串 → BIG5 位元組(產檔給舊系統)
DATA(lo_out) = cl_abap_conv_codepage=>create_out( codepage = 'BIG5' ).
DATA(lv_big5) = lo_out->convert( source = lv_str ).

" 切成一行一行
SPLIT lv_str AT cl_abap_char_utilities=>cr_lf INTO TABLE DATA(lt_lines).
OPEN DATASET 的權限:S/4HANA 會做 S_DATASET 權限檢查與 Validation/Virus Scan Profile/SCET/...)。正式機常常「開發機能跑、正式機開檔失敗 sy-subrc = 8」,八成是 Basis 沒開該目錄的權限。永遠加 MESSAGE lv_msg 把 OS 層錯誤訊息接出來,不然你只會看到一個 8。

12. FTP:SAP 標準函數群完整流程

講義的 FTP 流程設計得很完整,值得整套背下來:連線 → 檢查來源/備份目錄 → 設 binary 模式 → 列檔名 → 逐檔 get → 備份到 BAK 目錄 → 刪來源 → 斷線。這個「取走後立刻搬到備份目錄並刪除來源」的設計,可以保證同一個檔不會被處理兩次。

函數用途重點參數
HTTP_SCRAMBLE把密碼加擾(FTP_CONNECT 只吃加擾過的密碼)sourcesourcelenkey(慣用 26101957
FTP_CONNECT建立連線userpassword(已加擾)/hostrfc_destination → 回 handle
FTP_COMMAND下任意 FTP 指令:cdbinascnlistdirgetputdelmkdirhandlecommand,結果回 TABLES data
FTP_SERVER_TO_R3把 FTP 上的檔案抓進 ABAP 內表fnameTABLES blob(二進位)或 text(文字);回 blob_length
FTP_R3_TO_SERVER把 ABAP 內表寫回 FTPfnameblob_lengthTABLES blob
FTP_DISCONNECT斷線handle
RFC_CONNECTION_CLOSE釋放 RFC 連線(斷線後一定要做)destination
兩個 RFC destination(SM59 建立,類型 T / TCP-IP,程式 sapftp
  • SAPFTP:走前端(使用者的 PC),只能在有 GUI 時用。
  • SAPFTPA:走應用伺服器,可以在背景作業執行。介面程式一律用這個。講義的註解也特別標了:「需至 TOP 區設定 w_ftp_dest = 'SAPFTPA'(可 background 執行)」。
*&--- FTP 完整樣板(現代化改寫)
CLASS lcl_ftp DEFINITION.
  PUBLIC SECTION.
    METHODS: connect    IMPORTING iv_host TYPE string
                                  iv_user TYPE string
                                  iv_pwd  TYPE string
                        RAISING   cx_static_check,
             command    IMPORTING iv_cmd    TYPE string
                        RETURNING VALUE(rt_result) TYPE ty_t_text,
             get_file   IMPORTING iv_fname  TYPE string
                        RETURNING VALUE(rv_xstr) TYPE xstring,
             put_file   IMPORTING iv_fname  TYPE string
                                  iv_xstr   TYPE xstring,
             disconnect.
  PRIVATE SECTION.
    CONSTANTS: gc_key  TYPE i           VALUE 26101957,
               gc_dest TYPE rfcdes-rfcdest VALUE 'SAPFTPA'.  " 背景可用
    DATA gv_handle TYPE i.
ENDCLASS.

CLASS lcl_ftp IMPLEMENTATION.

  METHOD connect.

    DATA lv_pwd TYPE scms_ftppw.

    lv_pwd = iv_pwd.

    " 密碼必須先加擾,FTP_CONNECT 不吃明碼
    CALL FUNCTION 'HTTP_SCRAMBLE'
      EXPORTING
        source      = lv_pwd
        sourcelen   = strlen( lv_pwd )
        key         = gc_key
      IMPORTING
        destination = lv_pwd.

    CALL FUNCTION 'FTP_CONNECT'
      EXPORTING
        user            = iv_user
        password        = lv_pwd
        host            = iv_host
        rfc_destination = gc_dest
      IMPORTING
        handle          = gv_handle
      EXCEPTIONS
        not_connected   = 1
        OTHERS          = 2.

    IF sy-subrc <> 0.
      RAISE EXCEPTION TYPE cx_sy_conversion_error.   " 實務上換成自建例外
    ENDIF.

  ENDMETHOD.

  METHOD command.

    CALL FUNCTION 'FTP_COMMAND'
      EXPORTING
        handle        = gv_handle
        command       = iv_cmd            " 'cd /CO01/'、'bin'、'nlist'、'del x.csv'
      TABLES
        data          = rt_result
      EXCEPTIONS
        tcpip_error   = 1
        command_error = 2
        data_error    = 3
        OTHERS        = 4.

  ENDMETHOD.

  METHOD get_file.

    DATA: lt_blob TYPE STANDARD TABLE OF ty_x255,
          lv_len  TYPE i.

    CALL FUNCTION 'FTP_SERVER_TO_R3'
      EXPORTING
        handle        = gv_handle
        fname         = iv_fname
      IMPORTING
        blob_length   = lv_len
      TABLES
        blob          = lt_blob            " 二進位;文字檔可改用 text 參數
      EXCEPTIONS
        tcpip_error   = 1
        command_error = 2
        data_error    = 3
        OTHERS        = 4.

    CHECK sy-subrc = 0.

    CALL FUNCTION 'SCMS_BINARY_TO_XSTRING'
      EXPORTING
        input_length = lv_len
      IMPORTING
        buffer       = rv_xstr
      TABLES
        binary_tab   = lt_blob.

  ENDMETHOD.

  METHOD put_file.

    DATA: lt_blob TYPE STANDARD TABLE OF ty_x255,
          lv_len  TYPE i.

    CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
      EXPORTING
        buffer        = iv_xstr
      IMPORTING
        output_length = lv_len
      TABLES
        binary_tab    = lt_blob.

    CALL FUNCTION 'FTP_R3_TO_SERVER'
      EXPORTING
        handle        = gv_handle
        fname         = iv_fname
        blob_length   = lv_len
      TABLES
        blob          = lt_blob
      EXCEPTIONS
        tcpip_error   = 1
        command_error = 2
        data_error    = 3
        OTHERS        = 4.

  ENDMETHOD.

  METHOD disconnect.

    CALL FUNCTION 'FTP_DISCONNECT'
      EXPORTING handle = gv_handle.

    " 這一步很多人忘記:不做會累積 RFC 連線
    CALL FUNCTION 'RFC_CONNECTION_CLOSE'
      EXPORTING destination = gc_dest
      EXCEPTIONS OTHERS = 1.

  ENDMETHOD.

ENDCLASS.

主流程(對應講義的 GET_DATA_FROM_FTP / BASIS_SETTING):

FORM get_data_from_ftp.

  DATA(lo_ftp) = NEW lcl_ftp( ).

  TRY.
      lo_ftp->connect( iv_host = CONV #( p_ip )
                       iv_user = CONV #( p_user )
                       iv_pwd  = CONV #( p_pw ) ).
    CATCH cx_root.
      PERFORM send_alert_mail USING 'FTP connect fail !'.
      STOP.
  ENDTRY.

  lo_ftp->command( 'bin' ).                       " 二進位模式,避免換行字元被改
  lo_ftp->command( |cd { p_fpath }| ).            " 進入收檔目錄

  " 只列檔名(nlist),dir 會帶權限/日期等雜訊
  DATA(lt_files) = lo_ftp->command( 'nlist' ).

  LOOP AT lt_files INTO DATA(ls_file).

    " 只處理 .csv
    SPLIT ls_file-text AT '.' INTO DATA(lv_name) DATA(lv_ext).
    CHECK to_lower( lv_ext ) = 'csv'.

    DATA(lv_xstr) = lo_ftp->get_file( CONV #( ls_file-text ) ).
    IF lv_xstr IS INITIAL.
      PERFORM send_alert_mail USING |Read { ls_file-text } from FTP fail !|.
      CONTINUE.
    ENDIF.

    PERFORM parse_csv_xstring USING lv_xstr.      " 解析進 gt_xls

    " 備份:切到 BAK 目錄 put 一份,再回來刪掉原檔
    lo_ftp->command( |cd ..| ).
    lo_ftp->command( |cd { p_fbakup }| ).
    lo_ftp->put_file( iv_fname = CONV #( ls_file-text ) iv_xstr = lv_xstr ).
    lo_ftp->command( |cd ..| ).

    lo_ftp->command( |cd { p_fpath }| ).
    lo_ftp->command( |del { ls_file-text }| ).

  ENDLOOP.

  lo_ftp->disconnect( ).

ENDFORM.

取回的是二進位,講義用 SCMS_BINARY_TO_TEXTENCODING = '4103')轉成文字表再 SPLIT AT ','。現代做法直接對 xstring 轉碼:

FORM parse_csv_xstring USING iv_xstr TYPE xstring.

  TRY.
      DATA(lv_text) = cl_abap_conv_codepage=>create_in( codepage = 'UTF-8'
                        )->convert( source = iv_xstr ).
    CATCH cx_sy_conversion_codepage.
      " 若來源是 BIG5,改用 BIG5 再轉一次
      lv_text = cl_abap_conv_codepage=>create_in( codepage = 'BIG5'
                  )->convert( source = iv_xstr ).
  ENDTRY.

  REPLACE ALL OCCURRENCES OF cl_abap_char_utilities=>byte_order_mark_utf8
          IN lv_text WITH ``.

  SPLIT lv_text AT cl_abap_char_utilities=>cr_lf INTO TABLE DATA(lt_lines).

  " 第一列是標題(講義寫 LOOP AT t_texttab FROM 2 就是這個意思)
  LOOP AT lt_lines INTO DATA(lv_line) FROM 2.
    CHECK lv_line IS NOT INITIAL.
    " ...SPLIT AT ',' 進 gt_xls...
  ENDLOOP.

ENDFORM.
FTP 在 2020 年代已經不該用了。帳密與資料都是明碼傳輸。SAP 標準的 FTP_* 函數不支援 SFTP/FTPS。S/4HANA 的替代方案:
  • SFTP:用 SM69 定義外部指令、SXPG_COMMAND_EXECUTE 呼叫 OS 上的 sftpscp(配金鑰認證),SAP 端只讀寫應用伺服器目錄(AL11)。
  • HTTPS / REST:用 CL_HTTP_CLIENTCL_REST_HTTP_CLIENT,SM59 建 G 型 destination,走 STRUST 憑證。
  • 中介平台:SAP Integration Suite(CPI)/PO 有現成的 SFTP adapter,是最乾淨的選擇。
  • 不論哪一種,密碼一律不要寫在程式或 PARAMETERS 預設值裡

13. 寄信:CL_BCS 完整樣板

講義用的是舊 API SO_DOCUMENT_SEND_API1(更舊的還有 SO_NEW_DOCUMENT_ATT_SEND_API1)。這兩支要自己組 packing list、算 doc_size,很容易算錯導致附件壞掉。S/4HANA 一律建議改用 CL_BCS(Business Communication Services),物件導向、附件用一行 add_attachment 就好。

*&--- CL_BCS 完整樣板:HTML 本文 + PDF 附件 + XLSX 附件 + 多收件人
FORM send_mail_bcs USING it_recv   TYPE ty_t_email
                         iv_pdf    TYPE xstring
                         iv_xlsx   TYPE xstring.

  TRY.
      "--- 1) 建立傳送請求
      DATA(lo_send) = cl_bcs=>create_persistent( ).

      "--- 2) 本文(HTM 或 RAW)
      DATA(lt_body) = VALUE bcsy_text(
        ( line = '<p>各位好:</p>' )
        ( line = |<p>MES 工時回報上傳 SAP 已於 { sy-datum DATE = USER } { sy-uzeit TIME = USER } 完成。</p>| )
        ( line = |<p>成功 { lines( VALUE ty_t_xls( FOR w IN gt_xls WHERE ( status = 'S' ) ( w ) ) ) } 筆,| &&
                 |失敗 { lines( VALUE ty_t_xls( FOR w IN gt_xls WHERE ( status = 'E' ) ( w ) ) ) } 筆,明細請見附件。</p>| )
        ( line = '<p>(本信件由系統自動發出,請勿直接回覆)</p>' ) ).

      DATA(lo_doc) = cl_document_bcs=>create_document(
                       i_type    = 'HTM'                 " RAW=純文字 HTM=網頁
                       i_text    = lt_body
                       i_subject = CONV so_obj_des( 'MES 工時回報上傳 SAP' ) ).

      "--- 3) 附件:PDF(xstring → solix)
      IF iv_pdf IS NOT INITIAL.
        lo_doc->add_attachment(
          i_attachment_type    = 'PDF'
          i_attachment_subject = CONV so_obj_des( |工時上傳結果_{ sy-datum }| )   " 最長 50 字元
          i_att_content_hex    = cl_bcs_convert=>xstring_to_solix( iv_pdf ) ).
      ENDIF.

      "--- 4) 附件:XLSX
      IF iv_xlsx IS NOT INITIAL.
        lo_doc->add_attachment(
          i_attachment_type    = 'XLS'
          i_attachment_subject = CONV so_obj_des( |明細_{ sy-datum }.xlsx| )
          i_att_content_hex    = cl_bcs_convert=>xstring_to_solix( iv_xlsx ) ).
      ENDIF.

      lo_send->set_document( lo_doc ).

      "--- 5) 寄件者(用 SAP 使用者,或指定一個對外的位址)
      lo_send->set_sender( cl_sapuser_bcs=>create( sy-uname ) ).
      " 或:cl_cam_address_bcs=>create_internet_address( 'sapnoreply@company.com' )

      "--- 6) 收件者
      LOOP AT it_recv INTO DATA(lv_mail).
        lo_send->add_recipient(
          i_recipient = cl_cam_address_bcs=>create_internet_address( CONV adr6-smtp_addr( lv_mail ) )
          i_express   = abap_true ).
      ENDLOOP.

      "--- 7) 立即送出(不等 RSCONN01 排程)
      lo_send->set_send_immediately( abap_true ).
      DATA(lv_ok) = lo_send->send( i_with_error_screen = abap_true ).

      IF lv_ok = abap_true.
        COMMIT WORK AND WAIT.          " ← 沒有 COMMIT 就不會真的寄出!
        MESSAGE 'Mail 已送出,可至 SOST 查詢' TYPE 'S'.
      ELSE.
        MESSAGE '至少一位收件者送出失敗' TYPE 'W'.
      ENDIF.

    CATCH cx_bcs INTO DATA(lx_bcs).
      MESSAGE |寄信失敗:{ lx_bcs->get_text( ) }| TYPE 'E'.
  ENDTRY.

ENDFORM.
CL_BCS 的五個必踩坑:
  • 忘了 COMMIT WORKcreate_persistent 是走 update task 的,沒有 COMMIT 就完全沒動作,而且不會報錯。這是「程式說成功、信卻沒到」的第一名原因。
  • i_attachment_subject 最多 50 字元,超過會被截斷、附檔名跑掉。
  • 附件檔名的副檔名由 i_attachment_type 決定('PDF'/'XLS'/'CSV'/'TXT'/'BIN'),不是由 subject 決定。
  • 不呼叫 set_send_immediately( 'X' ) 的話,信件只會排進佇列,要等背景程式 RSCONN01 跑到才送。
  • SCOT 沒設定就永遠寄不出去SCOT 設定 SMTP node 與網域路由,SOST 查每一封信的狀態(等待/已送/失敗),這兩個交易碼一定要會。

把純文字或內表轉成附件用的 solix,用 CL_BCS_CONVERT

*--- 內表 → CSV 字串 → solix 附件
DATA(lv_csv) = REDUCE string(
  INIT s = |工單,作業,狀況,訊息{ cl_abap_char_utilities=>cr_lf }|
  FOR ls IN gt_xls
  NEXT s = s && |{ ls-aufnr },{ ls-vornr },{ ls-status },{ ls-msg }{ cl_abap_char_utilities=>cr_lf }| ).

DATA(lt_solix) = cl_bcs_convert=>string_to_solix(
                   iv_string   = lv_csv
                   iv_codepage = '4110'          " UTF-8
                   iv_add_bom  = abap_true ).    " 讓 Excel 開得出中文

" 反向:solix → xstring
DATA(lv_x) = cl_bcs_convert=>solix_to_xstring( it_solix = lt_solix ).

講義還示範了「用 SBWP 的通訊群組(Distribution List)當收件人」,好處是 IT 不用改程式,使用者自己在 SBWP 維護名單。用 SO_DLI_LIST_READ_XDL + SO_DLI_READ 把群組成員展開:

*&--- 讀 SBWP 通訊群組成員(講義 GET_MAIL_GROUP 的重點保留)
FORM get_mail_group USING iv_group TYPE sood-objnam
                 CHANGING ct_recv  TYPE ty_t_email.

  DATA: lt_xdl    TYPE STANDARD TABLE OF soxdl WITH EMPTY KEY,
        lt_member TYPE STANDARD TABLE OF sodm1 WITH EMPTY KEY,
        lt_para   TYPE STANDARD TABLE OF selc  WITH EMPTY KEY,
        lt_parb   TYPE STANDARD TABLE OF soop1 WITH EMPTY KEY.

  CALL FUNCTION 'SO_DLI_LIST_READ_XDL'
    EXPORTING
      dli_generic_name   = iv_group
      private            = abap_true          " 私人群組;共用群組改 public = 'X'
    TABLES
      dli_display_tab    = lt_xdl
    EXCEPTIONS
      dl_list_no_entries = 2
      OTHERS             = 7.

  IF sy-subrc <> 0 OR lt_xdl IS INITIAL.
    MESSAGE |通訊群組 { iv_group } 不存在| TYPE 'W'.
    RETURN.
  ENDIF.

  DATA(ls_xdl) = lt_xdl[ 1 ].
  DATA(ls_dli) = VALUE soodk( objtp = ls_xdl-dlitp
                              objyr = ls_xdl-dliyr
                              objno = ls_xdl-dlino ).

  CALL FUNCTION 'SO_DLI_READ'
    EXPORTING
      dli_id  = ls_dli
    TABLES
      member  = lt_member
      objpara = lt_para
      objparb = lt_parb
    EXCEPTIONS
      OTHERS  = 18.

  LOOP AT lt_member INTO DATA(ls_mem).
    CASE ls_mem-memtp.
      WHEN 'ADR'.       " 外部 e-mail 位址
        APPEND ls_mem-address TO ct_recv.
      WHEN 'USR'.       " 內部 SAP 使用者 → 查 SU01 的 e-mail
        SELECT SINGLE smtp_addr
          FROM adr6
          INNER JOIN usr21 ON usr21~persnumber = adr6~persnumber
                          AND usr21~addrnumber = adr6~addrnumber
          WHERE usr21~bname = @ls_mem-memnam
          INTO @DATA(lv_smtp).
        IF sy-subrc = 0.
          APPEND lv_smtp TO ct_recv.
        ENDIF.
    ENDCASE.
  ENDLOOP.

ENDFORM.
舊 API 還會遇到,要看得懂SO_DOCUMENT_SEND_API1 的四張表——PACKING_LIST(描述每個「包」是本文還是附件、從第幾列到第幾列、型別 RAW/PDF/XLS)、CONTENTS_TXT(本文,255 字元一列)、CONTENTS_BIN(附件二進位)、RECEIVERSrec_type = 'U' 外部位址 / 'B' 內部使用者,com_type = 'INT')。它最容易錯的是 doc_size:必須算成 ( 列數 - 1 ) * 255 + 最後一列的實際長度,算錯附件就毀了。這正是改用 CL_BCS 的理由。

14. Spool → PDF → 當附件寄出

講義最後那段 Z_PRINT_SPOOL_PDF 是很實用的技巧:報表印到 Spool,再把 Spool 轉成 PDF 寄出去。這樣「畫面上看到什麼、信裡就收到什麼」,不必為了寄信另外寫一份排版。

*&--- Spool → PDF → xstring
FORM get_spool_pdf CHANGING cv_pdf TYPE xstring.

  DATA: lt_pdf   TYPE STANDARD TABLE OF tline,
        lt_solix TYPE solix_tab,
        lv_bytes TYPE i.

  DATA(lv_spono) = sy-spono.        " 本次執行產生的 Spool 編號
  IF lv_spono IS INITIAL.
    RETURN.                          " 沒有 Spool 就不用寄
  ENDIF.

  CALL FUNCTION 'CONVERT_ABAPSPOOLJOB_2_PDF'
    EXPORTING
      src_spoolid              = lv_spono
      no_dialog                = abap_true
      pdf_destination          = 'X'
    IMPORTING
      pdf_bytecount            = lv_bytes
    TABLES
      pdf                      = lt_pdf
    EXCEPTIONS
      err_no_abap_spooljob     = 1
      err_no_spooljob          = 2
      err_no_permission        = 3
      err_conv_not_possible    = 4
      err_bad_destdevice       = 5
      user_cancelled           = 6
      err_spoolerror           = 7
      err_temseerror           = 8
      err_btcjob_open_failed   = 9
      err_btcjob_submit_failed = 10
      err_btcjob_close_failed  = 11
      OTHERS                   = 12.

  CHECK sy-subrc = 0.

  " PDF 回來是 132 位元組一列的 TLINE,要轉成 255 位元組的 SOLIX 才能當附件
  CALL FUNCTION 'SX_TABLE_LINE_WIDTH_CHANGE'
    EXPORTING
      line_width_dst = 255
    TABLES
      content_in     = lt_pdf
      content_out    = lt_solix
    EXCEPTIONS
      err_line_width_src_too_long = 1
      err_line_width_dst_too_long = 2
      err_conv_failed             = 3
      OTHERS                      = 4.

  cv_pdf = cl_bcs_convert=>solix_to_xstring( it_solix = lt_solix ).

ENDFORM.
講義那個 TRANSLATE ... USING ' ~' 的老技巧:舊寫法把 132 字元的 PDF 列一列列串成一個大字串時,字串串接會把尾端空白吃掉,PDF 就壞了。所以老師先把空白換成 ~、串好之後再換回空白,最後用 SHIFT ... LEFT BY 255 PLACES 切成 255 一段。技巧本身很聰明,但很脆弱(資料裡本來就有 ~ 就毀了)。新程式請直接用 SX_TABLE_LINE_WIDTH_CHANGE
sy-spono 只有在報表真的輸出到 Spool 時才會有值。在前景直接執行、結果顯示在螢幕上時,sy-spono 是空的。要在前景也產生 Spool,用 GET_PRINT_PARAMETERS 取得列印參數後 NEW-PAGE PRINT ON PARAMETERS ls_params NO DIALOG. ... NEW-PAGE PRINT OFF.SP01 可查 Spool,資料表是 TSP01

15. 背景作業:SM36 / SM37 與 JOB_OPEN / SUBMIT / JOB_CLOSE

介面程式的終點是「排成背景作業,每天/每小時自己跑」。兩條路:手動用 SM36 排,或程式自己建 Job

手動排程(SM36)的步驟:(依講義步驟整理)

  1. SM36 → 輸入 Job Name、Job Class(A 最高/B 中/C 一般,一般客製程式用 C)、Target server(可指定跑在哪台 App Server)。
  2. Step:選 ABAP program,填程式名 + Variant(變式)。背景作業一定要有變式,因為沒有人可以在畫面上輸入條件。變式在 SE38 →「以變式執行」→ Save 建立。
  3. Start condition:Immediate(立即)/Date-Time(指定時間)/After job(某個 Job 跑完之後)/After event(事件觸發,SM62 定義事件、SM64 手動觸發)/At operation mode change。
  4. Periodic job 並設定週期(每小時/每天/每週/每月)。
  5. Save。到 SM37 查狀態:Scheduled/Released/Ready/Active/Finished/Cancelled;點進去看 Job logSpool
*&--- 程式內建立背景作業(例如:使用者按下按鈕後排一支長時間的程式去背景跑)
FORM submit_in_background.

  DATA: lv_jobname  TYPE tbtcjob-jobname,
        lv_jobcount TYPE tbtcjob-jobcount.

  lv_jobname = |ZPP_WORKTIME_{ sy-datum }_{ sy-uzeit }|.

  "--- 1) 開 Job
  CALL FUNCTION 'JOB_OPEN'
    EXPORTING
      jobname          = lv_jobname
    IMPORTING
      jobcount         = lv_jobcount
    EXCEPTIONS
      cant_create_job  = 1
      invalid_job_data = 2
      jobname_missing  = 3
      OTHERS           = 4.

  IF sy-subrc <> 0.
    MESSAGE |無法建立作業 RC={ sy-subrc }| TYPE 'E'.
  ENDIF.

  "--- 2) 加入 Step:SUBMIT ... VIA JOB ... AND RETURN
  SUBMIT zppi0002
         WITH p_upftp  = abap_true
         WITH p_fpath  = '/CO01/'
         WITH p_fbakup = '/CO01_BAK/'
         WITH p_mode   = 'N'                    " 背景一定要 N
         WITH p_email  = abap_true
         WITH p_group  = 'PP_WORKTIME'
         USER           sy-uname                " 以哪個帳號執行
         VIA JOB        lv_jobname
         NUMBER         lv_jobcount
         AND RETURN.                            " ← 少了它會直接跑而不是排進 Job

  " 也可以直接套用已存在的變式,最推薦(條件集中維護)
  " SUBMIT zppi0002 USING SELECTION-SET 'DAILY'
  "        VIA JOB lv_jobname NUMBER lv_jobcount AND RETURN.

  "--- 3) 關 Job(此時才真正排入排程)
  CALL FUNCTION 'JOB_CLOSE'
    EXPORTING
      jobcount             = lv_jobcount
      jobname              = lv_jobname
      strtimmed            = abap_true          " 立刻執行
*     sdlstrtdt            = sy-datum           " 或指定日期
*     sdlstrttm            = '220000'           " 與時間
*     predjob_checkstat    = abap_true          " 前置作業成功才跑
    EXCEPTIONS
      cant_start_immediate = 1
      invalid_startdate    = 2
      jobname_missing      = 3
      job_close_failed     = 4
      job_nosteps          = 5
      job_notex            = 6
      lock_failed          = 7
      OTHERS               = 8.

  IF sy-subrc = 0.
    MESSAGE |背景作業 { lv_jobname } 已排定,請至 SM37 查詢| TYPE 'S'.
  ENDIF.

ENDFORM.

寫背景程式的六條紀律:

  • 不可有任何畫面互動MESSAGE ... TYPE 'I'POPUP_TO_CONFIRMCALL SCREENWRITE 之後的互動式清單,全部不行。背景遇到 TYPE 'I''A' 會直接讓 Job Cancel。
  • 不可用任何 CL_GUI_* / GUI_UPLOAD / GUI_DOWNLOAD / OLE。用 sy-batch = abap_true 判斷分流。
  • ALV 在背景會自動印到 SpoolCL_SALV_TABLEdisplay( ) 在背景會輸出成清單,可從 SM37 → Spool 看到。
  • WRITE 寫執行記錄:背景執行時 WRITE 的內容會進 Spool,是最省事的 log。重要訊息另外用 MESSAGE ... TYPE 'S'(會進 Job log)。
  • 長交易要分批 COMMIT:避免 update table 爆掉、避免鎖太久。
  • 要能重跑(idempotent):同一個檔案再跑一次不可以變成雙重過帳。做法是「處理完立刻移走檔案」或「紀錄表用 MODIFY 而非 INSERT」。
*--- 前景/背景分流的標準寫法
IF sy-batch = abap_true.
  " 背景:讀應用伺服器檔案、輸出到 Spool
  PERFORM read_app_server_file USING lv_phys.
  DATA(lo_alv) = NEW cl_salv_table( ).
  cl_salv_table=>factory( IMPORTING r_salv_table = lo_alv
                          CHANGING  t_table      = gt_xls ).
  lo_alv->display( ).                     " 背景 → 自動進 Spool
ELSE.
  " 前景:讓使用者選 PC 檔案、顯示 ALV Grid
  PERFORM get_pc_file CHANGING p_files.
  PERFORM upload_csv  USING CONV string( p_files ).
  PERFORM display_alv_rtn.
ENDIF.

*--- 觸發事件,讓下游 Job 接著跑
CALL FUNCTION 'BP_EVENT_RAISE'
  EXPORTING
    eventid                = 'Z_WORKTIME_LOADED'   " SM62 定義
    eventparm              = CONV btcevtparm( sy-datum )
  EXCEPTIONS
    bad_eventid            = 1
    eventid_does_not_exist = 2
    eventid_missing        = 3
    raise_failed           = 4
    OTHERS                 = 5.
常用資料表與函數TBTCO(Job 表頭/狀態)、TBTCP(Job Step)、TBTCS(排程)、BTCEVTJOB(事件對應)。程式要查 Job 狀態用 BP_JOB_SELECT / BP_JOB_READ;要刪 Job 用 BP_JOB_DELETE。監控整批 Job 的健康度可以自己寫一支報表 SELECT TBTCOstatus = 'A'(Aborted)的作業並寄信告警。

16. S/4HANA 補充:Application Jobs 與 Fiori 排程

SM36/SM37 是「技術人員」的工具,需要 S_BTCH_JOB 權限。S/4HANA 另外提供一套給業務使用者用的排程機制

  • Application Jobs(Fiori App「Application Jobs」/ F1240):使用者在 Fiori 上選一個 Job Template、填參數、設定週期,就能自己排程,不必進 SAP GUI,也不需要 SM36 的權限。
  • Job Catalog Entry:開發端要做的事。建立一個實作 IF_APJ_DPC_RUN(定義參數與預設值)與 IF_APJ_RT_RUN(實際執行邏輯)的 ABAP 類別,再在 ADT/SE80 建立 Job Catalog Entry 與 Job Template 指向它。之後這支程式就會出現在 Fiori 的可排程清單裡。
  • SJOBREPO(Technical Job Repository):SAP 標準的技術背景作業(例如 SAP_APPLICATION_LOG_DELETE、各種 housekeeping)改由 Job Repository 自動註冊與啟用,不再需要人工一個一個排。SJOBREPO 可以看到哪些標準 Job 已啟用。
  • SAP Job Scheduling Service / BTP:跨系統的排程,屬於雲端方案。
什麼時候該做成 Application Job?當「使用者自己想決定什麼時候跑、跑哪些條件」時。例如月結報表、對帳檔產出。純技術性的介面(每 10 分鐘抓 FTP)用傳統 SM36 即可,不必包成 Application Job。
ABAP Cloud(RAP / Steampunk)不允許 BDC。CALL TRANSACTION USINGBDC_* 函數、GUI_*OPEN DATASET 全部不在允許的 API 清單裡。如果你的開發要往 Clean Core 走(S/4HANA Cloud、Embedded Steampunk),這一章的技巧只能用在傳統 ABAP 開發模型(Classic ABAP)的自建程式上。

17. 現代化重寫:ZPPI0002 的 S/4HANA 版骨架

把前面所有原則套回講義那支程式,S/4HANA 版本應該長這樣。重點是:資料來源可換、驗證獨立、過帳優先 BAPI、全程留痕、可背景可前景。

REPORT zppi0002 MESSAGE-ID zpp01.

*&---------------------------------------------------------------------*
*& 資料來源抽象化:不管 FTP 還是 PC,都回傳同一種 gt_xls
*&---------------------------------------------------------------------*
INTERFACE lif_source.
  METHODS fetch RETURNING VALUE(rt_data) TYPE ty_t_xls
                RAISING   cx_static_check.
ENDINTERFACE.

CLASS lcl_source_ftp DEFINITION. PUBLIC SECTION. INTERFACES lif_source. ENDCLASS.
CLASS lcl_source_pc  DEFINITION. PUBLIC SECTION. INTERFACES lif_source. ENDCLASS.

*&---------------------------------------------------------------------*
START-OF-SELECTION.

  " 前置檢查
  IF p_email = abap_true AND p_group IS INITIAL.
    MESSAGE e000 WITH TEXT-e01.        " 「勾選寄信時必須指定收件群組」
  ENDIF.

  " 1) 取得來源(多型,之後要加 SFTP / OData 只要多一個 class)
  DATA(lo_src) = COND REF TO lif_source(
                   WHEN p_upftp = abap_true THEN NEW lcl_source_ftp( )
                   ELSE                          NEW lcl_source_pc( ) ).

  TRY.
      gt_xls = lo_src->fetch( ).
    CATCH cx_static_check INTO DATA(lx).
      PERFORM send_alert_mail USING lx->get_text( ).
      MESSAGE lx->get_text( ) TYPE 'E'.
  ENDTRY.

  IF gt_xls IS INITIAL.
    MESSAGE s000 WITH '無資料可處理'.
    RETURN.
  ENDIF.

  " 2) 正規化 + 驗證(一次把主檔全抓回來,不要在迴圈裡 SELECT)
  PERFORM normalize_and_validate.

  " 3) 過帳(只跑通過驗證的)
  PERFORM post_confirmations.

  " 4) 留痕
  PERFORM modify_log_table.

  " 5) 呈現
  PERFORM display_alv_rtn.

  " 6) 通知
  IF p_email = abap_true.
    PERFORM get_spool_pdf CHANGING DATA(lv_pdf).
    PERFORM get_mail_group USING p_group CHANGING DATA(lt_recv).
    PERFORM send_mail_bcs  USING lt_recv lv_pdf VALUE #( ).
  ENDIF.

*&---------------------------------------------------------------------*
FORM normalize_and_validate.

  " 先把工單號碼正規化(去引號、去空白、ALPHA 補零)
  LOOP AT gt_xls ASSIGNING FIELD-SYMBOL(<ls>).
    REPLACE ALL OCCURRENCES OF '"' IN <ls>-aufnr WITH ``.
    CONDENSE <ls>-aufnr.
    <ls>-aufnr = |{ <ls>-aufnr ALPHA = IN }|.       " 取代 CONVERSION_EXIT_ALPHA_INPUT
    REPLACE ALL OCCURRENCES OF '"' IN <ls>-vornr WITH ``.
    CONDENSE <ls>-vornr.
  ENDLOOP.

  " 一次撈全部工單,不要在 LOOP 裡 SELECT SINGLE(講義的舊寫法效能很差)
  IF gt_xls IS NOT INITIAL.
    SELECT aufnr, auart, werks, autyp
      FROM aufk
      FOR ALL ENTRIES IN @gt_xls
      WHERE aufnr = @gt_xls-aufnr
      INTO TABLE @DATA(lt_aufk).
  ENDIF.

  SORT lt_aufk BY aufnr.

  LOOP AT gt_xls ASSIGNING <ls>.

    IF NOT line_exists( lt_aufk[ aufnr = <ls>-aufnr ] ).
      <ls>-status = 'E'.
      <ls>-msg    = |工單 { <ls>-aufnr } 不存在|.
      CONTINUE.
    ENDIF.

    IF <ls>-vornr IS INITIAL.
      <ls>-status = 'E'.
      <ls>-msg    = '作業號碼為空'.
      CONTINUE.
    ENDIF.

    " 過帳日期檢查
    IF <ls>-budat CN '0123456789' OR strlen( <ls>-budat ) <> 8.
      <ls>-status = 'E'.
      <ls>-msg    = |過帳日期格式錯誤:{ <ls>-budat }|.
      CONTINUE.
    ENDIF.

  ENDLOOP.

ENDFORM.

*&---------------------------------------------------------------------*
FORM display_alv_rtn.

  TRY.
      cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                              CHANGING  t_table      = gt_xls ).

      lo_alv->get_functions( )->set_all( abap_true ).
      lo_alv->get_columns( )->set_optimize( abap_true ).
      lo_alv->get_display_settings( )->set_striped_pattern( abap_true ).
      lo_alv->get_display_settings( )->set_list_header( 'MES 工時回報上傳結果' ).

      " 中文欄位抬頭(取代講義那一長串 CHANGE_FIELD_NAME)
      DATA(lo_cols) = lo_alv->get_columns( ).
      DATA(lt_head) = VALUE ty_t_head(
        ( fname = 'AUFNR'  text = '工單號碼' )   ( fname = 'VORNR'  text = '作業' )
        ( fname = 'LMNGA'  text = '產出量' )     ( fname = 'MEINH'  text = '產出量單位' )
        ( fname = 'XMNGA'  text = '壞品數量' )   ( fname = 'GRUND'  text = '無效工時原因代碼' )
        ( fname = 'ISM01'  text = '工時1' )      ( fname = 'ILE01'  text = '工時1單位' )
        ( fname = 'ISM02'  text = '工時2' )      ( fname = 'ILE02'  text = '工時2單位' )
        ( fname = 'ISM03'  text = '工時3' )      ( fname = 'ILE03'  text = '工時3單位' )
        ( fname = 'ISM04'  text = '工時4' )      ( fname = 'ILE04'  text = '工時4單位' )
        ( fname = 'BUDAT'  text = '過帳日期' )   ( fname = 'STATUS' text = '狀況' )
        ( fname = 'MSG'    text = '訊息' ) ).

      LOOP AT lt_head INTO DATA(ls_head).
        DATA(lo_col) = lo_cols->get_column( CONV #( ls_head-fname ) ).
        lo_col->set_long_text(   CONV #( ls_head-text ) ).
        lo_col->set_medium_text( CONV #( ls_head-text ) ).
        lo_col->set_short_text(  CONV #( ls_head-text ) ).
      ENDLOOP.

      lo_alv->display( ).          " 背景執行時自動輸出到 Spool

    CATCH cx_salv_msg cx_salv_not_found INTO DATA(lx_salv).
      MESSAGE lx_salv->get_text( ) TYPE 'E'.
  ENDTRY.

ENDFORM.
講義原版的三個效能問題,改寫時一定要修掉:
  • 迴圈裡 SELECT SINGLE * FROM aufk:一萬筆資料就是一萬次資料庫往返。改成 FOR ALL ENTRIES 一次撈完,或用 SELECT ... FOR ALL ENTRIES + line_exists( )
  • SELECT SINGLE *:只是要判斷存不存在,卻抓整列。明列欄位,或改用 SELECT SINGLE @abap_true FROM aufk WHERE ... INTO @DATA(lv_exists)
  • 迴圈裡 DELETE FROM zppworkma0001 逐筆刪 + MODIFY 逐筆寫:改成整批 DELETE ... FOR ALL ENTRIESMODIFY ... FROM TABLE

18. 快速查表:本章重點速記

要做什麼用什麼注意
錄畫面操作SHDBFiori 畫面錄不了
同步跑交易CALL TRANSACTION ... USING ... MODE ... UPDATE ... MESSAGES INTO背景 MODE 用 'N';defsize='X'
非同步批次BDC_OPEN_GROUP / BDC_INSERT / BDC_CLOSE_GROUPSM35背景消化用 RSBDCSUB
訊息轉文字MESSAGE ... INTOFORMAT_MESSAGEMESSAGE_TEXT_BUILD優先用語法內建的 INTO
PC 檔案CL_GUI_FRONTEND_SERVICES背景不能用
伺服器檔案OPEN DATASET / TRANSFER / READ / CLOSEAL11路徑用 FILE 邏輯檔名
讀 XLSXCL_FDT_XL_SPREADSHEET純 ABAP,背景可用
編碼轉換CL_ABAP_CONV_CODEPAGECL_ABAP_CHAR_UTILITIESUTF-8 CSV 記得 BOM
FTPFTP_CONNECT / FTP_COMMAND / FTP_SERVER_TO_R3 / FTP_R3_TO_SERVER / FTP_DISCONNECT背景用 SAPFTPA;密碼要 HTTP_SCRAMBLE
寄信CL_BCS(新)/SO_DOCUMENT_SEND_API1(舊)一定要 COMMIT WORKSCOT 設定、SOST 查詢
Spool 轉 PDFCONVERT_ABAPSPOOLJOB_2_PDF + SX_TABLE_LINE_WIDTH_CHANGEsy-spono 前景可能是空的
背景作業SM36SM37JOB_OPEN / SUBMIT VIA JOB / JOB_CLOSEAND RETURN 不能忘;要有變式
使用者自助排程Fiori「Application Jobs」+ Job Catalog Entry需實作 IF_APJ_DPC_RUN / IF_APJ_RT_RUN

練習與自我檢核

  1. SHDB 錄一支 MM02。錄「修改物料的基本資料視圖描述」,把產生的程式改成從 CSV 讀 MATNR + MAKTX 批次修改。提示:注意 MM02 有「選擇視圖」的彈出畫面(SAPLMGMM0060),要用 MSICHTAUSW-KZSEL(01) 勾選視圖。
  2. MODE 三選一。同一支 BDC 程式,前景執行時要讓使用者看到錯誤畫面、背景執行時完全不顯示,程式該怎麼寫?答案要點:DATA(lv_mode) = COND #( WHEN sy-batch = abap_true THEN 'N' ELSE 'E' ).
  3. UPDATE 'A' 為什麼危險?請說明「程式回報成功但資料庫沒有資料」的可能原因,以及該去哪個交易碼查(提示:SM13)。
  4. 日期陷阱重現。把使用者 SU3 的日期格式從 YYYY.MM.DD 改成 MM/DD/YYYY,再跑同一支 BDC,觀察會發生什麼。寫出修正的程式碼(提示:WRITE lv_d TO lv_cCONVERT_DATE_TO_EXTERNAL)。
  5. Table Control 捲動。寫一段 BDC,在 VA01 的項目 Table Control 輸入 12 個項目,每頁只能顯示 5 列。畫出 OKCODE 的順序表。
  6. FOR ALL ENTRIES 空表陷阱。把第 17 節的 DELETE ... FOR ALL ENTRIESgt_xls 為空時執行,會發生什麼?請寫出正確的防呆。
  7. CSV 中文亂碼。使用者說「你們產的 CSV 用 Excel 開中文都是亂碼」。列出三個可能原因與對應解法(提示:codepage、BOM、Excel 的地區設定)。
  8. 背景不能用 GUI_UPLOAD。把第 10 節的 upload_csv 改寫成「前景讀 PC 檔、背景讀 AL11 目錄」的雙軌版本。
  9. FTP 改 SFTP。公司資安要求禁用 FTP。請提出至少兩種替代方案,並說明各自需要 Basis 配合什麼(提示:SM69 外部指令、SAP Integration Suite)。
  10. CL_BCS 寄不出去。程式跑完 send( ) 回傳 abap_true,但收件人沒收到信。列出四個檢查點(提示:COMMIT、SOSTSCOTRSCONN01)。
  11. 把 ALV 結果寄出去。寫一支程式:背景執行 → CL_SALV_TABLE 輸出到 Spool → 取 sy-spono 轉 PDF → 用 CL_BCS 寄給 SBWP 通訊群組。這題就是把第 13、14 節合起來。
  12. 排成每小時的 Job。用 SM36 排定第 17 節那支程式每小時跑一次,並說明為什麼一定要建變式、Job Class 該選什麼、以及如何確保「同一個檔不會被處理兩次」。
  13. BDC 還是 BAPI?下列情境各該選哪一個,並說明理由:(a) 期初匯入 5 萬筆客戶主檔;(b) 每天 200 筆生產訂單報工;(c) 客戶自建的 Z 交易,沒有任何 API;(d) 要在 S/4HANA Cloud 上做。
  14. 紀錄表設計。替第 17 節的程式設計一張執行紀錄表,欄位至少要能回答:這筆是哪個檔案哪一行來的、什麼時候處理的、誰執行的、成功還失敗、失敗原因、原始內容。

附錄 A:舊語法 → S/4HANA 現代寫法速查

看舊程式時最常遇到的寫法,以及在 S/4HANA 1809 之後應該改成什麼。左欄不要再寫,右欄照抄即可。

A-1 宣告與指派

舊寫法(不要再寫)現代寫法說明
DATA lv_x TYPE i. lv_x = 1.DATA(lv_x) = 1.內嵌宣告,7.40 起
MOVE a TO b.b = a.MOVE 已過時
ADD 1 TO n.n += 1.複合指派 7.54 起;1809 可寫 n = n + 1
CLEAR wa. wa-a = 1. wa-b = 2.DATA(wa) = VALUE ty_s( a = 1 b = 2 ).VALUE 建構式
MOVE-CORRESPONDING s TO t.t = CORRESPONDING #( s ).可加 MAPPING / EXCEPT / BASE
CREATE OBJECT lo_x.DATA(lo_x) = NEW zcl_x( ).NEW 運算子
CONCATENATE a b INTO c.c = |{ a }{ b }|.字串範本,可直接格式化
IF a = 1. r = 'X'. ELSE. r = ''. ENDIF.r = COND #( WHEN a = 1 THEN 'X' ELSE space ).COND / SWITCH

A-2 內表

舊寫法現代寫法說明
DATA it TYPE TABLE OF ty OCCURS 0 WITH HEADER LINE.DATA it TYPE STANDARD TABLE OF ty WITH EMPTY KEY.表頭列在 OO 與 ABAP Cloud 禁用
LOOP AT it INTO wa.LOOP AT it INTO DATA(wa).ASSIGNING FIELD-SYMBOL(<wa>)大量資料用 field-symbol 免搬移
READ TABLE it INTO wa WITH KEY k = v.
IF sy-subrc = 0.
IF line_exists( it[ k = v ] ).
DATA(wa) = it[ k = v ].
表格運算式;找不到會丟 CX_SY_ITAB_LINE_NOT_FOUND
迴圈逐筆 APPENDDATA(it2) = VALUE ty_t( FOR ls IN it WHERE ( x = 1 ) ( CORRESPONDING #( ls ) ) ).FOR 迭代
迴圈累加DATA(total) = REDUCE d( INIT s = 0 FOR ls IN it NEXT s = s + ls-amt ).REDUCE
排序後 AT NEW 手動分組LOOP AT it INTO DATA(ls) GROUP BY ( k = ls-k ) INTO DATA(grp).內表 GROUP BY,7.40 起
DELETE ADJACENT DUPLICATES FROM it.SORT it BY 欄位DELETE ADJACENT DUPLICATES FROM it COMPARING 欄位.沒排序等於沒去重
it[ ... ] 找不到資料會直接丟例外中斷程式,務必搭配 line_exists( ) 或包在 TRY ... CATCH cx_sy_itab_line_not_found

A-3 ABAP SQL

舊寫法現代寫法說明
SELECT * FROM mara INTO TABLE it WHERE matnr = lv_matnr.SELECT matnr, mtart, meins FROM mara WHERE matnr = @lv_matnr INTO TABLE @DATA(lt_mara).明列欄位 + @ 逸出;欄位間要逗號
SELECT ... ENDSELECT.一次 INTO TABLE,或 SELECT ... INTO ... UP TO n ROWS逐筆迴圈會拖垮效能
SELECT SINGLE * ...SELECT SINGLE 欄位 ... WHERE 完整主鍵主鍵不完整時 HANA 不保證取到同一筆
迴圈內 SELECT SINGLE先整批撈進內表,再用 it[ key = ... ] 或 JOINN+1 查詢是最常見的效能地雷
撈回內表後 LOOP 累加SELECT ... SUM( x ) ... GROUP BY ...Code pushdown:算給資料庫做
FOR ALL ENTRIES IN itIF it IS NOT INITIAL,或改用 JOIN / CDS / WITH CTE驅動表為空會撈全表;重複值會被自動去重
EXEC SQL ... ENDEXEC.cl_sql_statement(ADBC)或 AMDPEXEC SQL 已過時

A-4 報表輸出

舊寫法現代寫法說明
WRITE / ULINE / SKIP 清單CL_SALV_TABLEWRITE 只留給極簡的除錯輸出
REUSE_ALV_GRID_DISPLAYCL_SALV_TABLE=>factory( )不需編輯時最省事
REUSE_ALV_GRID_DISPLAY_LVCCL_GUI_ALV_GRID + Custom Container需要可編輯、事件、細控時
手刻 fieldcat 每欄lo_alv->get_columns( )->get_column( 'X' )->set_short_text( )或用 DDIC 結構自動帶出

A-5 其他常見過時語法

過時改成
TABLES: mara.(宣告表工作區)DATA ls_mara TYPE mara.TABLES 只在 dialog 程式與選擇畫面必要時使用
PERFORM ... USING ...(FORM)本地類別的方法或全域類別(ABAP Cloud 禁用 FORM)
DEFINE ... END-OF-DEFINITION(Macro)方法;Macro 無法除錯
classic exception(EXCEPTIONS + sy-subrcTRY ... CATCH cx_...
CALL FUNCTION 'POPUP_TO_CONFIRM' 之外的舊 popup FM仍可用,但新程式建議包成 Z 類別統一管理
DESCRIBE TABLE it LINES n.DATA(n) = lines( it ).
DESCRIBE FIELD ... TYPE ...RTTS:cl_abap_typedescr=>describe_by_data( )
直接 UPDATE / INSERT 標準表一律走 BAPI 或標準 API;直接改標準表會破壞一致性且不受支援

A-6 版本門檻速查

語法最低版本
內嵌宣告 DATA( )@DATA( )VALUEFORREDUCECONDSWITCHCORRESPONDINGNEW、字串範本、表格運算式、內表 GROUP BY7.40 SP08
ABAP SQL 的 UNION、SQL 運算式增強、CDS DEFINE VIEW(含 sqlViewName)7.50 / 7.51
WITH(CTE)、視窗函數 OVER( )SELECT ... , @DATA(...) 混合7.51 / 7.52(S/4HANA 1809 屬 7.52 或更高)
複合指派 += -=VALUE ... BASE 延伸、Virtual sorting7.54
DEFINE VIEW ENTITY(免 sqlViewName 的 CDS)、RAP Managed Scenario7.55 / S/4HANA 2020
PCRE 正規表示式(PCRE 附加)7.55
不確定手上的系統支援到哪,執行 SE38RSABAPVERSION,或直接看 系統 → 狀態 → 元件資訊 中的 SAP_BASIS 版本:7.52 = S/4HANA 1809/1909,7.54 = 2020,7.55 = 2021,7.56 = 2022,7.57 = 2023。