總覽:如何使用這座圖書館
這是把 Dropbox\09_SAP_AB_ABAP編程 多年累積的教材,整理成可以隨時查閱的 ABAP 線上圖書館。所有程式碼都已改寫為 SAP S/4HANA 1809 以後可直接使用的現代 ABAP 寫法(內嵌宣告、@ 逸出主機變數、建構式運算、CL_SALV_TABLE、class-based exception)。
1. 四部曲學習路線
| 部 | 主題 | 學完可以做什麼 | 章節 |
|---|---|---|---|
| 第一部 | 資料字典:讓模組顧問自由自在的飛 | 看得懂 SAP 資料結構、會建 Z 表與維護畫面、不寫程式也能出報表 | 1-1 ~ 1-4 |
| 第二部 | 內表與 SQL:模組顧問進化成 ABAPER | 寫得出可上線的 ALV 報表,會用 7.40+ 新語法與效能寫法 | 2-1 ~ 2-4 |
| 第三部 | 視窗報表與增強:顧問會程式等於空間無限 | 設計選擇畫面、OOALV、看得懂並寫得出 User Exit / BAdI | 3-1 ~ 3-4 |
| 第四部 | 進階技巧:目標成為專職的 ABAPER | 介面(RFC/BAPI)、對話式程式、表單、批次匯入與排程 | 4-1 ~ 4-4 |
2. 這份文件的體例
- SE38 這樣的標記代表交易碼,可直接在 SAP 命令列輸入。
- 藍底方塊是補充說明、綠底方塊是實務技巧或老師口訣、紅底方塊是 S/4HANA 注意事項或常見錯誤,紅色的請務必看。
- (依講義步驟整理) 代表原講義該處只有截圖,內文是依 SAP 標準流程補寫成可操作的文字步驟。
- 每章最後都有「練習與自我檢核」,題目沿用原教材編號,方便對照舊講義。
3. 開發前的三個提醒
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 / SOAMANAGER | RFC 目的地、tRFC/qRFC 佇列、BAPI 瀏覽、Web Service |
| 批次 | SHDB / SM35 / SM36 / SM37 / SP01 | 錄製、批次輸入、排程作業、Spool |
| 表單 | SMARTFORMS / SMARTSTYLES / SE71 / SFP / SE78 / NACE | SmartForms、樣式、SAPscript、Adobe Form、圖檔、輸出決定 |
| 傳輸 | SE09 / SE10 / SE01 / STMS / SCC1 | 傳輸請求與傳輸管理 |
5. 最常用的資料表(S/4HANA 版)
| 模組 | 資料表 | S/4HANA 變動 |
|---|---|---|
| MM 物料 | MARA / MARC / MARD / MBEW / MAKT / MCHB | MARD、MBEW 等庫存與評價表在 S/4 為相容視圖(NSDM),實體資料在 MATDOC 聚合 |
| MM 採購 | EKKO / EKPO / EKET / EKBE / EBAN | 大致不變 |
| MM 異動 | MATDOC | 取代 MKPF / MSEG,後兩者保留為相容視圖 |
| SD | VBAK / VBAP / LIKP / LIPS / VBRK / VBRP / PRCD_ELEMENTS | VBUK / VBUP 已併入 VBAK / VBAP;KONV 改名 PRCD_ELEMENTS |
| FI / CO | ACDOCA / BKPF / BSEG / SKA1 / SKB1 | ACDOCA 為總帳與明細的單一真相來源,BSIS/BSAS/COEP/GLT0 等已成視圖 |
| PP | AUFK / AFKO / AFPO / AFVC / RESB / STPO / MAST | 大致不變 |
| 主檔 | KNA1 / LFA1 / BUT000 / BUT020 | 客戶與供應商統一走 Business Partner(BP),XD01 / XK01 停用 |
| 組織 | T001 / T001W / T001L / T001K / TCURR | 大致不變 |
6. 這份圖書館還可以怎麼長大
同目錄下的 CLAUDE.md 與 SKILL.md 已定義好擴充規則。之後想加東西時,只要把新資料放進 Dropbox 對應資料夾,然後告訴 Claude「照 SKILL.md 把 XXX 加進 ABAP 圖書館」,就會依同樣的體例、同樣的 S/4HANA 語法基準續寫,章節編號與側欄目錄會自動接上。可以優先補的方向:
- 09_ABAP二次開發 的 SD / MM / PP / FI / CO / QM 實務案例(ZMMI0001、ZPPB0002 缺料表、DBCO 連 SQL Server 等)。
- 15_累計心得 的常用日期函數、HR 表、ACDOCA 筆記、透過欄位查表名。
- L36 的 136 個 ABAP OOP 範例、L31 的 36 套源碼。
- 原廠 BC 教材(BC400 / BC401 / BC402 / BC405 / BC410 / BC414 / BC425 / BC430 / BC470 / BC490)的重點摘要與對照。
- 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 該怎麼改。
1. 認識十個核心 TCODE(先會查,才會寫)
老師的第一堂課不是教語法,而是要你先「看得懂 SAP 在做什麼」。寫 ABAP 的人如果不會用標準交易查資料,寫出來的程式一定跟業務對不上。下表是講義列出的十個 TCODE,建議背下來當日常工具。
| TCODE | 用途 | 備註/對應資料表 |
|---|---|---|
| SE11 | ABAP Dictionary 資料字典 | 看 Table/View/Data Element/Domain 的定義與外鍵 |
| SE38 | ABAP 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 |
參考答案方向: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 標示為「不再增強」。
3. 寫程式的四步驟與命名規則(Z/Y 命名空間)
講義把建立第一支程式拆成四個動作,這四步在 S/4HANA 依然完全一樣:
- 程式名稱一定要 Z 開頭(或 Y 開頭)。
- 設定屬性:Title、Type、Status、Application。
- 指定 Package:練習環境可選 Local Object($TMP);正式開發要放進自己的 Package(例如講義用的 Z600_2409_ERP000)。
- 執行程式:F8,或用 SA38/SE93 建成 TCODE 執行。
命名空間規則:SAP 保留了所有不以 Z/Y 開頭的名稱給標準物件。客戶自建物件只能用 Z* 或 Y*;若公司有向 SAP 申請的專屬命名空間,則是 /ABCDE/ 這種斜線包起來的形式。表格、資料元素、Domain、Class、Function Module 全部適用同一規則。
| Program Status | 意義 | 使用時機 |
|---|---|---|
| P | SAP Standard Production Program(標準生產程式) | SAP 原廠用,客戶不選 |
| K | Customer Production Program(客戶生產程式) | 預設選這一項,正式上線的客製程式 |
| S | System Program(系統程式) | 系統層級工具 |
| T | Test Program(測試程式) | 臨時測試、不打算上線的 |
Z + 模組 + 用途 + 序號,例如 ZSD_ORDER_LIST01、ZFI_ACDOCA_RPT。看名字就知道歸誰維護。講義的課堂命名是 Z2202_00_01 這種「課程代號_學號_序號」,方便老師點名批改。4. 用 SE16N/SE16H 學寫 SQL
老師的心法:不會寫 SQL 就先去 SE16N 點一遍。SE16N 的選擇畫面本質上就是一句 SELECT,把條件填一填、按執行,你要的 WHERE 條件就成形了。
- SE16N 進入後在指令列輸入
&SAP_EDIT可開啟編輯模式(正式機嚴禁使用,會繞過所有業務邏輯與變更文件)。 - SE16N 支援「技術欄位名稱/描述」切換、可指定輸出欄位、可設定筆數上限。
- SE16H(HANA 版)更強:可直接做 Group By、Sum、Count、以及多表 JOIN,等於在畫面上做出一份聚合報表,很適合先驗證邏輯再寫成 ABAP。
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,效能最差
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.
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.
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.
TYPE c LENGTH 40 → 沒有 Search Help;TYPE matnr(Data Element)→ 有 Search Help(Data Element 掛了 Search Help 或 Domain 有 Value Table);TYPE mara-matnr(表-欄位)→ 有 Search Help,且欄位說明、外鍵檢查一起繼承。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' ) ).
金額欄位一律用 CURR(DDIC)/P DECIMALS 2(ABAP),絕不要用 F 浮點數算錢。
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 ) } 筆|.
要刪的是
TABLES: VBAP.(用不到了)、TYPES: TY_MATNR(40) TYPE C. 與 DATA: W_MATNR(沒被用到);要改的是把 PARAMETERS p_matnr TYPE ty_matnr 改成 TYPE mara-matnr,Search Help 就出來了。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.
WRITE: a, b, c. 等同三句 WRITE。JOIN 時三個關聯鍵(BUKRS、BELNR、GJAHR)少接一個,就會變成笛卡兒乘積,筆數暴增且金額全錯——這是 FI 報表最常見的災難。・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).
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 消費。
答案要點: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 |
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 }|.
提示欄位: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.
WRITE 的常用附加:
NO-ZERO(去前導零)、NO-GAP(不留空白)、LEFT-JUSTIFIED / RIGHT-JUSTIFIED、UNDER f(對齊某欄)、DD/MM/YYYY(日期格式)、COLOR n、DECIMALS 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.
AT LINE-SELECTION 被觸發時才依行號取回。除錯查資料時,比開 ALV 還快。・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.
1. 右表的過濾條件寫在 WHERE 就會退化成 INNER JOIN(因為 NULL 不滿足條件而被濾掉),必須寫在
ON 子句裡。2. 若右表對同一鍵有多筆(例如 MAKT 沒限定 SPRAS),LEFT JOIN 會把左表的列數放大,金額類欄位再做 SUM 就會重複計算。這是報表金額對不起來最常見的原因。
語系 T002:SAP 用單碼語言鍵(SPRAS)。講義列出的常用對照:
| 語言 | SPRAS(單碼) | ISO(雙碼) |
|---|---|---|
| 簡體中文 | 1 | ZH |
| 繁體中文 | M | ZF |
| 英文 | E | EN |
| 德文 | D | DE |
" 語系轉換:單碼 <-> 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 模組最容易搞混的地方。
| 欄位 | 在訂單 | 在交貨單 |
|---|---|---|
| VBELN | VBAK-VBELN/VBAP-VBELN=訂單編號 | LIKP-VBELN/LIPS-VBELN=DN 交貨單編號 |
| POSNR | VBAP-POSNR=訂單項次 | LIPS-POSNR=DN 項次 |
| 串接關係 | LIPS-VGBEL = VBAP-VBELN(來源訂單號) LIPS-VGPOS = VBAP-POSNR(來源訂單項次) | |
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 表固定有四個欄位:
| 欄位 | 型態 | 意義 |
|---|---|---|
| SIGN | CHAR 1 | I=Include 包含(一般用這個);E=Exclude 排除 |
| OPTION | CHAR 2 | EQ 等於、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 區間。
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)(依講義步驟整理):
- 執行 SE21(Package Builder),或 SE80 → 下拉選 Package。
- 輸入 Package 名稱(必須 Z/Y 開頭,例如講義的 Z600_2409_ERP000),按 Create。
- 填入 Short Description、Software Component(客戶物件選
HOME)、Transport Layer(選系統的標準傳輸層,例如ZDEV或SAP;此欄決定物件會被傳到哪條傳輸路徑)。 - Package Type 一般選 Not a Main Package(不是主套件)。
- 存檔 → 系統要求指定 Workbench Request → 建立或選一個既有的請求號 → 完成。
SAP 或留空),物件可能無法沿著客戶的 DEV→QAS→PRD 路徑傳輸,或被歸為「不可傳輸」。建 Package 前先問 Basis 這套系統的客戶傳輸層代碼。SE80 物件瀏覽器:同一個 Package 底下可組織多個物件——程式、Function Group、Class、Table、Screen、Transaction、Message Class 全都掛在樹上。開發時把 SE80 開著,比反覆切 SE38/SE11/SE37 快得多。
程式類型(Program Type,可由 TRDIR 資料表查詢):
| Type | 名稱 | 說明 |
|---|---|---|
| 1 | Executable Program 可執行程式 | 可由 ABAP Editor 直接執行;processing blocks 依預先定義的順序執行;可使用標準 selection screen;主要用來顯示 List。報表都是這一型。 |
| M | Module Pool 模組池 | 必須至少定義一個 TCODE 指定 initial screen;畫面順序可由 screen attributes 靜態指定,或由程式碼動態指定。VA01/VA02/VA03 三個 TCODE 共用同一個 Module Pool 與畫面。 |
| F | Function Group 函數組 | 包含 function modules、local data declarations 與 screens。 |
| I | Include Program | 可包含任何 ABAP 敘述;不能單獨執行,是模組化的容器。 |
| J | Interface Pool 介面池 | 包含 global interfaces 與 local data declarations。 |
| K | Class Pool 類別池 | 包含 global classes 與 local data declarations。 |
| S | Subroutine Pool | 只放 FORM 的容器(已不建議新開發使用)。 |
查詢方式:
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.
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 | 用途 |
|---|---|
| SE09 | Transport Organizer(Workbench 請求,講義用這支查請求號) |
| SE10 | Transport Organizer(含 Customizing 請求,功能與 SE09 幾乎相同) |
| SE01 | Transport Organizer(擴充視圖,可看傳輸日誌、建立各類請求) |
| SE03 | Transport Organizer Tools(改物件的 Package、搜尋請求中的物件、鎖定/解鎖) |
| STMS | Transport Management System,匯入佇列、執行傳輸 |
請求狀態(講義原文):Modifiable=可修改(還在開發中,物件被鎖住);Released=已釋放(已寫出檔案,可以被匯入下一個系統,內容不能再改)。
釋放與傳輸的完整流程(依講義步驟整理):
- 開發完成 → SE09 找到自己的 Request。
- 先釋放 Task(子項):選中 Task → 按卡車圖示(Release Directly)。
- 再釋放 Request(主項):同樣按卡車圖示。此時系統會做語法/一致性檢查,並把物件寫成傳輸檔案(cofiles + data files)。
- STMS → Import Overview → 進入 QAS 的 Import Queue → 找到剛才的請求號 → Import Request。
- 在 QAS 測試通過後,再從 STMS 的 PRD 佇列匯入。
STMS 的觀念(講義 10-5):標準三層架構是 DEV → QAS → PRD,「有時不只這三區」。常見的變化包括:
| 系統 | 全名 | 角色 |
|---|---|---|
| DEV | Development | 唯一可以改程式的地方(其他系統通常鎖住) |
| QAS / QA / TST | Quality Assurance | 使用者測試 UAT、整合測試 |
| PRD / PRO | Production | 正式生產環境 |
| SBX | Sandbox | 沙箱,通常不在傳輸路徑上 |
| TRN | Training | 教育訓練機 |
| PRE / PPD | Pre-Production | 上線前最後演練(含真實資料副本) |
1. 傳輸順序不能亂。請求號是依釋放時間編號的,匯入時若跳號(先匯 900130 再匯 900125),舊的請求會覆蓋新的,造成「明明修好了卻又壞掉」。務必依序匯入或用 Import All。
2. 相依物件要同一批傳。Table 改了欄位、程式用了新欄位,兩者若分在不同 Request 且分批傳,QAS 會出現「欄位不存在」的短暫錯誤。
3. $TMP 的東西傳不出去。上線前用 SE03 → Change Object Directory Entry 把 Package 從 $TMP 改成正式 Package,系統會要求指定 Request。
" 查某個物件被登記在哪些傳輸請求裡(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,注意會有資料轉換風險)。
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 頁籤設定),否則無法激活。
VBAK-MATNR 會自動有 F4 與輸入檢查。全部做完後回頭看 SE11 裡的標準表 MARA,就能理解 STRUCTURE(結構)與 Append Structure 的意義。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 Printer | Shift + F1 | 語法檢查 | Ctrl + F2 |
| 激活 | Ctrl + F3 | 執行 | F8 |
| Where-Used List | Ctrl + Shift + F3 | 設中斷點 | Ctrl + Shift + F12 |
其他必知工具:
- 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 可能還是昨天。使用者的時區在 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 的四種方法:
- 設中斷點:在 SE38 編輯器中,游標停在該行 → 點左側「停止」圖示,或按 Ctrl+Shift+F12。執行到該行就會停。
- 指令列輸入
/h:在任何畫面的指令列輸入/h按 Enter,畫面顯示 "Debugging switched on",接著按下一個動作就會進入偵錯。這招對標準程式最好用。 - 程式中寫死:加一行
BREAK-POINT.(所有人都會停,絕不能傳到正式機)或BREAK <使用者名稱>.(只有指定使用者會停)。 - SE80/SAT:對背景 Job 用 SM37 → 選 Job → 指令列
JDBG進入偵錯。
Debugger 的操作鍵:
| 鍵 | 動作 | 用途 |
|---|---|---|
| F5 | Single Step(單步進入) | 會鑽進 FORM/FUNCTION/METHOD 內部 |
| F6 | Execute(單步跳過) | 把整個子程式當一行執行完 |
| F7 | Return(跳出) | 執行完目前子程式後回到呼叫端 |
| F8 | Continue(繼續) | 跑到下一個中斷點或結束 |
常用技巧:
- 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.
19. 解決企業問題的手段與順序
講義最後一節只有三句話,但它是整套課程的價值觀,也是資深顧問與新手最大的差別:
- 能配置的就用後台配置(SPRO)解決。
- 一定要改程式就用增強(Enhancement/BAdI/User Exit)。
- 非不得已才寫程式。
延伸順序(S/4HANA 版):標準功能 → 配置 → Fiori 標準 App → CDS View / Custom Field(Key User Extensibility) → BAdI / Enhancement → 自建 ABAP 程式 → Side-by-side(BTP)。Clean Core 是 S/4HANA 的核心原則:核心越乾淨,升級越輕鬆。
練習與自我檢核
- 作業 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-01:用 SE16H 把上題找到的雙檔做成關聯,並試著加上 Group By 與 Sum。
- 作業 3-01:把 Z2202_00_05 的多餘程式碼刪掉,並讓選擇螢幕的料號能下拉選單。
要點:刪TABLES: VBAP.、未使用的 TYPES 與 DATA;PARAMETERS p_matnr TYPE mara-matnr。 - 作業 4-01:說明 MARA、MARC、MARD、MAKT 的關聯性。
要點:MATNR → +WERKS → +LGORT 的三層結構,MAKT 是 MATNR+SPRAS 的多語系描述。 - 作業 5-01:列出你認為最重要的 MATDOC 二十個欄位,並照第 9 節寫成 JOIN 報表程式。
- 作業 7-01:檢查自己的 SE21,確認 Package 已建立、程式已歸屬、Request 已產生。
- 作業 8-01:
DESCRIBE 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-02:把第 11 節的 ZDEMO 改寫成「由畫面上決定語系」。
要點:加PARAMETERS p_spras TYPE sy-langu DEFAULT sy-langu.,WHERE 改成spras = @p_spras。進階:用SELECT-OPTIONS支援多語系並在 ALV 中分欄顯示。 - 作業 9-01 / 10-01:建立客戶、料號、訂單主檔、訂單明細四張自建表,設定技術設定、產生 Table Maintenance、用 SM30 建資料。
- 自我檢核:不看講義,能否在 15 分鐘內完成——建 Package(SE21)→ 建程式(SE38)→ 寫出「選擇畫面 + SELECT 進內表 + ALV 輸出」→ 激活 → 執行 → 在 SE09 找到自己的 Request?做得到,這一章就過關了。
- 觀念題:為什麼在 S/4HANA 上
SELECT *特別傷?
要點:HANA 是欄式(Column Store)儲存,只讀需要的欄位成本極低;SELECT * 等於強迫掃描並重組所有欄位(ACDOCA 有 500 欄),且無法使用只掃描部分欄位的最佳化。 - 觀念題:為什麼老師說「條件用區間跟用列舉,答案會不同,用列舉的才正確」?
要點: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。
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_ 兩支函數 |
四種 View 的差異,是本章最常被考的觀念:
| View 類型 | 資料來源 | 連接條件 | 能否維護資料 |
|---|---|---|---|
| Database View | 一張或多張表 | 自己定義;一律是 INNER JOIN | 單表視圖可維護(改視圖=改表);多表視圖只能顯示 |
| Projection View | 只能一張表 | 無 | 用來「遮罩欄位」保護資料,可維護 |
| Maintenance View | 多張表 | 不能自訂,必須來自既有外來鍵 | 可維護;需用「表格維護產生器」產生對話框;所有主鍵都要放進視圖欄位 |
| Help View | 多張表 | 必須存在外來鍵(LEFT OUTER JOIN 語意) | 不可維護;專供 Search Help 的 Selection Method 使用 |
2. 用 SE11 建第一張 Z 資料表(含 Delivery Class 與 Maintenance)
講義的目標很明確:本節做完,要能用 SE16N 維護自己的資料。而且提醒「實作之前,請先學會建立 function group」,因為後面 SE54 產生維護畫面時一定會用到。
SE11 建表的標準步驟:(依講義步驟整理)
- SE11 → 選 Database table → 輸入 ZERP300_KNA1 → Create。
- Delivery and Maintenance 頁籤:
- Delivery Class 選 A(Application table,主資料與交易資料)。老師強調:這個畫面一定要選 A,其他的不要選;C 雖然是客製,但那是 SPRO 的客製。
- Data Browser/Table View Maint. 選 Display/Maintenance Allowed,否則 SE16N / SM30 進不去、也產不出維護畫面。
- Fields 頁籤:第一個欄位一律是 MANDT(Data Element MANDT,Domain MANDT,CLNT 3),並勾 Key,代表這是 client-dependent 的表。
- 逐欄輸入欄位名與 Data Element,勾選主鍵(Key)與 Initial Values。
- 幣別/數量欄位要設參考欄位:CURR 型別要指定 Reference table + Reference field(幣別欄位 CUKY),QUAN 型別要指定單位欄位(UNIT)。這是講義中「處理數量 QUAN 與金額 CURR 欄位」的重點。
- Extras → Enhancement Category 設為「Can Be Enhanced (Deep)」,否則激活時會一直跳警告。
- 按 Technical Settings(技術設定)填 Data Class / Size Category(見第 3 節)。
- 存檔 → 指定 Package 與 Request → Activate。老師提醒:要 ACTIVE 才可被 SE16N 使用。
講義示範的客戶資料表 ZERP300_KNA1:
| 欄位 | Data Element | 型別 | 參考 | 說明/對應標準欄位 |
|---|---|---|---|---|
| MANDT | MANDT | CLNT 3 | — | 用戶端(Key) |
| KUNNR | KUNAG | CHAR 10 | — | 客戶編碼(Key);對應 VBAK-KUNNR |
| NAME1 | NAME1_GP | CHAR | — | 客戶名稱;對應 KNA1-NAME1 |
| ZWERT | DZWERT | CURR | VBAP-WAERK | 金額(需指定幣別參考欄位) |
| ZMENG | DZMENG | QUAN | VBAP-ZIEME | 數量(需指定單位參考欄位) |
| ZIEME | DZIEME | UNIT | — | 單位 |
建好、激活好之後,先用最單純的方式把資料撈出來驗證。以下是 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.
3. 技術設定:Data Class、Size Category、Buffering、Logging
SE11 裡按下「Technical Settings」是很多人跳過、卻在上線後出事的地方。
| 設定 | 選項 | 怎麼選 |
|---|---|---|
| Data Class(資料類別) | APPL0 | 主資料(很少變動):如 ZERP300_KNA1、ZERP300_MARA |
| Data Class | APPL1 | 交易資料(頻繁新增變動):如 ZERP300_VBAK、ZERP300_VBAP |
| Data Class | APPL2 | 組織/客製設定資料(幾乎不變) |
| Data Class | USER / 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)
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(值表)。
Data Element(SE11 → Data type → Data element)重點:
- Elementary Type:綁 Domain(建議)或直接給 Predefined Type。
- Field Label:Short/Medium/Long/Heading——這是畫面與 ALV 抬頭的文字來源,一定要填,否則報表抬頭是空白。
- Further Characteristics:Search 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 ).
5. 外來鍵(Foreign Key)與 Text Table
定義:兩個表中有一個相同欄位,它在一個表中是主鍵,那麼這個公有欄位就是另一個表的外鍵。以另一個表的外鍵作關鍵字的表稱為主表(Check Table),具有此外鍵的表稱為從表(Foreign Key Table)。作用是保持資料一致性與完整性:外鍵只能引用主表中存在的值,或使用空值。
SE11 建外來鍵的步驟:(依講義步驟整理)
- Fields 頁籤,游標點在要建外鍵的欄位上(例如 Z2202_00_VBAK-KUNNR)。
- 按工具列的外來鍵圖示(鑰匙)。
- 輸入 Check table(例如 Z2202_00_KNA1);系統會依 Domain 自動建議欄位對應(Check table field ↔ Foreign key field)。
- 設定 Cardinality(基數):主表 1 或 C(C=可為空),從表 1/C/N/CN。客戶 1 → 訂單 N 就是 1:CN。
- 設定 Foreign key field type(外鍵欄位類型):
- Non-key-fields/candidates:外鍵欄位不是主鍵
- Key fields/candidates:是主鍵欄位或能唯一確定記錄
- Key fields of a text table:這張表是主表的文本表
- 可勾 MsgClass/MsgNo 指定檢核失敗時的訊息。
" 上傳/過帳前自行檢核外來鍵:一次撈出所有主表 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?講義列了四種辦法:
- SE11 顯示 MAKT,在主鍵欄位 MATNR 上按「外來鍵關係」,就看得到 Check table = MARA。
- 看 Delivery and Maintenance 頁籤(舊版在 Further characteristics),會標註 Text Table/Master Data Table。
- 依 SAP 命名習慣猜(主表 + T,如 MARA→MAKT、TCURC→TCURT),但光憑名稱不一定準,最終仍要回 SE11 核對。
- 用 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 | 物料資料表 | MANDT、MATNR(MATNR, CHAR 40)、MAKTX(MAKTX, CHAR 40) |
| ZERP300_VBAK | 訂單主檔 | MANDT、VBELN(VBELN_VA)、ERDAT(ERDAT)、KUNNR(KUNAG, CHAR 10)、NETWR(NETWR_AK, CURR 15,2)、WAERK(WAERK, CUKY) |
| ZERP300_VBAP | 訂單明細 | MANDT、VBELN(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 |
7. SE54 表格維護產生器、SM30 與自訂 TCODE
光有表還不夠,使用者要能自己維護資料。這就是表格維護產生器(Table Maintenance Generator)的工作。入口有兩個:SE54,或 SE11 顯示該表 → Utilities → Table Maintenance Dialog(或 Table Maintenance Generator)。
產生步驟:(依講義步驟整理)
- Authorization Group:沒有特別需求填 &NC&。
- Function Group:填一個已存在且已激活的 Function Group(講義示範 ZFG2202_00_KNA1)。老師提醒:若無適合的 Function Group,可選 Z000,記得要按左上的新增鈕(像 A4 紙);也可以用 SE37/SE80 先建一個,Function group 一定要激活。
- Maintenance type:
- one step:只有一張總覽畫面(Overview),適合欄位少的表
- two step:總覽 + 單筆明細畫面,欄位多時用
- Maint. Screen No.:按「Find Scr. Number(s)」讓系統自動配號(例如 0001 / 0002)。
- 按 Create,系統自動產生 Screen、PBO/PAI 模組與更新程式。
- 激活。若激活不完整,要用 SE80 打開該 Function Group 全部激活(老師 250501 特別註記過兩次的坑)。
產生後,使用者用 SM30 輸入表名/視圖名就能維護;SM31 是舊版入口;SM34 用來維護 View Cluster(多層設定表串成一個維護樹)。
SM30 也能變 TCODE(講義 02-4):用 SE93 建立「Transaction with parameters(parameter transaction)」:
- SE93 → 輸入 ZKNA1M → Create → 選 Transaction with parameters (parameter transaction)。
- Transaction 填 SM30,勾 Skip initial screen。
- Default Values 區填:VIEWNAME = ZERP300_KNA1、UPDATE = X(X=維護模式,空白=顯示)。
- 存檔激活,使用者輸入 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。
- SE11 → View → Create → 選 Database view。
- Tables 區輸入兩張表 → 按 Relationships,系統會依既有外來鍵自動帶出 Join 條件(也可自己在 Join Conditions 手打)。
- View Fields 區按 Table fields 逐一挑欄位。
- Selection Conditions 可加固定過濾條件(例如 MANDT = 或某狀態碼)。
- Maintenance Status:多表視圖只能設 read only。
- 激活後,就可以在 ABAP SQL 直接 SELECT ... FROM ZDV2202_00_VBAK。
練習題: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.
特殊結構 SYST(講義 07-2):SY 是系統結構,老師要求至少記 10 個:
| 欄位 | 意義 | 常見用法 |
|---|---|---|
| SY-SUBRC | 上一個指令執行成功與否,0 表示成功 | 每個 SELECT/CALL FUNCTION 之後必查 |
| SY-UNAME | 當前登入的使用者名稱 | 寫入異動人欄位 |
| SY-DATUM | 當前系統日期 | 異動日;注意是應用伺服器日期 |
| SY-UZEIT | 當前系統時間 | 異動時間 |
| SY-TCODE | 當前執行的交易碼 | Enhancement 中判斷來源 |
| SY-INDEX | DO/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 後確認 |
10. Search Help:Elementary、Collective 與 Search Help Exit
搜尋說明(F4 說明)是很有用的功能,可提高使用者友善性;可用於自建表或資料元素。設計時老師要你注意四個點:傳入屬性、顯示屬性、條件屬性、傳回屬性——對應到 SE11 Search Help 畫面的參數表格。
| 欄位 | 意義 |
|---|---|
| Search help parameter | 參數名(通常=Selection Method 的欄位名) |
| IMP | Import:畫面的值傳入當作過濾條件(連動的關鍵) |
| EXP | Export:選定後回填到畫面欄位 |
| LPos | List Position:出現在 F4 清單的第幾欄(顯示屬性) |
| SPos | Selection Position:出現在「限制條件對話框」的第幾列(條件屬性) |
| DPos | Dialog Position:兩段式對話時的位置 |
| Data element | 決定該參數的欄位說明與型別 |
| Modification not allowed | 使用者不可在限制畫面上改這個條件 |
Elementary Search Help(簡單型)建立步驟,以講義 10-1 的 ZSH_MSEHI(數量單位)為例:(依講義步驟整理)
- SE11 → Search help → Create → 選 Elementary search help。
- Selection method:填資料來源,可以是 Table、View 或 Help View。單位用 T006A(含語言)或建一個 Help View 把 T006 + T006A 串起來。
- Dialog type:Display values immediately(直接列值)/Dialog with value restriction(先問條件)/Dialog depends on set of values(值多才問)。
- 參數表格填 MSEHI(IMP/EXP 勾 EXP、LPos 1)、MSEHT(LPos 2)、SPRAS(IMP 勾、預設 SY-LANGU、Modification not allowed 勾)。
- 激活 → 按「測試」鈕(F8)試跑。
講義另一個練習 ZSH_WAERK(讓 ZERP300_VBAK 使用)的四個步驟,重點在多語言:
- 直接用 TCURC 當 Selection Method「似乎不是好主意,因為不能多語言」。
- 換成 TCURC 的 Text Table(標準是 TCURT;講義自建版寫成 ZTURT)。
- 找到「登錄語系的系統變數」= SY-LANGU,設成 SPRAS 參數的預設值。
- 把 Search Help 掛到資料表欄位上。
Search Help 加掛在物件的五種模式(講義原文):
- SE11 顯示資料表 → 游標在欄位上 → 按工具列的 Srch Help 鈕(掛在 Table Field)
- Screen(Dynpro)的欄位屬性 → Search help 欄位
- Data Element 的 Further Characteristics → Search Help(最推薦,一次全系統生效)
- Dialog/報表:PARAMETERS p_x TYPE ... MATCHCODE OBJECT <search help>
- 透過 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,這樣升級不會被蓋掉)。
第一種寫法不會連動: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.
11. Lock Object 鎖物件與 ENQUEUE/DEQUEUE
鎖物件是對資料的存取進行並行控制。加鎖就是把使用者正在存取的資料登記在記憶體的 Lock Table(由 Enqueue Server 管理),解鎖就釋放。建立鎖物件會自動產生兩個函數,一個加鎖、一個解鎖;鎖物件名稱要以 E 開頭。
SE11 建立步驟:(依講義步驟整理)
- SE11 → Lock object → 輸入 EZERP300_VBAK → Create。
- Tables 頁籤:Primary table 填 ZERP300_VBAK;若要連明細一起鎖,可再加 Secondary table(要有外來鍵關係)。
- Lock mode 選:
- E(Write lock,獨佔可累加):最常用,同一交易可重複加鎖
- S(Read lock,共享):多人可同時讀,但不可有人寫
- X(Exclusive,不可累加)
- O(Optimistic):先樂觀讀,存檔時才升級為寫鎖
- Lock parameter:預設是主表的所有 Key 欄位;可只挑部分(未挑的欄位視為萬用,會鎖更廣)。
- 激活 → 系統產生 ENQUEUE_EZERP300_VBAK 與 DEQUEUE_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)。
12. 擴充既有資料表:Append Structure 與 Include
要在標準表(或別人的表)上加欄位,不能直接改表定義(升級會被蓋掉、且屬於修改標準),正確做法有兩種:
| 方式 | 做法 | 適用 |
|---|---|---|
| Append Structure | SE11 顯示表 → Append Structure… → 建 ZA_MARA_EXT,欄位一律以 ZZ 或 YY 開頭 | 加在表的最後面;升級安全;一張表可以有多個 Append |
| Customizing Include(CI_) | SAP 事先在表中預留 .INCLUDE CI_xxxx,你只要去建那個結構 | SAP 指定的擴充點,如 CI_COBL(會計指派區塊) |
| .INCLUDE 自建結構 | 自建表的 Fields 中直接寫 .INCLUDE 引用一個 Structure | 自己的 Z 表,讓多張表共用同一段欄位(如異動人/異動日) |
" 讀取自己 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 函數、程式碼可進版控。
" 寫法 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。
14. 用程式讀字典:DD* 表與 RTTI
字典本身也是一堆表。會查這些表,等於多了一把萬用鑰匙:
| 字典表 | 內容 |
|---|---|
| DD02L / DD02T | 資料表清單/表的說明文字 |
| DD03L / DD03T | 表的欄位清單(含 Key 旗標、位置、Data Element)/欄位說明 |
| DD04L / DD04T | Data Element/其文字 |
| DD01L / DD01T | Domain/其文字 |
| DD07L / DD07T | Domain 的固定值/固定值文字 |
| DD08L / DD05Q | 外來鍵表頭/外來鍵欄位對應 |
| DD25L / DD26S / DD27S | View 表頭/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.
練習與自我檢核
- 建立 ZERP300_KNA1:MANDT/KUNNR/NAME1/ZWERT/ZMENG/ZIEME。檢核點:CURR 有沒有指定幣別參考欄位(VBAP-WAERK)、QUAN 有沒有指定單位參考欄位(VBAP-ZIEME)?Delivery Class 是不是 A?做到能用 SE16N 新增出資料才算成功。
- SE54 建維護畫面 → SM30 能維護。若 Function Group 選不到,用 SE37/SE80 自己建一個並全部激活;也可先用 Z000。做完再用 SE93 把它包成一支 TCODE(parameter transaction,VIEWNAME + UPDATE=X + Skip initial screen)。
- 建立四張表並手繪 ER Diagram:ZERP300_KNA1 / ZERP300_MARA / ZERP300_VBAK / ZERP300_VBAP,標出 1:N 關係與外來鍵方向。
- Domain + Data Element 練習:建一個幣別 Data Element,限定只能填 TCURC 內的值(Domain 型別 CUKY 5 + Value Table TCURC,Data Element 綁它,表欄位建外來鍵)。另建一個數值 Domain 並觀察 Sign 不打勾=只能輸入正數。
- 外來鍵練習(講義 04):Z2202_00_VBAK N → 1 Z2202_00_KNA1。要先建 ZDKUNNR(Domain)+ ZEKUNNR(Data Element)。練習題:為訂單明細建 ZDMATNR + ZEMATNR,Z2202_00_VBAP N → 1 Z2202_00_MARA。
- Text Table 練習:知道 MAKT,如何找到 MARA?寫出四種方法(SE11 外鍵、Delivery and Maintenance 頁籤的 Text Table 標註、命名習慣+回頭核對、Where-Used List / DD03L / DD08L)。
- Database View:建 ZDV2202_00_VBAK(VBAK + KNA1)。練習題:ZDV300_VBAP_MARA、ZDV300_VBAK_KNA1。答案要點:多表 DB View 只能顯示、強制 INNER JOIN。
- Maintenance View:建 ZMV_2202_00_VBAK,記得所有主鍵都要進視圖並用 SE54 產生螢幕。練習題:ZMV300_VBAP_VBAK。
- Structure / Table Type:建一個 ZDV_ 開頭的多層結構(不要放 MANDT,前三碼不能有底線),再建對應的 Table Type,然後在報表中用它接 SELECT 結果並用 CL_SALV_TABLE 顯示。
- SYST 默寫:至少寫出 10 個 SY 欄位與用途(SUBRC/UNAME/DATUM/UZEIT/TCODE/INDEX/TABIX/MANDT/LANGU/ULINE/VLINE/MSGV1/DBCNT)。
- Type Group:說明 TYPE-POOL slis 裡的型別命名規則(必須以類型組名開頭),並回答:S/4HANA 上為什麼不必再寫 TYPE-POOLS?新開發應該改用什麼?
- 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)。
- Search Help 連動:解釋為什麼 P_1 TYPE T001W-WERKS 不會連動、P_1 TYPE T001L-WERKS 會連動。
- Collective Search Help 擴充:以 ME23 找出採購資訊的 Search Help 是 MEKK,說明如何用 Append Search Help 掛上自己的 Elementary(升級安全)。並說出 Search Help Exit 必須遵守哪支範例函數的介面(F4IF_SHLP_EXIT_EXAMPLE)與五個 step(SELONE/PRESEL/SELECT/DISP/RETURN)。
- Lock Object:建 EZERP300_VBAK,寫一支程式做「加鎖 → 讀 → 改 → COMMIT → 解鎖」,並測試兩個 session 同時執行時第二個會拿到什麼訊息(FOREIGN_LOCK,SY-MSGV1 帶鎖定者)。再說明 _SCOPE 1/2/3 的差別,以及用 SM12 查鎖。
- 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 畫面就反推出它背後是哪張表、是怎麼被設計出來的。
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。
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 執行 |
3. 第一步:SE11 建三張表 ZMARA / ZMARC / ZMARD 與外部索引鍵
這三張表在模仿 SAP 標準物料主檔的三層結構:ZMARA(一般資料,Key = MANDT + MATNR)→ ZMARC(工廠資料,Key = MANDT + MATNR + WERKS)→ ZMARD(儲存地點/庫存資料,Key = MANDT + MATNR + WERKS + LGORT)。
| 表 | 主鍵 | 建議欄位 | 外部索引鍵 |
|---|---|---|---|
| ZMARA | MANDT、MATNR | MTART(物料類型)、MATKL(物料群組)、MEINS(基本單位)、ERSDA、ERNAM | MTART → T134、MEINS → T006 |
| ZMARC | MANDT、MATNR、WERKS | DISMM(MRP 類型)、EKGRP(採購群組)、BESKZ(採購類型) | MANDT、MATNR → ZMARA;WERKS → T001W |
| ZMARD | MANDT、MATNR、WERKS、LGORT | LABST(未受限庫存)、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 視情況。(依講義步驟整理)
三張表建好之後,先用一段 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 }|.
4. 第二步:SE11 建三個維護視圖 ZMV_MARA / ZMV_MARC / ZMV_MARD
路徑:SE11 → View → 輸入 ZMV_MARA → Create → 選 Maintenance View。
- Tables/Join Conditions:Tables 只放一張表(ZMARA)。三層維護的每一個維護視圖各自對應一張表,不要在這裡就 JOIN 起來,JOIN 是 View Cluster 的工作。
- View Flds:按 Table fields 把要維護的欄位全部帶進來。Key 欄位一定要在。
- Maint.Status(維護狀態)頁籤:Access = Read and change(可讀可改);Delivery class 通常設 A(Application table)或 C(Customizing table,會產生傳輸請求)。
- 存檔 → 指定 Package(三個 View 必須放同一個 Package)→ Activate。
- ZMV_MARC、ZMV_MARD 重複同樣動作。
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 type | one step 或 two step | 見下表 |
| Overview screen / Single screen | 按 Find scr. number(s) 讓系統自動配號(通常 0001 / 0002) | 兩步式才需要第二個畫面號 |
| 維護型態 | 畫面 | 適用時機 |
|---|---|---|
| 一步式 One step | 只有一個總覽畫面(Overview),所有欄位都在同一列上編輯 | 欄位少(大約 10 個以內)、螢幕放得下;三層維護的下層通常用一步式 |
| 兩步式 Two step | 總覽畫面只顯示 Key + 少數欄位,雙擊某一列才進入單筆明細畫面(Single screen)維護全部欄位 | 欄位多、或需要在明細畫面做分組與說明文字 |
三個維護視圖各做一次 SE54,做完三個維護對話框。做完之後用 SM30 逐一測試:輸入 ZMV_MARA → Maintain → 應該可以新增/修改/刪除。
6. 在維護對話框掛事件:存檔前檢核(Event 01 / 05 / 21)
SE54 → 進入產生畫面後選單 Environment → Modification → Events,選一個事件編號,填一個 FORM 名稱,按小鉛筆讓系統把 FORM 骨架寫進 Function Group 的 include(通常是 LZFG_MM_MARAF01)。常用事件:
| 事件 | 時機 | 典型用途 |
|---|---|---|
| 01 | Before saving the data in the database | 存檔前的完整性檢核、擋掉不合理資料 |
| 02 | After saving the data in the database | 寫 Log、觸發後續處理 |
| 05 | Creating a new entry | 自動帶預設值、自動編號 |
| 21 | Fill hidden fields | 補 ERDAT/ERNAM/AEDAT/AENAM 這種不顯示在畫面上的欄位 |
| AA | Instead 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.
'N' 新增、'U' 修改、'D' 刪除、空白=未異動),資料本體從第 2 個字元起,所以上面才要用 <ls_total>+1 搭配 CASTING。直接把 TOTAL 當成 view 結構用會整個欄位位移。7. 第四步:SE54 建 View Cluster(三層)與 SM34 執行
路徑:SE54 → 在初始畫面選 Edit View Cluster(維護視圖群組)→ 輸入名稱 ZMV_MARA_000 → Create。
- Object Structure(物件結構):依序把三個維護視圖登記進來
- ZMV_MARA Position 1、Dep. = 上層(起始節點)、Predecessor 留空
- ZMV_MARC Position 2、Predecessor = ZMV_MARA
- ZMV_MARD Position 3、Predecessor = ZMV_MARC
- 每一列雙擊進 Field-Dependence(欄位相依):把上層傳下來的 Key 欄位對應好,並勾上前面說的 S。ZMARC 要對 MANDT + MATNR;ZMARD 要對 MANDT + MATNR + WERKS。系統自動抓的關聯鍵可能不準,要會自己調整。
- Header Entry:填 View Cluster 的說明文字。
- 存檔 → 指定 Package(跟三個 View 同一個 Package)→ 返回上一頁之後,要再按 ACTIVATE 鈕(老師特別提醒,很多人漏掉這一步,結果 SM34 打不開)。
執行:SM34 → 輸入 ZMV_MARA_000 → Maintain。應該看到左邊 Dialog Structure 樹狀,點 ZMARA 選一筆物料 → 雙擊左樹的「工廠」節點 → 只出現該物料的工廠資料 → 再往下就是儲存地點。
三層維護實作心得(老師的三條鐵律)
- 所有的 View 都要在同一個 Package。跨 Package 會在啟用或傳輸時出問題。
- SE54 讓系統自動抓關聯鍵時,可能不準,要會自己調整。自動推導靠的是外部索引鍵,外鍵設計不完整就會抓錯。
- 「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)補齊,再重新產生一次維護對話框。
8. 第五步:把 View Cluster 變成自己的 TCODE(參數交易)
SM34 每次都要輸入 View Cluster 名稱很不方便。SAP 標準的做法(OMT3、OVXM 這些交易就是這樣做出來的)是建一支參數交易(Parameter Transaction),直接跳過初始畫面。
操作:SE93 → 建立交易碼 ZMM_MARA3 → 選 Transaction with parameters (parameter transaction) →
| 設定 | View Cluster(SM34) | 單一維護視圖(SM30) |
|---|---|---|
| Transaction | SM34(或 VIEWCLUSTER) | SM30 |
| Skip initial screen | 勾選 | 勾選 |
| Default Values:欄位 1 | VCLDIR-VCLNAME = ZMV_MARA_000 | VIEWNAME = ZMV_MARA |
| Default Values:欄位 2 | UPDATE = X | UPDATE = X |
VCLDIR-VCLNAME zmv_mara_000 / UPDATE x 兩行的意思。相關的系統表(想知道某個 TCODE 背後掛的是哪個 View Cluster,就查這幾張):
| 表 | 內容 |
|---|---|
| VCLDIR | View Cluster 目錄(VCLNAME=群組名稱) |
| VCLSTRUC / VCLSTRUCT | View Cluster 的物件結構與階層關係 |
| VCLMF | View 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:分割螢幕編輯器與程式比較
SE39 是 ABAP 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 的客戶現場仍然是必備技能。
10. 查資料的兩把刀:SE16N 與 SE16H;以及怎麼由畫面欄位反查資料表
SE16 / SE16N / SE16H 的差別
| TCODE | 特色 | 什麼時候用 |
|---|---|---|
| SE16 | 最原始的 Data Browser,畫面陽春 | 權限只給到 SE16 時 |
| SE16N | General Table Display。可自選輸出欄位、可存 Variant、可直接輸出成 ALV/Excel、支援 &SAP_EDIT(多數 S/4 系統已鎖住或需 Debug 才能開) | 日常查資料的主力 |
| SE16H | HANA 版。多了 Group by/Aggregation(SUM、MAX、MIN、AVG、COUNT)、Having、Order by、多表 JOIN(Table Join 頁籤)、以及「Total」「Sort」欄位設定 | 要做小計、分群、快速驗證統計數字時 —— 不用寫程式就能當一次性報表 |
由畫面欄位反查資料表(四招)
- F1 → 技術資訊(Technical Information):把游標放在欄位上按 F1 → 按小鎚子(Technical Information)→ 會顯示 Table Name(畫面結構名)、Field Name、Data Element、Screen field、Program name / Screen number。注意這裡看到的常常是畫面結構(例如 RM03M、VBAK 的 dialog structure),不一定是真正的資料庫表。
- 從 Data Element 反查:拿到 Data Element 之後,SE11 顯示該 Data Element → 按 Where-Used List(Ctrl+Shift+F3) → 勾 Table Fields → 就會列出所有用到這個資料元素的資料表。
- SE84(Repository Information System):ABAP Dictionary → Database Tables/Views,可以用名稱樣式、Package、Application Component 搜尋。例如想找 SD 的表就用 Application Component = SD 去撈。SE84 也可以反查「某個 Table 被哪些程式用到」。
- SDBE / ST05(追 SQL):
- ST05 SQL Trace:Activate Trace → 去畫面做一次操作 → 回來 Deactivate Trace → Display 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)
| 表 | 層級 / 主鍵 | 內容 |
|---|---|---|
| MARA | MATNR | 物料一般資料:MTART 物料類型、MATKL 物料群組、MEINS 基本單位、MBRSH 產業別 |
| MAKT | MATNR + SPRAS | 物料說明(多語言)。查中文要加 spras = @sy-langu |
| MARC | MATNR + WERKS | 工廠層資料:DISMM MRP 類型、EKGRP 採購群組、BESKZ 採購類型、DZEIT 自製時間 |
| MARD | MATNR + WERKS + LGORT | 儲存地點庫存:LABST 未受限、INSME 品檢、SPEME 凍結 |
| MBEW | MATNR + BWKEY(+BWTAR) | 評價資料:VPRSV 價格控制(S/V)、STPRS 標準價、VERPR 移動平均價、LBKUM 總量、SALK3 總值 |
| MVKE | MATNR + VKORG + VTWEG | 銷售資料 |
| MCHA / MCHB | 批次主檔 / 批次庫存 | 有啟用批次管理才有 |
| MARDH / MBEWH / MCHBH | + LFGJA + LFMON | 歷史庫存(期末)。查上月底庫存必用,但只有「該月有異動」才會產生歷史列 |
客戶主檔(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(付款) |
廠商主檔(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 | 採購資料(依產品別) / 銀行 / 扣繳 | — |
CVI_EI_INBOUND_MAIN 或 OData API_BUSINESS_PARTNER)。銷售 / 出貨 / 開票(SD)
| 單據 | 抬頭 | 明細 | 備註 |
|---|---|---|---|
| 銷售訂單 VA01/02/03 | VBAK(VBELN) | VBAP(VBELN+POSNR) | VBEP 交期排程、VBPA 夥伴、VBKD 商務資料 |
| 交貨單 VL01N/VL03N | LIKP | LIPS | LIPS-VGBEL/VGPOS 回指銷售訂單 |
| 發票 VF01/VF03 | VBRK | VBRP | VBRP-AUBEL/AUPOS 回指銷售訂單 |
| 流程串接 | VBFA(單據流 Document Flow) | — | VBELV 前置單據 → VBELN 後續單據,VBTYP_N 單據類別 |
採購(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 / STPO | BOM:物料↔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 / T002 | Client 清單 / 語言碼 |
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-PERSNUMBER | NAME_FIRST 名字、NAME_LAST 姓氏、NAME_TEXT 全名、DATE_FROM/DATE_TO 有效期間 |
| USR02(登入資料) | USR21-BNAME = USR02-BNAME | ANAME 建檔人員、TRDAT 最後登錄日、LTIME 最後登錄時、ERDAT 建檔日、UFLAG 鎖定旗標、CLASS 使用者群組 |
| 其他常用 | — | USR03 位址、ADR6 email、AGR_USERS 角色分派、UST04 Profile、USREFUS 參考使用者 |
SELECT SINGLE 抓 ADRP,如果剛好有多筆就會抓到不確定的那一筆 —— 這正是「SELECT SINGLE 沒有排序保證」的經典陷阱。正解是加上 date_from <= @sy-datum AND date_to >= @sy-datum 與 nation = @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.
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,一旦中間加了 SORT 或 DELETE 就會改到錯的列。改用 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.
WHERE b~bukrs IN @s_bukrs 會把「沒有公司代碼資料的客戶」整筆濾掉(因為 NULL 不符合任何條件),所以上面才寫成 ( b~bukrs IN @s_bukrs OR b~bukrs IS NULL )。這是 OUTER JOIN 最常見的坑。真的要嚴格篩選就改用 INNER JOIN。15. Infotype 與 HR 資料表
什麼是 Infotype
Infotype(資訊類型)是把內容相似的資料分組管理的單位,用四位數字當代碼,例如 0001 組織分派、0002 個人資料。從技術面看,一個 Infotype 同時是一組資料分群、一個畫面、一張資料庫表、以及一個結構;它還帶著系統控制屬性,例如時間限制(Time Constraint)、追溯計算相關性等。
為什麼 HR 要用 Infotype?講義給了兩個理由:
- Security(安全):HR 資料的敏感度差很多。地址、稅務資料員工可以自己看自己改;薪資、考評不行。切成不同 Infotype 才能對每一組資料套不同的權限設定 —— 誰能看、誰能改。
- History(歷史):HR 資料是時間相依的。你現在的薪資是一個數字,下個月一號調薪之後是另一個數字;七月一日搬家。舊資料必須保留,Payroll 才知道今天該付多少、下個月該付多少;報表也才回答得出「YuLin 從到職到現在的薪資變化」。所以每一個 Infotype 記錄都有起始日(BEGDA)與結束日(ENDDA)。
| Infotype | 內容 |
|---|---|
| 0000 | Events(人事異動措施) |
| 0001 | Org assignment(組織分派):PERNR、姓名、員工群組/次群組、職位、人事範圍/次範圍、成本中心 |
| 0002 | Personal info(個人資料) |
| 0003 | Payroll data(薪資狀態) |
| 0007 | Work time(計畫工時) |
| 0221 | Adjustment(薪資調整) |
| 1000–1999 | PD Relationship infotypes(組織管理的物件與關係) |
Pnnnn,例如 P0001),程式用 GET PERNR 事件搭配 INFOTYPES: 0001, 0002. 取用。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 |
| PCERT | Payroll posting runs(薪資過帳執行) | — |
| CATSDB | CATS 工時登錄 | 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 + 有效期間。16. 開發命名規則釋例
這是一份實際專案的開發準則案例,僅供參考;到職場之後仍需依公司的規範。命名的四個原則:簡易快捷且唯一、符合 SAP 客戶化命名空間、盡可能短、有意義;少用特殊字元(底線除外);如果是複製 SAP 標準物件,請在前面加 "Z_"。
Repository 物件
| 物件 | 範例 | 長度 | 規則 |
|---|---|---|---|
| Program / Report | ZFIR0001 | 8 | 1=Z;2-3=模組簡寫;4=程式類型;5-8=序號 |
| Transaction Code | ZFIR0001 | 8 | 同 Program |
| Function Group | ZFI_CHECK_DOCNUM | 26 | 1=Z;2-3=模組;4 起底線+有意義名稱 |
| Function Module | ZFI_ASK_FOR_CONFIRMATION | 30 | 同上 |
| Package / Development Class | ZDEV_FI | 30 | 1=Z;2-4=DEV;5-7=模組 |
| Authorization Object | ZMM_INVENT | 10 | 1=Z;2-3=模組;其後自由但唯一 |
| Enhancement Project | ZMM0001 | 8 | 1=Z;2-3=模組;5-8=序號 |
| SmartForms / SAPscript | ZMMF_INVOICE | 30 | 1=Z;2-3=模組;4=F;5-30 自由 |
| Table / View / Structure | ZSD_BILLING_RANGES | 16 | 1=Z;2-3=模組;5-16 自由。View 加 _V 後綴 |
| Domain | ZCUSTTYPE | 30 | Z 開頭 |
| Data Element | ZCUSTYPE | 30 | Z 開頭 |
| Type Group | ZCUST | 5 | Z 開頭 |
| Field | CUSTTYPE | 16 | 自訂欄位加 Z;SAP 既有欄位沿用不加 |
| Search Help | ZSH_CUST | 30 | 1=Z;2-3=SH;其後自由 |
| Lock Object | EZTCUST | 16 | 1=E;其後接物件名(通常是表或 view 名) |
| Append Structure | ZZVBAK(append 到 VBAK) | 16 | 1-2=ZZ |
| Custom BAdI | ZMM_CUSTOMER_DATA | 20 | 1=Z;2-3=模組 |
| BAdI Implementation | ZMM_CUSTOMER_DATA_01 | 20 | 最好包含 BAdI 名稱 |
| BAdI Method | GetSalesOrder | 30 | 自由但要描述功能 |
| Message ID | ZSD01 | 20 | 1=Z;2-3=模組;4-5=序號 |
| Area Menu(PF-Status) | S001 | 20 | 1=S;2-4 自由 |
| IDoc Type / Message Type / Process Code | ZFIDELV001、ZSDBILLING | 30 | 1=Z;2-3=模組 |
| Segment Definition / Documentation | ZSDORDER01 / Z1KORDER01 | 27 | Z 開頭+模組 |
程式類型代碼(Program Type,命名第 4 碼)
| 代碼 | 說明 | 代碼 | 說明 |
|---|---|---|---|
| F | Smartform / Scriptform | M | Module Pool |
| R | Report | B | BDC |
| U | Utility Program | E | Customer Enhancement |
| C | Conversion | I | Interface |
SAP 模組代碼
| 代碼 | 模組 | 代碼 | 模組 |
|---|---|---|---|
| XA | Cross Application | PS | Project System |
| FI | Finance | HR | Human Resource / Payroll |
| CO | Controlling | BW | Business Warehouse |
| LO | Logistics General | SM | Strategic Enterprise Mgmt |
| MM | Materials Management | BS | Basis related program |
| WM | Warehouse Management | SV | Service Management |
| SD | Sales and Distribution | PL | Product Lifecycle Management |
| PP | Production Planning | CR | CRM |
| QM | Quality Management | AM | Asset Management |
| PM | Plant Maintenance | EP | Enterprise 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 Element | RB_ / 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 Field | SY- | SY-DATUM、SY-UNAME、SY-SUBRC |
WHERE ... IN。現代寫法建議改用 TYPES ty_r_lifnr TYPE RANGE OF lifnr. 搭配 VALUE #( ( sign = 'I' option = 'EQ' low = '0000100001' ) ),比 RANGES 更清楚且可當方法參數傳遞。訊息與傳輸請求
| Message Type | 意義 |
|---|---|
| A | Abend —— 取消資訊(程式直接中止) |
| E | Error —— 錯誤資訊 |
| W | Warning —— 警告資訊 |
| I | Informational —— 訊息視窗 |
| S | Success —— 成功資訊(顯示在下一個畫面的狀態列) |
Message Number 為 3 位數字,000~999。傳輸請求(Task / Request)的描述格式:「YYMMDD:程式名稱代碼:修改或新增主要描述」,最長 60 字元,例如 060720:ZMMR0001:增加物料類型條件。實務上仍應以 Basis 顧問提供的 CR 描述規定為準。
17. 總結練習:從零做出一整套(第一節的驗收題)
講義開宗明義給的練習順序,是把 1-2 章的資料字典與這一章的維護畫面串起來的完整流程:
- Domain(域)—— 定資料型態、長度、值範圍(Value Range/Fixed Values)
- Data Element(資料元素)—— 掛 Domain,定四種欄位標籤與 F1 說明文件
- Master Table(主檔表)—— 例如 Z00_KNA1
- Transaction Table(交易表)—— 例如 Z00_VBAK
- Foreign Key(外部索引鍵)—— 把交易表接回主檔表
- Search Help(搜尋說明)—— 掛在欄位或 Data Element 上,F4 才有東西
- 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.
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。練習與自我檢核
- 照著 SE11 建出 ZMARA / ZMARC / ZMARD 三張表,並設定 ZMARC → ZMARA(MANDT、MATNR)與 ZMARD → ZMARC(MANDT、MATNR、WERKS)的外部索引鍵。提示:Cardinality 選 1:CN。
- 建三個維護視圖 ZMV_MARA / ZMV_MARC / ZMV_MARD,全部放在同一個 Package,用 SE54 各產生一次維護對話框。提示:Function Group 用自己的 Z 開頭,不要用 TMP。
- 建 View Cluster ZMV_MARA_000,用 SM34 執行,做到「選一個物料 → 只看到該物料的工廠 → 只看到該工廠的儲存地點」。提示:Field-Dependence 要勾 S,MANDT 不用勾 S;存檔返回後記得按 Activate。
- 用 SE93 把 ZMV_MARA_000 包成一支自己的 TCODE。答案要點:參數交易,Transaction = SM34、勾 Skip initial screen、VCLDIR-VCLNAME = ZMV_MARA_000、UPDATE = X。
- 在 ZMV_MARA 的維護對話框掛上 Event 21,讓 ERSDA/ERNAM 自動帶值;再掛 Event 01,檢核 MTART 必須存在於 T134。提示:路徑是 SE54 → Environment → Modification → Events。
- 去 NACE、OKKP、OMT3 各點一遍,說出它們各有幾層、每一層對應哪張表。提示:用 SE93 看 TCODE 的參數,再查 VCLDIR / VCLSTRUC。
- 寫 ZUSERS(WRITE 版),輸出項次/員工帳號/建檔人員/員工編號/姓/名,最後印總筆數與執行者。要求:不用 TABLES、不用 SELECT SINGLE 在迴圈裡、用 lines( ) 取代 DESCRIBE。
- 把上題改寫成 CL_SALV_TABLE 版本,加上中文欄位抬頭、斑馬紋、最佳欄寬、清單抬頭。提示:get_columns( ) / get_display_settings( ) / get_functions( )。
- 做 XD99 練習:把 KNA1 + KNB1 + KNVV 三視角合併在同一個 ALV。陷阱:LEFT OUTER JOIN 加 WHERE 會濾掉 NULL 那一側。
- 做 XK99 練習:把 LFA1 + LFB1 + LFM1 合併在同一個 ALV。順便回答:LFA1-KTOKK 是什麼?搜尋字串是哪個欄位?(答:帳戶群組;SORTL)
- 用 F1 → 技術資訊查出 VA03 畫面上「銷售訂單號碼」欄位的 Data Element,再用 Where-Used List 找出所有含此資料元素的資料表。答案要點:Data Element = VBELN_VA,會找到 VBAK、VBAP、VBFA 等。
- 用 ST05 追一次 ME23N 的顯示動作,說出它讀了哪些表;再用 SE16H 對 EKPO 做 Group by LIFNR 的金額小計。
- 說明在 S/4HANA 中,下列各項的現況:MSEG、KONV、VBUK、MARD、BSIS、COEP。答案要點:分別對應 MATDOC、PRCD_ELEMENTS、併入 VBAK、NSDM 相容視圖、ACDOCA 相容視圖、ACDOCA。
- 用 SE39 把「自己寫的 ZUSERS」與「老師版 ZUSERS」並排比較,列出三個你改掉的地方與理由。
- Infotype 為什麼要切這麼細?用「安全」與「歷史」兩個角度各舉一個例子。再說明 PA0001 與 HRP1001 的差別。
- 依命名規則,替下列需求取名:一支 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 的時機。老師說這門課三小時可以教完,但它能幫你擋掉一半以上的臨時報表需求。
1. 三層工具的定位:SE16H → SQVI → SAP Query → 自己寫程式
使用者丟一個報表需求過來,先問自己「非得寫程式不可嗎?」。SAP 給了一整排不寫程式的工具,成本由低到高排序如下:
| 層級 | 工具 | 能做什麼 | 不能做什麼 |
|---|---|---|---|
| 0 | SE16N / SE16H | 單表瀏覽;SE16H 在 HANA 上可做群組加總、排序、甚至多表聯結 | 沒有固定版面、不能給一般使用者當正式報表、不易掛 TCODE |
| 1 | SQVI QuickViewer | 個人用的多表聯結報表,ALV 輸出,五分鐘完成 | 只屬於建立者本人、無法傳輸、無法給別人執行 |
| 2 | SQ03+SQ02+SQ01 SAP Query | 可分享、可授權、可掛 TCODE、可傳輸;支援附加欄位與 ABAP 程式碼區段 | 複雜邏輯、效能調校、單元測試、對外介面都很吃力 |
| 3 | SE38 自己寫 ABAP | 什麼都能做 | 成本最高,要走完整開發與傳輸流程 |
| 3+ | CDS View + Fiori(S/4HANA) | 可重用、可 OData 曝露、可進 Analytical List Page/Embedded Analytics | 需要 Eclipse ADT 與較完整的建模概念 |
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,一個畫面內就把「選哪些表、怎麼聯結、要哪些欄位、要哪些選擇條件」全部做完。(依講義步驟整理)
- 進入 SQVI,輸入 QuickView 名稱(最長 14 碼,建議 ZERP###V),按「建立」。
- 輸入抬頭說明(Title)、備註,選擇資料來源:
- 表格(Table):只查一張表,最單純。
- 表格聯結(Table join):多表聯結,二階報表就選這個。
- 邏輯資料庫(Logical database):沿用 SAP 標準的取數樹(含權限檢查)。
- InfoSet:直接沿用 SQ02 已經做好的 InfoSet。
- 選擇「表格聯結」後進入 Join 畫面,按「插入表格」加入第二張表;SAP 會依外鍵自動連線,務必檢查自動連上的欄位是否正確,多餘的連線用滑鼠右鍵刪除。
- 回到基本清單(Basis mode)畫面,左邊是欄位群組樹,勾選 清單欄位(List fields) 與 選擇欄位(Selection fields)。
- 存檔後按 執行(F8),出現選擇畫面 → 再 F8 → ALV 報表。
1. QuickView 是使用者私有的,別人登入看不到,也不能執行。
2. 無法掛傳輸請求,DEV 做好不會自己跑到 QAS/PRD;老師的說法是「一般是不傳輸,直接就地使用」。
3. 不支援附加欄位的 ABAP 程式碼區段(那是 SQ02 InfoSet 才有的功能)。
要正式化,就在 SQ01 用「Query → 由 QuickView 轉換(Convert QuickView)」把它升級成 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 | 物件 | 做什麼 |
|---|---|---|---|
| 1 | SQ03 | 使用者群組 User Group | 決定「誰可以用這組報表」,並把 InfoSet 掛進來 |
| 2 | SQ02 | InfoSet | 決定「資料從哪來、有哪些欄位」(Join/LDB/程式) |
| 3 | SQ01 | Query 查詢 | 決定「報表長什麼樣子」(選擇欄位、輸出欄位、小計、排序) |
| 4 | SQ00 | 執行 | SQ00 與 SQ01 畫面幾乎一樣,SQ00 定位是「執行」、SQ01 是「維護」 |
SQ03 建立步驟(依講義步驟整理):
- 進 SQ03,先看畫面上方的查詢區(Query Area)是不是你要的(見下一節),必要時用「環境 → 查詢區」切換。
- 輸入使用者群組名稱(講義用 ZCERPS、ZVBAP_QALS、ZVBAK_KNA1、Z000),按「建立」,輸入說明文字後存檔。
- 按 「指定使用者與 InfoSet」:
- 指定使用者:把要執行報表的 SAP 帳號加進來。有勾「Change authorization」的人才能改 Query。
- 指定 InfoSet:把 SQ02 做好的 InfoSet 打勾掛進這個群組(InfoSet 還沒建時先跳過,建完再回來掛)。
4. 查詢區 Standard Area 與 Global Area:能不能傳輸的關鍵
SAP Query 的所有物件(使用者群組、InfoSet、Query)都存在某個查詢區裡,這決定了它的可見範圍與傳輸方式。講義第 01-3 節的「AREA 群組範圍」講的就是這件事。
| 查詢區 | 代碼 | 範圍 | 傳輸方式 |
|---|---|---|---|
| 標準區 Standard Area | (空白) | 依 Client 分離,只在建立它的 client 看得到;不屬於任何 Package | 不會自動進傳輸請求,要用程式 RSAQR3TR 匯出/匯入 |
| 全域區 Global Area | G | 跨 Client,整個系統共用 | 屬於 Package、可掛傳輸請求,像一般開發物件一樣走 STMS |
如何切換查詢區:在 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,不必每次手動切換。
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 建立步驟(依講義步驟整理):
- 進 SQ02,確認查詢區,輸入 InfoSet 名稱(講義:ZKNA1_KNB1、ZVBAP_QALS、ZVBAK_KNA1),按「建立」。
- 輸入說明文字,選「表格聯結」並填第一張表(主表),Enter。
- 在 Join 畫面按「插入表格」加入第二張表;SAP 依外鍵自動畫連線,手動核對每一條連線;按右鍵可切換 Inner/Left Outer Join。
- 回上一頁時,SAP 問「欄位群組(Field Groups)」要怎麼建,通常選 「每個表格一個欄位群組,只含清單欄位/關鍵欄位」 或「包含所有表格欄位」。欄位群組決定使用者在 SQ01 看到的欄位分類。
- 把需要的欄位從右邊的表格樹拖進左邊的欄位群組(沒有拖進去的欄位,SQ01 就選不到)。
- 存檔 → 產生(Generate,Ctrl+F3)。沒有按產生,Query 端不會看到你的修改。
- 按「角色/使用者群組指派」,把這個 InfoSet 掛給 SQ03 建好的使用者群組。
講義的三個 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.
坑二: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.
8. SQ01 設計報表:從欄位到版面
InfoSet 準備好、使用者群組也掛好之後,才輪到 SQ01。步驟(依講義步驟整理):
- 進 SQ01,先按「使用者群組(Shift+F7)」選定群組(例如 ZVBAP_QALS)——沒選群組就建不出 Query。
- 輸入 Query 名稱(講義:QUE_KNV1、ZVBAP_QALS、QUE_VBAK_KNA1),按「建立」,選 InfoSet。
- 第 1 頁:抬頭與備註——填 Query 標題、備註,以及輸出格式(SAP List Viewer/ALV Grid 最常用,也可選 ABAP List、Excel、Word 合併列印、下載檔案)。
- 第 2 頁:選擇欄位群組——勾選這個 Query 要用到哪些欄位群組。
- 第 3 頁:選擇欄位——勾選要出現在報表上的欄位。
- 第 4 頁:選擇(Selections)——勾選哪些欄位要出現在選擇畫面上,並可指定是 SELECT-OPTION 還是 PARAMETER、是否必填。
- 基本清單(Basic List)——排欄位順序(Line/Sequence)、排序(Sort)、小計(Total/Counter)、群組小計。
- 還可再做 統計清單(Statistics)與排名清單(Ranked List),做出「前 10 大客戶」這種報表。
- 存檔 → 執行(F8)→ 測試 → 回頭調整。
9. 產生的程式:AQ 開頭的程式名怎麼解碼
存檔 Query 時,SAP 會偷偷幫你產生一支真的 ABAP 程式,名字是 AQ 開頭的 30 碼怪名字。講義列出的三個實例:
| 產生的程式名 | 使用者群組 | Query 名稱 |
|---|---|---|
AQZZZCERPS======QUE_KNV1====== | ZCERPS | QUE_KNV1 |
AQZZZVBAP_QALS==ZVBAP_QALS==== | ZVBAP_QALS | ZVBAP_QALS |
AQZZZ000========QUE_VBAK_KNA1= | Z000 | QUE_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 }|.
產生的程式長成下面這樣。它是系統自動產生的骨架,只是把工作丟給幾支同樣自動產生的 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.
WITH HEADER LINE 這種在自寫程式中已被禁止的舊語法,也不代表你可以照抄——那是產生器的內部實作。另外,S/4HANA 升級後常見的 LOAD_PROGRAM_NOT_FOUND dump,原因就是 AQ 程式尚未重新產生;解法是在 SQ01 重新產生該 Query,或執行標準的 Query 重產生程式(RSAQGENQ 系列)。
10. 把 Query 掛成 TCODE:方式一(直接掛程式)與方式二(START_REPORT 參數交易)
方式一:SE93 直接掛 AQ 程式
最直覺的做法:SE93 → 建立交易碼(講義:ZQUE_KNV1、ZVBAP_QALS、ZVBAK_KNA1)→ 選「使用選擇畫面的程式與變式(Report Transaction)」→ 程式名填那 30 碼的 AQ...。
/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 固定填 AQ | AQ |
| D_SREPOVARI-REPORT | SQ03 使用者群組,補空白到 12 碼;第 13 碼填查詢區(全域區填 G) | ZCERPS GZVBAP_QALS GZVBAK_KNA1 G |
| D_SREPOVARI-EXTDREPORT | SQ01 的 Query 名稱 | QUE_KNV1ZVBAP_QALSQUE_VBAK_KNA1 |
1234567890123ZCERPS G(ZCERPS 6 碼 + 6 個空白 + G)ZERP300 G(ZERP300 7 碼 + 5 個空白 + G)ZVBAP_QALS G(ZVBAP_QALS 10 碼 + 2 個空白 + G)空白數算錯,執行時就會說找不到 Query。用 SE38 試跑時,把游標放到欄位上按 F1 → 技術資訊,可確認欄位長度。
SE93 建立參數交易的步驟(依講義步驟整理):
- SE93 → 輸入交易碼(講義:ZVBAP_QALS2、ZQUE_VBAK_KNA1,建議命名 ZERP###A)→ 建立。
- 選 「具有參數的交易(Transaction with parameters / parameter transaction)」。
- Transaction 填 START_REPORT;勾選 「Skip initial screen(略過起始畫面)」與 「Inherit GUI attributes」。
- 下方參數表填入三行:
D_SREPOVARI-REPORTTYPE = AQ、D_SREPOVARI-REPORT = <使用者群組>+G、D_SREPOVARI-EXTDREPORT = <Query 名>。 - 存檔、掛傳輸請求、測試。
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( ).
12. SQ00、SQ99 與 Query 家族 TCODE 一覽
| TCODE | 名稱 | 說明 |
|---|---|---|
| SQ00 | SAP Query: Start Queries | 執行 Query。講義原話:「Sq00 : to start queries and Sq01 : to maintain queries,其實畫面長得一樣」 |
| SQ01 | SAP Query: Maintain Queries | 建立與維護 Query(主戰場) |
| SQ02 | SAP Query: Maintain InfoSet | 建立資料來源與欄位群組 |
| SQ03 | SAP Query: Maintain User Groups | 使用者群組與 InfoSet 指派 |
| SQ07 | SAP Query: Language Comparison | Query 文字的多語言比對/翻譯 |
| SQVI | QuickViewer | 個人用快速報表 |
| SE93 | Maintain Transaction | 把 Query 掛成 TCODE |
| SE16H | General Table Display (HANA) | HANA 版資料瀏覽器,支援群組、加總、多表關聯 |
Query 跳轉(Drill-down,講義 10-2):在 SQ01 的「基本清單」設計畫面,可以用選單「Goto → 報表指派(Report assignment)」把某個欄位連結到另一支報表或交易(例如點訂單號跳 VA03、點客戶號跳 XD03)。這樣使用者從清單可以直接鑽進單據,是 Query 最受歡迎的功能之一。(依講義步驟整理)
13. Report Painter / Report Writer:另一條不寫程式的路
SAP 還有另一套「不寫程式」的報表工具,方向完全不同,常被拿來跟 SAP Query 混淆。
| 比較項目 | SAP Query / SQVI | Report Painter / Report Writer |
|---|---|---|
| TCODE | SQ00~SQ03、SQVI | GRR1/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) |
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
}
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_TABLE | Query 的 Code 區段撐不住 |
| 要重複被多個報表/App/介面使用 | CDS View | 一次建模、處處可用(ABAP SQL、OData、Analytics) |
| 使用者要在 Fiori Launchpad 上看、要下鑽、要圖表 | CDS Cube + Analytical List Page / Query Browser | Embedded Analytics 直接讀即時資料,免 BW |
| 要對外提供 API | CDS + RAP/OData Service | Query 沒有介面能力 |
1. MKPF/MSEG 與 BKPF/BSEG 在 S/4 已改為相容視圖(實體表為 MATDOC 與 ACDOCA)。把它們拉進 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 | 條件(資料) |
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 | 銷售訂單項目的成本核算對象分配 |
| COSP | CO 對象:外部記帳的成本總計 |
| COEP / COBK | CO 物件:期間別行項目 / 憑證抬頭 |
| COST | CO 物件:價格總計 |
| FAGLFLEXA / FAGLFLEXT | 新總帳實際行項目 / 總計(S/4 由 ACDOCA 取代) |
QM 品質管理
| 資料表 | 說明 |
|---|---|
| QALS | 檢驗批記錄(本章 ZVBAP_QALS 實作的主角) |
| QAVE | 檢驗處理:使用決策(UD) |
| QAMV | 檢驗處理的特性說明 |
| QAMR | 檢驗處理的特性結果 |
| QASR | 檢驗特性的採樣結果 |
PP 生產計劃
| 資料表 | 說明 |
|---|---|
| AUFK | 訂單主檔(工單/內部訂單共用) |
| AFKO | PP 訂單抬頭資料 |
| 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 | 評估群組 |
*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 參數不支援內嵌宣告。練習與自我檢核
- (講義 11 節)SQVI 品質檢驗:用 SQVI 建 ZERP###V,聯結 VBAP 與 QALS,做出「待出貨訂單+檢驗批狀態」清單。
提示:資料來源選「表格聯結」,Join 條件 MATNR+WERKS;輸出欄位至少要有 VBELN、POSNR、MATNR、KWMENG、PRUEFLOS、HERKUNFT、STAT34。做完想想:為什麼這份 QuickView 沒辦法給主管執行? - (講義 02-1)ZKNA1_KNB1:用 SQ03 建使用者群組、SQ02 建 InfoSet 聯結 KNA1+KNB1、SQ01 做出「客戶編號 → 統馭科目(AKONT)」對照表。
提示:KNB1 一定要用 BUKRS 當必填選擇條件,否則跨公司代碼的客戶會出現重複列。 - (講義 02-3 / 03-3)ZVBAK_KNA1 + QUE_VBAK_KNA1:做出訂單抬頭+客戶名稱報表,並在 SQ01 加上「依銷售組織小計淨值」。
提示:小計要在「基本清單」畫面對 VKORG 設 Sort,再對 NETWR 勾 Total。 - 把第 3 題的 Query 用 方式二 START_REPORT 掛成 TCODE ZERP###A。
答案要點:REPORTTYPE=AQ;REPORT=使用者群組補空白到 12 碼+第 13 碼 G;EXTDREPORT=Query 名。做之前先在 SE38 執行 START_REPORT 試跑成功。 - 找出第 3 題 Query 產生的程式名,並說明它的 30 碼各代表什麼。
答案要點:SQ01 選單「Query → 其他功能 → 顯示報表名稱」,或用 RSAQ_DECODE_REPORT_NAME;AQ+區域 2 碼+使用者群組 12 碼(以=補滿)+Query 名 14 碼(以=補滿)。 - 在 InfoSet 裡加一個附加欄位「未交數量」= KWMENG − KLMENG,並讓它只顯示大於 0 的資料。
答案要點:Extras → 附加欄位 → 型別參考 VBAP-KWMENG;程式寫在 RECORD PROCESSING;過濾用CHECK。 - 解釋:為什麼在 RECORD PROCESSING 裡寫
SELECT SINGLE maktx FROM makt ...是壞習慣?該怎麼改?
答案要點:每筆資料一次 DB 存取;改成 START-OF-SELECTION 一次撈進 HASHED TABLE,或用「附加表格」。 - 你的 Query 在 DEV 100 建好了,PRD 的使用者說看不到。列出三個要檢查的地方。
答案要點:(a) 建在標準區還是全域區?標準區要跑 RSAQR3TR;(b) 使用者有沒有被指派到該 SQ03 使用者群組;(c) S_QUERY 與資料表權限(SU53)。 - 比較 SAP Query 與 Report Painter 的差異,各舉一個最適合的報表需求。
答案要點:Query=明細清單(訂單清單);Report Painter=期間矩陣(成本中心實際 vs 預算)。 - 在 S/4HANA 上,什麼情況你會選擇「不做 Query,直接建 CDS View」?至少說三個理由。
答案要點:需要被多處重用/要曝露成 OData 給 Fiori 或外部系統/要進 Embedded Analytics 做下鑽與圖表/要版本控管與傳輸/效能需要 code pushdown。 - 把講義的 ZVBAP_QALS InfoSet 改寫成一支自己的 ABAP 報表(CL_SALV_TABLE),並加上 Left Outer Join 讓沒有檢驗批的訂單也能顯示。
提示:對照本章第 6 節的程式碼,注意 QALS 的過濾條件要放在 ON 而不是 WHERE。 - 用本章最後的 ZAQ_INVENTORY 概念,盤點你手上系統有幾支 Query、分屬哪些使用者群組,並整理成「TCODE ↔ Query ↔ InfoSet」對照表。
第 2-1 章 基本語法:資料型別 / 判斷式 / 迴圈 / 日期時間 / 字串處理
這一章是 ABAP 的地基:宣告什麼型別、值怎麼在型別之間搬、判斷與迴圈怎麼寫、日期時間怎麼算、字串怎麼切怎麼拼、金額小數怎麼才不會差一塊錢。老師在講義裡放了 20 支練習程式(ZDATATYPE01~ZDATATYPE20)與 10 個作業,這裡全部整理成可查閱的條目,並把 1992 年代的舊寫法一律改寫成 S/4HANA 1809 以後可直接編譯執行的現代 ABAP。學完之後,你看到任何一段別人寫的老程式,都能立刻判斷它在做什麼、以及該怎麼改。
1. 內建資料型別總表(先把這張表背起來)
ABAP 的「資料型別」本身不佔記憶體,只有「資料物件(變數/常數)」才佔記憶體。型別可以是系統預定義(built-in)、也可以是 SE11 裡的 Data Element / Domain。下表是講義的核心表格,加上 S/4HANA 常用但講義只帶過的幾個型別。
| 縮寫 | 型別 | 預設長度 | 允許長度 | 初始值 | 說明與典型用途 |
|---|---|---|---|---|---|
| C | 文字型 Character | 1 | 1~262143 | space | 固定長度字串。取值靠左,超長截斷。MATNR、KUNNR 這類 key 都是 C |
| N | 數字字串 Numeric text | 1 | 1~262143 | '00…0' | 只放 0~9 的字串。取值靠右、左補 0。單據號、月份、POSNR 用它 |
| D | 日期 Date | 8 | 固定 8 | '00000000' | 內部格式一律 YYYYMMDD,可直接加減(單位=天) |
| T | 時間 Time | 6 | 固定 6 | '000000' | 內部格式一律 HHMMSS,可直接加減(單位=秒) |
| I | 整數 Integer | 4 bytes | 固定 | 0 | -2 147 483 648 ~ 2 147 483 647,計數器用 |
| INT8 | 長整數 | 8 bytes | 固定 | 0 | 約 ±9.2×10^18。筆數/金額分位加總避免溢位 |
| P | 壓縮數 Packed | 8 | 1~16 bytes(1~31 位) | 0 | 定點十進位,小數最多 14 位。所有金額、數量都是 P |
| F | 二進位浮點 Float | 8 bytes | 固定 | 0 | 約 15~16 位有效數字,不精確。只用在科學計算 |
| DECFLOAT16 | 十進位浮點 | 8 bytes | 固定 | 0 | 16 位精確十進位,運算比 P 快、比 F 準 |
| DECFLOAT34 | 十進位浮點 | 16 bytes | 固定 | 0 | 34 位,超大/超精密數字(利率、匯率連乘)首選 |
| STRING | 可變長字串 | 動態 | 不限 | ``(空字串) | 長度隨內容變動,保留尾端空白 |
| X | 位元組 Byte | 1 | 1~524287 | X'00' | 二進位資料、GUID、旗標位元 |
| XSTRING | 可變長位元組串 | 動態 | 不限 | 空 | 檔案上傳/下載、PDF、附件內容 |
講義強調的五個取值規則,實務上天天踩到:
- 不寫 TYPE 時,預設型別是 C(DATA 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( ).
2. TYPES / DATA / CONSTANTS / STATICS:宣告的四把刀
TYPES 定義的是「型別」,不能賦值;DATA 才是「變數」;CONSTANTS 是一經定義就不能改的常數;STATICS 是「離開副程式仍然記得上次的值」的區域變數。
| 關鍵字 | 語法骨架 | 重點 |
|---|---|---|
| TYPES | TYPES ty_x TYPE ... . | 只是型別;不能 ty_x = 1 |
| DATA | DATA lv_x TYPE ty_x VALUE '...'. | 真正配置記憶體 |
| CONSTANTS | CONSTANTS c_pi TYPE p LENGTH 8 DECIMALS 5 VALUE '3.14159'. | 必須有 VALUE,執行期不可改 |
| STATICS | STATICS lv_cnt TYPE i. | 只在副程式/方法內宣告,值跨呼叫保留 |
| LIKE | DATA 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( ).
靜態變數 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。
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 ).
4. 賦值、offset+length、指標(FIELD-SYMBOLS / REF)
ABAP 給值的方式,講義列了三種:=、MOVE、WRITE ... 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( ).
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( ).
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( ).
陷阱三:先轉型再運算,還是先運算再轉型?講義的 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( ).
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 SUPPLIED | — | field 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( ).
7. 迴圈:DO / WHILE / LOOP 與 CHECK / EXIT / CONTINUE
ABAP 共有五大迴圈:DO、WHILE、LOOP AT、SELECT ... ENDSELECT(禁用)、PROVIDE(幾乎沒人用)。控制指令四個:
| 指令 | 作用 | 用在哪 |
|---|---|---|
| CONTINUE | 無條件結束本圈,直接跑下一圈 | 所有迴圈 |
| CHECK cond | 條件不成立時等同 CONTINUE(跳過本圈) | 迴圈內;在副程式/方法內則是直接離開 |
| EXIT | 離開最內層迴圈;不在迴圈裡則離開該處理區塊 | 所有迴圈 |
| STOP | 中止 START-OF-SELECTION,直接跳到 END-OF-SELECTION | 只在 Report 事件區塊;已列 obsolete |
| RETURN | 立刻離開整個副程式/方法 | 方法內建議用它取代 EXIT |
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.
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( ).
系統日期時間欄位的差別(做全球化系統時最容易出錯的地方):
| 欄位 | 意義 | 使用時機 |
|---|---|---|
| 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( ).
工作天(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_WEEK 與 FIMA_DAYS_AND_MONTHS_AND_YEARS 為例)(依講義截圖步驟整理):
- 進 SE37,函數模組欄位輸入函數名(不確定名字就用 *DATE*WEEK* 之類的萬用字元按 F4 找)。
- 按「顯示」看 Import / Export / Changing / Tables / Exceptions 四個頁籤,先搞清楚要餵什麼、會吐什麼。
- 按 執行(F8)直接測試:畫面會列出所有 Import 參數,填好值再按 F8。
- 結果畫面會顯示 Export 參數的值、以及 RUNTIME 執行時間(微秒),可以順便比較兩支函數誰快。
- 測完滿意後,回到 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 | 使用者格式日期字串 → 內部 YYYYMMDD | date_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 / LT | i_date_1, i_date_2 → e_result |
| SD_DATETIME_DIFFERENCE | 兩個「日期+時間」相減 | date1,time1,date2,time2 → datediff, timediff |
| HRVE_CONVERT_TIME | 12 小時制 ↔ 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( ).
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 = 0 且 SY-FDPOS 是起始位置(從 0 開始算),失敗時 SY-SUBRC = 4、SY-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( ).
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 = X | XML/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( ) / 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_from | STRING ↔ 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 不讓你啟用。
例:日圓 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_IDOC | SAP → IDoc | EDI / ALE 介面 |
| CURRENCY_AMOUNT_IDOC_TO_SAP | IDoc → SAP | 介面回寫 |
| CURRENCY_AMOUNT_SAP_TO_DISPLAY | SAP → 顯示 | Dialog 畫面 |
| CONVERT_TO_LOCAL_CURRENCY | 外幣 → 本位幣 | 依 TCURR 匯率換算 |
| CONVERT_TO_FOREIGN_CURRENCY | 本位幣 → 外幣 | 同上,反方向 |
| UNIT_CONVERSION_SIMPLE | 數量單位換算 | KG ↔ G、EA ↔ CAR(表 T006) |
| MD_CONVERT_MATERIAL_UNIT | 物料專屬單位換算 | 依 MARM 的物料換算率 |
13. SY 系統欄位:一定要記住的十個
系統欄位放在結構 SYST(SE11 可查全部)。老師點名的十個:
| # | 欄位 | 意義 | 使用要點 |
|---|---|---|---|
| 1 | SY-SUBRC | 上一個指令的回傳碼,0=成功 | 資料庫/READ/CALL FUNCTION 之後必檢查;下一個指令就會被覆寫,要用就馬上存起來 |
| 2 | SY-UNAME | 登入者帳號 | 權限判斷、記錄異動者 |
| 3 | SY-DATUM | 系統日期 | 寫入 DB 用;顯示給人看用 SY-DATLO |
| 4 | SY-UZEIT | 系統時間 | 同上,本地時間用 SY-TIMLO |
| 5 | SY-TCODE | 目前執行的交易碼 | 增強/BAdI 裡判斷「從哪個畫面進來的」 |
| 6 | SY-INDEX | DO / WHILE 目前跑第幾圈 | 巢狀時只代表最內層;離開迴圈後歸零 |
| 7 | SY-TABIX | LOOP AT / READ TABLE 目前是內表第幾筆 | 只對 INDEX TABLE 有意義;HASHED 表恆為 0 |
| 8 | SY-MANDT | 用戶端(Client) | 開表時第一個欄位;ABAP SQL 會自動加條件 |
| 9 | SY-ULINE | 橫線字串 | WRITE 報表畫線 |
| 10 | SY-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( ).
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.
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( ).
方案一(配置):OMCQ 把訊息號 163 設為 E,再用 ME22N 手動勾「交貨已完成」並把「最近交貨日期」設為過去日期。
方案二(增強):加判斷條件,一旦勾了「已完成收貨」就不允許再收貨。
案例 6:結構的整批運算(第 09-2 節)。MULTIPLY-CORRESPONDING、ADD-CORRESPONDING、SUBTRACT-CORRESPONDING、DIVIDE-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 節的清單,這是老師整章最想留下來的東西。
- 命名規則:lv_(區域變數)、gv_(全域變數)、iv_(輸入參數)、ev_(輸出參數)、cv_(可改參數)、rv_(回傳值)、lt_/gt_(內表)、ls_/wa_(工作區)、lo_(物件)、lr_(參考)、<fs_>(field symbol)。
- 常數與程序名一眼看得懂:CONSTANTS c_max_attempts TYPE i VALUE 3.;程序名用「模組+物件+動作」如 ZMM_PO_CREATE_AND_VALIDATE。
- 模組化:一個方法只做一件事,超過一個畫面高度就該拆。
- 一定要寫註解:註解寫「為什麼」,不要寫「做什麼」。
- 內縮與對齊:ABAP Editor 的 Ctrl+Shift+F1(Pretty Printer)養成習慣。
- 迴圈中少用 SELECT SINGLE:改成一次撈進內表再對應(FOR ALL ENTRIES 或 JOIN)。
- 少用 SELECT *:只取需要的欄位,HANA 是欄式儲存,多取一欄就多一份成本。
- 注意索引:WHERE 條件的欄位若不在索引上,效能會斷崖式下降;用 ST05 / SAT 驗證。
- 內表操作注意排序:READ TABLE ... BINARY SEARCH 之前一定要先 SORT 同樣的 key,否則會讀不到。
- HASHED 表加速查詢:DATA lt_cust TYPE HASHED TABLE OF ... WITH UNIQUE KEY customer_id.(只能用完整 key 讀,但 O(1))。
- 批次取代逐筆:APPEND LINES OF 取代逐行 APPEND;INSERT ... FROM TABLE 取代逐筆 INSERT。
- 用 LOOP AT ... GROUP BY 取代雙迴圈。
- 重要片段做例外處理:TRY ... CATCH cx_...。
- SY-SUBRC 檢查:資料庫操作後必加。
- 訊息用 SE91 訊息類別:MESSAGE e001(zmm_po) WITH lv_ebeln. 才能多語言,不要硬編中文字串。
- OO 可以不會寫,但要看得懂:PUBLIC(對外開放)、PROTECTED(子類可用)、PRIVATE(只有自己用)。
- 禁止 SELECT ... ENDSELECT,尤其放在迴圈裡。
- 多表操作用內表與透明表交互:先 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( ).
練習與自我檢核
講義的 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 報表。
- 作業一(歐幾里德距離):設 x1,y1,x2,y2 四個變數,把距離存入 dist。提示:** '0.5' 或 sqrt( );用途是找異常值/相似度勾稽。答案見第 15 節。
- 作業二(Z-score):設 x、u(平均)、stdv(標準差),求 z = ( x - u ) / stdv。想想為什麼型別要用 DECFLOAT34 而不是 I。
- 作業三(字串反轉):禁用 STRING_REVERSE,把 'I-LOVE-ABAP' 變成 'PABA-EVOL-I'。提示:strlen( ) + DO n TIMES + substring( );注意 offset 從 0 起算,所以 pos = 11 - sy-index。
- 作業四(民國年):把 '20220315' 顯示成 '1110315'。提示:C→I 轉型、減 1911、再補零成 3 碼。踩雷點:直接用 N 型別相減會失去型別長度控制。
- 作業五(本月/上月/去年同期):上網找出標準函數並實作。答案:RP_CALC_DATE_IN_INTERVAL(signum '+'/'-')、HR_JP_MONTH_BEGIN_END_DATE、RP_LAST_DAY_OF_MONTHS。
- 作業六(字串逐行輸出):輸入 80 碼、逗號分隔的字串,一列一列輸出。提示:SPLIT ... INTO TABLE + CONDENSE。
- 作業七(排序輸出):同作業六,但存入內表並遞增排序。提示:SORT lt ASCENDING.;別忘了先 DELETE lt WHERE table_line IS INITIAL.。
- 作業八(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 上每次結果可能不同。
- 作業九(學會線上搜尋):練習用 SE37 直接執行函數、用 SE38 輸入 DEMO* 按 F4 找 SAP 示範程式、用 ABAPDOCU 查語法樹。
- 作業十(標準差複習):用 Excel 對 10,12,23,23,16,23,21,16 算 =AVERAGE( )、=VAR.P( )、=STDEV.P( )、=SQRT(144/8),再用 ABAP 寫出同樣結果(見第 15 節)。記住:變異數就是標準差還沒開根號的值。
- 自我檢核 A:DATA(lv_x) = 5 / 2. 的結果是多少?為什麼?要得到 2.5 該怎麼改?
- 自我檢核 B:-5 MOD 3 在 ABAP 是多少?跟 C / Java 有何不同?
- 自我檢核 C:日圓 1000 元在 BSEG-WRBTR 裡存的是什麼數字?要用哪個 BAPI 換算?表 TCURX 的作用是什麼?
- 自我檢核 D:CHECK 寫在 LOOP 裡跟寫在 FORM 裡,行為有什麼不同?
- 自我檢核 E:strlen( ) 對 TYPE c LENGTH 20 但只放 13 個字的變數回傳多少?換成 TYPE string 呢?
- 自我檢核 F:SY-TABIX 在 HASHED 內表的 LOOP 裡是多少?為什麼?
- 自我檢核 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 以後的正式系統。
1. 內表是什麼:記憶體中的臨時表
內表(internal table)是程式執行期間在記憶體中建立的一張臨時表。你可以對它做插入、修改、刪除、查詢,程式跑完它就自動被釋放,不必像 T-SQL 的 #table 還要記得 DROP(雖然 T-SQL 現在也有 DECLARE @table 了)。
老師的第一句話:掌握內表是成為 ABAPer 的關鍵一步。除了寫程式之外,小到 SELECT-OPTIONS、大到 Function Module 的 TABLES 參數、ALV 的資料來源,到處都是 internal table 的影子。
內表、表格列(table line)與工作區(work area)
- 內表:多筆資料的集合(相當於 SQL 的 table)。
- table line:內表中的一筆記錄,很容易跟工作區搞混。
- 工作區 work area:內表是「按行存取」的,必須有一塊區域當作與內表互相傳輸的介面。從內表讀資料時,被定址的那一行內容會覆蓋工作區;要寫資料進內表時,必須先把資料填進工作區。
換句話說:工作區=一列的結構,內表=很多列。在現代 ABAP 中,工作區往往不需要事先宣告,直接用 INTO DATA(ls_x) 或 ASSIGNING FIELD-SYMBOL(<ls_x>) 在迴圈裡就地產生。
2. 三種內表型別與 KEY 設計(STANDARD / SORTED / HASHED)
內表共三種類型,差別在「怎麼找到那一列」:
| 型別 | 關鍵字 | 索引 | 主鍵 | 存取複雜度 | 適用時機 |
|---|---|---|---|---|---|
| 標準表 | STANDARD TABLE | 有主表索引(可用 INDEX / sy-tabix) | Non-unique | 循序 O(n);有排序+BINARY SEARCH 才 O(log n) | 一般報表、資料量小(<1 萬筆)。簡單環境用它就夠了 |
| 排序表 | SORTED TABLE | 有邏輯索引,依主鍵昇冪自動排序後存放 | UNIQUE 或 NON-UNIQUE | O(log n)(二分搜尋,系統自動做) | 邊新增邊排序、需要頻繁 READ 的中量資料(>1 萬筆) |
| 雜湊表 | HASHED TABLE | 沒有主表索引,只能用鍵值存取 | 必須 UNIQUE | O(1),定址時間與筆數無關 | 巨量資料(>10 萬筆)的鍵值查表、對照檔 |
SAP 官方對 hashed table 的定義:資料列依 hash 演算法存放、以唯一 hash key 存取;沒有主表索引,但可以另外掛 secondary table index(透過 secondary key)。
主鍵、次鍵與 USING KEY
- Primary key:WITH [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.
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.
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.
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.
把資料庫資料直接灌進內表,才是最常用的「新增」:
" 一次讀進內表(現代寫法:明列欄位 + 內嵌宣告 + 逸出字元 @)
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%'.
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.
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.
*&---------------------------------------------------------------------*
*& 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.
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-symbol | READ 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.
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 ... 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.
10. 排序、去重、計數、清空與動態 SORT
SORT itab [BY f1 [ASCENDING|DESCENDING] f2 ...] [AS TEXT] [STABLE].
- 不寫 BY 就依「標準鍵」排序,通常不是你要的,請一律明寫 BY。
- AS TEXT:依語言的字典順序排(處理中文/重音字元時用)。
- STABLE:保持相同鍵值列的原有相對順序(做多層排序時很重要)。
- SORT 只能用在 STANDARD TABLE;SORTED 本來就有序、HASHED 沒有順序。
*&---------------------------------------------------------------------*
*& 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.
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).
13. ABAP SQL 概論:Native SQL、Open SQL 與 ABAP SQL
| 種類 | 特性 | 風險 / 限制 |
|---|---|---|
| Native SQL | 直接下該資料庫的 SQL,能做 Open SQL 做不到的事(預儲程序、游標、DB 專屬函數) | 綁死資料庫、不做 client 處理、不走表緩衝、繞過 SAP 授權與一致性檢查,有安全與資料一致性風險 |
| Open SQL | SAP 定義的資料庫無關標準,執行期由 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.
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 | 自行控制 MANDT | 7.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.
子查詢(subquery):IN、EXISTS、以及純量子查詢。講義 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.
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.
視窗函數 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.
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.
① 空表災難:驅動內表是空的時候,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 的替代方案(優先順序由上而下):
- JOIN:兩張表本來就有關聯,直接 JOIN,一次往返。
- 子查詢 IN / EXISTS:邏輯是「存在性判斷」時最乾淨。
- CTE(WITH):中間結果多、邏輯分層時可讀性最好。
- SELECT ... FROM @itab(7.52+):把已在記憶體的內表當成資料來源,直接下 SQL。
- 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 SINGLE | SELECT ... UP TO 1 ROWS | |
|---|---|---|
| 條件要求 | 必須指定全部主鍵才保證取到唯一那筆;缺任何一個主鍵就不該用 | 任意條件、甚至不給條件都可以 |
| 可否 ORDER BY | 不可以 | 可以,因此可以「取最新/最大的那一筆」 |
| 回傳 | 一個工作區 | 可 INTO 工作區或內表 |
| 沒給齊主鍵時 | 回傳「任意一筆」,結果不保證穩定 | 加 ORDER BY 就穩定 |
這正是老師 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.
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 WAIT 與 COMMIT 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).
21. Native SQL、ADBC、AMDP 與 CDS 的定位
當 ABAP SQL 做不到(要用 HANA 專屬函數、要呼叫預儲程序、要跑游標)時,才往下走。
EXEC SQL(已過時,只為讀懂舊程式)
講義 02-09 用 EXEC SQL ... ENDEXEC 示範查詢、建立 Oracle 函數與游標。三個語法規則要記得:語句結尾不加句點、區塊內不可有註解(星號與雙引號都不行)、參數用冒號 :var 而不是問號;欄位與條件都用逗號分開。
*&---------------------------------------------------------------------*
*& 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 | 正確,永遠成立 |
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 | 用途 |
|---|---|
| ST05 | SQL Trace:看程式實際下了哪些 SQL、跑幾次、花多久(最常用) |
| SAT | ABAP Runtime Analysis,取代舊的 SE30;看哪一行 ABAP 最花時間 |
| SDBE | Display Explain:把一段 SQL 的執行計畫叫出來看,判斷有沒有走索引(講義此處僅有標題,依 SAP 標準補述) |
| SQLM / SWLT | SQL 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_TIMEZONE | ABAP 端逐一計算(用了會啟動 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 }|.
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.
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.
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.
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. | 推薦 |
| 11 | SQL 灌入 | SELECT ... INTO TABLE itab. | SELECT ... INTO TABLE @DATA(itab). |
| 12 | SQL 追加 | 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-SQL | ABAP SQL | ABAP 內表 |
|---|---|---|---|
| 新增 | INSERT | INSERT | INSERT / COLLECT / APPEND / it_a = it_b |
| 修改 | UPDATE | UPDATE | MODIFY |
| 刪除 | DELETE | DELETE | DELETE |
| 查詢 | SELECT | SELECT | READ TABLE / LOOP AT / itab[ ] |
| 增改(Upsert) | MERGE | MODIFY | COLLECT(數字累加) |
| 清空 | TRUNCATE TABLE | DELETE FROM dbtab. | CLEAR / CLEAR it[] / REFRESH / FREE |
| 筆數 | SELECT COUNT(*) | SELECT COUNT( * ) | lines( itab ) / DESCRIBE |
| 判斷 | IF ... ELSE | IF / CHECK | IF / CHECK / line_exists( ) |
| 指令回傳 | @@ERROR | SY-SUBRC | SY-SUBRC |
| 當前筆數 / 列號 | @@ROWCOUNT | SY-DBCNT | SY-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 ~ _23 | SELECT SINGLE/彙總函數/JOIN/UPDATE/INSERT+DELETE/MODIFY | 第 13~19 節 |
| 第 64 個練習 | Z202202_64 | Native SQL EXEC SQL PERFORMING | 第 21 節 |
| 第 65 個練習 | Z202202_65 | DATA(@it):新版 740 內表精簡寫法、內表當資料來源 | 第 17 節 |
| 第 66 個練習 | Z202202_66 | ACDOCA GROUP BY 加總 | 第 16 節 |
| 期中題 | Z00_MIDTERM | 歐幾里德距離+Z-score+最適伙伴 | 第 24 節 |
| 效能測試 | Z00_03 | HASHED vs SORTED 讀取效能(用 SE30/SAT 測) | 第 2 節 |
| OPEN SQL 20 題 | ZOPENSQL01 ~ ZOPENSQL20 | UP 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_DEMO | sum / mean / stddev / z-score,進階套用到 ACDOCA 740300 | 第 25 節 |
| 動態排序 | ZSORT | SORT BY (欄位變數)、VALUE 建表、p_sort+5 偏移 | 第 10 節 |
| 雙迴圈 | ZLOOP | 巢狀 LOOP、AT 事件、call by value/reference、header line | 第 9~12 節 |
SQL 終極大考驗:採購全鏈欄位(講義 09-1)
老師列了 26 個欄位當作綜合考題,要用一段 SQL 把採購從抬頭到收貨串起來:
| # | 欄位 | 說明 | # | 欄位 | 說明 |
|---|---|---|---|---|---|
| 01 | EKKO-EBELN | 採購單號 | 14 | MARA-MTART | 物料類型 |
| 02 | EKKO-BUKRS | 公司代碼 | 15 | MAKT-MAKTX | 物料名稱 |
| 03 | EKKO-BSTYP | 採購類別 | 16 | EKPO-INFNR | 價格條件(採購資訊記錄) |
| 04 | EKKO-BSART | 採購單別 | 17 | EINA-ERDAT | 條件日期 |
| 05 | EKKO-LIFNR | 採購廠商 | 18 | EKPO-WERKS | 工廠代碼 |
| 06 | LFA1-NAME1 | 廠商名稱 | 19 | EKPO-LGORT | 儲存位置 |
| 07 | EKKO-AEDAT | 建檔日期 | 20 | EKPO-MATKL | 物料群組 |
| 08 | EKKO-ERNAM | 建檔人員 | 21 | EKPO-MENGE | 採購數量 |
| 09 | EKKO-EKORG | 採購組織 | 22 | EKPO-NETPR | 採購單價 |
| 10 | EKKO-EKGRP | 採購群組 | 23 | EKPO-NETWR | 採購金額 |
| 11 | EKKO-WAERS | 採購幣別 | 24 | EKBE-GJAHR | 物料年度 |
| 12 | EKPO-EBELP | 採購項次 | 25 | EKBE-BELNR | 物料憑證號 |
| 13 | EKPO-MATNR | 物料編號 | 26 | EKBE-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。
更重要的是:在 S/4HANA 上,這類報表請改讀 ACDOCA(一張表就有檔頭+檔身欄位,不必 JOIN,而且欄位齊全、效能好得多)。BSEG/BKPF 仍在,但 JOIN 大量資料會很慢。
練習與自我檢核
- (作業 1・內表)針對 20190101~20191231 的接單資料做 Group by 分析:輸出「客戶編號、下單金額」。提示:用 COLLECT 練一次,再用 SQL GROUP BY kunnr 寫一次,比較兩者效能與程式長度。
- (作業 2・內表)同期間做 RFM 分析:輸出「客戶編號、累計下單金額、累計下單筆數、最近下單日、首次下單日」。提示:一句 SQL 即可 —— SELECT kunnr, SUM( netwr ), COUNT( * ), MAX( audat ), MIN( audat ) ... GROUP BY kunnr。
- (作業 3・內表)建立 Euclidean distance 的計算結果:輸出「客戶編號、累計下單金額、累計下單筆數、金額距離、筆數距離」。提示:LOOP + MODIFY,或用 field-symbol 就地寫回;先做 Z-score 標準化,否則金額會壓過筆數。
- (作業 4・內表)請證明「READ TABLE 只讀符合條件的第一筆」。提示:造一張有三筆相同 key 的內表,READ 後印出 sy-tabix;再用 LOOP ... WHERE 對照筆數。當做要教別人,把程式改得讓人一看就信。
- (作業 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.
- 三種內表型別各自的 Big O 是多少?為什麼 HASHED TABLE 不能用 READ TABLE ... INDEX?答:O(n)/O(log n)/O(1);HASHED 沒有主表索引。
- DELETE ADJACENT DUPLICATES FROM itab COMPARING f1 f2. 執行前一定要先做什麼?不做會怎樣?答:先 SORT BY f1 f2;不做只會刪掉「剛好相鄰」的重複列,資料看起來少了一些又沒去乾淨。
- FOR ALL ENTRIES 的驅動內表如果是空的,會發生什麼事?正確的防呆寫法是什麼?答:整段 WHERE(含其他條件)被忽略,把整張表撈回來;一律先 IF lt_x IS NOT INITIAL.
- SELECT SINGLE 與 SELECT ... UP TO 1 ROWS 差在哪?要「取最新一筆」該用哪一個、為什麼?答:SINGLE 要主鍵給齊、不能 ORDER BY;取最新一筆必須用 UP TO 1 ROWS + ORDER BY,因為 HANA 沒有排序保證。
- AT NEW / AT END OF 有哪三個前提或副作用?答:必須先排序;比對該欄位「及其左邊所有欄位」,所以結構欄位順序有影響;區塊內右側欄位會被填成 * 或 0。
- 把下面這段舊程式改成 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.
- 什麼情況該建 CDS View、什麼情況直接在程式裡 JOIN?答:邏輯會被多支程式重複使用、要給 Fiori/OData 用 → CDS;一次性、條件隨選擇畫面變動 → 程式內 JOIN。
- S/4HANA 上查 ACDOCA 必帶哪些條件?為什麼不該再用 BSEG+BKPF 做大報表?答:RLDNR、RBUKRS、GJAHR(再加期間或傳票號);ACDOCA 是 Universal Journal,一張表就有全部明細,不必 JOIN。
- EXEC SQL 為什麼不該再用?替代方案是什麼?答:已 obsolete,且不做 client 處理、不走表緩衝、不檢查授權;改用 ADBC(CL_SQL_STATEMENT)、AMDP 或 CDS。
- 把「LOOP 內對每一列下 SELECT」的程式改寫成三種更快的版本,並說明各自適用時機。答:JOIN(關聯明確)/子查詢 EXISTS(存在性判斷)/先撈進內表再用 SORTED KEY 對照(資料要重複使用)。
- 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 秒內生一支能看的報表,也能在半天內生一支能編輯、能存檔、能鑽取、能匯出的正式報表。
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 ALV | GRID ALV(OOALV) | SALV(OOALV) | Fiori(S/4HANA) |
|---|---|---|---|---|
| 年代 | 1996 | 2000 | 2004 | 2015 以後 |
| 版本 | SAP R/3 3.11 | SAP R/3 4.5 | SAP NetWeaver | S/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 節 |
(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 的三步驟永遠一樣:
- 備資料:SELECT 進內表。
- factory:cl_salv_table=>factory( ) 把內表交給 ALV,拿回一個 lo_alv 參考。
- display:lo_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.
• 不帶 r_container → 全螢幕 ALV(最常用)。
• 帶 r_container = lo_container(CL_GUI_CUSTOM_CONTAINER)+ container_name → 嵌在自畫的 Screen 裡。
• 帶 CL_GUI_DOCKING_CONTAINER → 停靠在選擇畫面旁邊,做「不用另開畫面」的報表。
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_LAYOUT | Layout 變式(儲存/預設/限制) |
| get_selections( ) | CL_SALV_SELECTIONS | 選取模式、取得選取行/選取格 |
| get_event( ) | CL_SALV_EVENTS_TABLE | 事件:ADDED_FUNCTION、DOUBLE_CLICK、LINK_CLICK |
| get_functional_settings( ) | CL_SALV_FUNCTIONAL_SETTINGS | Tooltip、F4 說明 |
| top_of_page / end_of_page | CL_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.
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_DISPLAY 與 REUSE_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、F4 | DDIC 參照 |
| ROLLNAME | 資料元素(F1 說明) | 無 DDIC 參照時使用 |
| DATATYPE / INTTYPE | DDIC 資料型別(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' ).
5. 紅綠燈(Exception / Lights)與圖示欄位
紅綠燈有兩種做法,講義兩種都示範了:
- Exception 欄位:內表加一個 CHAR1(或 CHAR4)欄位,值填 1 = 紅、2 = 黃、3 = 綠,再告訴 ALV 這欄是燈號欄。
傳統:layout-lights_fieldname = 'LIGHTS'(LVC:excp_fname)。
SALV:lo_cols->set_exception_column( 'LIGHT' )。 - 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 ).
另外,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
整欄顏色 → 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.
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_GET | INITIALIZATION | 取得預設變式,填進選擇畫面的 P_VARI |
| REUSE_ALV_VARIANT_F4 | AT SELECTION-SCREEN ON VALUE-REQUEST | 提供 F4 選變式 |
| REUSE_ALV_VARIANT_EXISTENCE | AT 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.
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_EXIT | ALV 顯示前最後一個掛勾 | 進階 |
| 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_MODIFY | report/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.
9-1. OO ALV 的事件對照
| 要做的事 | CL_SALV_TABLE | CL_GUI_ALV_GRID |
|---|---|---|
| 自訂按鈕被按 | ADDED_FUNCTION | USER_COMMAND(+ TOOLBAR 加按鈕) |
| 雙擊某一格 | DOUBLE_CLICK | DOUBLE_CLICK |
| 單擊熱點 | LINK_CLICK | HOTSPOT_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.
① 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 為樣板複製。步驟如下(依講義步驟整理):
- SE41 Menu Painter → 程式名輸入 SAPLSALV、Status 輸入 STANDARD(或 STANDARD_FULLSCREEN)。
- 功能表 Status → 複製,目標程式填你自己的 Z 程式、Status 名稱自取(例如 ZSTATUS)。
- 在 Application Toolbar(應用工具列)新增按鈕,功能碼取 ZADD、ZSAV、ZCAS 之類的 Z 開頭代碼,填上圖示與 Quickinfo。建議放 Application Bar,不要塞進 Menu Bar——使用者根本不會去點選單。
- 存檔後一定要啟動(Activate)。物件變多時,可用 SE80 的物件清單一次全選啟動,或用 SE38 執行 RS_WORKING_AREA_ACTIVATE/在 SE80 對 Package 按「Activate All」。
- 在程式的 PF_STATUS_SET 事件裡 SET PF-STATUS 'ZSTATUS' EXCLUDING rt_extab.
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。做法(依講義步驟整理):
- 執行 OAER,Class Name 填 PICTURES、Class Type 填 OT、Object Key 填你自取的名稱(講義用 ZNCU、ZCERPS43)。
- 進入主控畫面後選「新增」,在視窗下方的 Standard Doc. Type 一定要選 SCREEN(若要列印用則選 PRINT)。
- 選擇本機圖檔上傳。建立成功後,重新進入該物件點 Attachment 就能看到 LOGO。
- 程式中 REUSE_ALV_COMMENTARY_WRITE 的 I_LOGO 填該 Object Key。
12. Drilldown:從報表跳到標準交易
ALV 報表的價值一半來自「雙擊能跳過去看單據」。SAP 的作法是先 SET PARAMETER ID 把值塞進使用者的 SPA/GPA 記憶區,再 CALL TRANSACTION ... AND SKIP FIRST SCREEN;或直接呼叫該模組提供的顯示 FM。
| 要跳到 | Parameter ID | 作法 |
|---|---|---|
| 採購單 ME23N | BES | SET PARAMETER + CALL TRANSACTION,或 FM ME_DISPLAY_PURCHASE_DOCUMENT |
| 銷售訂單 VA03 | AUN | SET PARAMETER + CALL TRANSACTION |
| 交貨單 VL03N | VL | SET PARAMETER + CALL TRANSACTION |
| 物料 MM03 | MAT(+WRK 工廠、LIF 廠商、MXX 視圖) | SET PARAMETER + CALL TRANSACTION,或 FM MMPUR_MATERIAL_DISPLAY |
| 會計傳票 FB03 | BLN + BUK + GJR | SET PARAMETER + CALL TRANSACTION |
| 物料憑證 MIGO | MBN + MJA + BUK | FM 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.
A 工作計畫|B 會計|C 分類|D 物料需求規劃|E 採購|F 生產資源/工具|G 成本核算|K 基本資料|L 儲存|P 預測|Q 品質管制|S 倉庫管理|V 銷售|X 工廠庫存|Z 儲存位置庫存。
例:SET PARAMETER ID 'MXX' FIELD 'E'. 就會直接開採購視圖。
各模組的顯示 FM 多半集中在幾個函數群組:CK50、EDI5、MEEXT 等。
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.
S/4HANA 上要做樹狀報表,建議改用 CL_GUI_ALV_TREE,或乾脆用 CDS 的 parent-child association 交給 Fiori 呈現。
SAP 內建一整套 ALV 示範程式,都在 SE38 直接執行、SE80 直接讀原始碼,是最好的自學教材:
| 程式 | 示範內容 | 用到的關鍵 FM |
|---|---|---|
| BALVSD01 | 飛機模型 Simple list:變式、事件、抬頭、群組、紅綠燈、勾選欄,全部參數化成 CheckBox | REUSE_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 |
| BALVSD04 | Merged list:多表 JOIN 後輸出,示範自訂欄位 SEATSFREE(DDIC 沒有的欄位) | REUSE_ALV_LIST_DISPLAY |
| BALVSD06 | simple 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_DUMMY | BALVSD11 用 SUBMIT 呼叫的示範被叫程式 | — |
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 ).
另外 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_excel、zcl_excel_worksheet、zcl_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.
• 物料憑證: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。
另外,講義用 LOOP 內逐筆 SELECT SINGLE 補廠商名稱/物料名稱,這是效能殺手(N+1 查詢)。正解:一次 JOIN 進來,或先把 key 收集起來一次 SELECT 進暫存內表,再用 line_exists( ) 對應。
16. 老師強調的注意事項與坑
- 在 ALV 中,所有要顯示的欄位都必須在對應內表中有欄位,欄位名不能寫錯。
- ABAP 字串大小寫很重要。單引號包起來的字串,呼叫系統方法時一律用大寫。
- 傳給 ALV 的欄位名稱必須大寫,否則資料不會顯示。
- 用 巨集(DEFINE)或副程式(FORM)可以大幅縮短欄位目錄的程式碼並重複使用;現代寫法更推薦 VALUE #( ) 或抽成方法。
- FORM 內定義的變數在離開 FORM 後就被系統釋放、記憶體回收——不要期待它跨 FORM 存活。
- 程式一直出錯時,建議把關鍵字都改成大寫再試(講義 Z202202_25 的註記)。
- 有些 SLIS 結構無法用 SE11 直接查,要用 SE80 打開 type-pool SLIS 來看。
- ZEKET_43 的功能演進清單(可當作自己練習的檢核表):排序、抓供應商編號、加物料編號、TOP_OF_PAGE 抬頭與圖片、日期格式化、日期可修改、PF_STATUS_SET 功能表、USER_COMMAND 存檔、回寫 EKET、單位欄位、紅綠燈、LEAVE PROGRAM 退出、DO_SUM、用副程式改寫、兩次 INNER JOIN。
練習與自我檢核
- (講義題)把 VBAK、VBAP、EKKO、EKPO、EKET、LFA1、MAKT 這幾張表的關聯圖畫出來,標出主鍵與外鍵。提示:VBAK-VBELN ↔ VBAP-VBELN;EKKO-EBELN ↔ EKPO-EBELN ↔ EKET-EBELN+EBELP;MAKT 要加 SPRAS。
- (講義題,第 88 題)把 Z202202_24 改寫成採購訂單版報表,欄位為:採購編號、採購日期、廠商編號、物料編號、採購數量、採購單位、工廠代碼、儲存位置。若能再加上 Z202202_25 的特色(TOP_OF_PAGE 抬頭、LOGO、PF_STATUS 自訂按鈕、USER_COMMAND、排序),每個特色加 2 分。提示:資料來自 EKKO ⋈ EKPO;工廠 WERKS、儲位 LGORT 都在 EKPO。
- 把上題改用 CL_SALV_TABLE 重寫,要求:欄寬自動最佳化、依廠商小計、數量合計、雙擊採購單號跳 ME23N、可儲存 Layout 變式。
- 做一支「交期預警報表」:交期已過=紅燈、7 天內=黃燈、其餘=綠燈,並在燈號上加 Tooltip。提示:exception column 用 1/2/3;Tooltip 用 cl_salv_tooltip=>c_type_exception。
- ALV 上的數量欄位設了 do_sum = 'X' 卻加總不出來,列出至少三個可能原因。答案要點:沒綁 qfieldname/cfieldname;datatype 被指定成 CHAR;欄位設了 no_sum;資料型別本身不是 QUAN/CURR/數值。
- 使用者反映「我在 ALV 上改了數量,按存檔卻存到舊值」,請說出三個檢查點。答案要點:fieldcat 沒設 edit;沒有 register_edit_event/EDT_CLL_CB;存檔前沒呼叫 check_changed_data( )。
- 寫出 AT END OF 的三個使用前提,並說明為什麼 ALV 的自動小計通常比它更好。
- 解釋 I_SAVE 的四個值(' '、'U'、'X'、'A')分別代表什麼,以及 SALV 對應的 restrict_* 常數。
- 下載 CSV 給業務,對方回報中文全是亂碼。請寫出 gui_download 應該怎麼設參數,並說明為什麼背景作業不能用這個方法。
- 列出 ALV 17 個事件中「一定要會」的四個,並各寫一句話說明用途。
- 說明 RT_EXTAB 的作用,以及不把它傳給 SET PF-STATUS ... EXCLUDING 會發生什麼事。
- 在 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 函數,也知道為什麼老師說「新程式一律寫方法」。
1. 為什麼要模組化:五種手法與選用原則
模組化就是「把程式中一段原始碼獨立出來、封裝成一個特定功能」。講義列的四大好處:提高程式透明度、提高程式碼重用、簡化程式維護、方便程式呼叫。最典型的例子就是「取得每月最後一天」「把檔案上傳成內表」這種到處都要用的小功能。
| 手法 | 可見範圍 | 參數 | 可否 Debug | S/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):傳進去、改完再傳回來。
講義開頭那段 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.
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 p | Call by reference(傳址) | 語意上是「只讀」,但 ABAP 不會擋你改,改了外面真的會變 |
USING VALUE(p) | Call by value(傳值) | 副程式內改動不影響外部變數 |
CHANGING p | Call by reference | 明確表示「要改」 |
CHANGING VALUE(p) | Call by value and result | 執行中不影響外部;ENDFORM 正常結束時才把值寫回 |
TABLES p | Call by reference,內表 | 已過時:參數會被當成帶表頭行(header line)的內表 |
ENDFORM 正常結束時才回寫。如果副程式中途遇到 MESSAGE ... TYPE 'E'、EXIT 離開程式或發生例外,回寫不會發生,呼叫端拿到的還是舊值。這種 bug 非常難查。FORM f TABLES it STRUCTURE mara. 語法檢查會出現「過時」警告。改用型別化的 USING/CHANGING。注意 FORM f USING it TYPE TABLE OF mara 是錯的 —— FORM 的參數只能用「已存在的型別名稱」或泛型型別(TYPE STANDARD TABLE、TYPE INDEX TABLE、TYPE 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.
FORM/PERFORM 自 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,但方式三的 a、b 保持原值;方式二的 c 仍是 0。
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 溫床。
遞迴示範(副程式與方法都適用,這裡示範方法版):
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 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 Group | SE37 → 功能表 Goto → Function Groups → Create/Change/Display/Delete Group | 也可以在 SE80 直接建(建議) |
| 顯示 Function Group 結構 | SE80 物件瀏覽器 | 可以看到底下所有 Include、FM、畫面 |
| 建立/修改 Function Module | SE37 | 輸入 FM 名稱 → 選 Function Group → 填描述 |
| 測試 FM | SE37 → 測試/執行(F8) | 不用寫呼叫程式就能測 |
Function Group 產生的 Include 結構(假設 Group 名為 ZFG_SD,主程式為 SAPLZFG_SD):
| Include | 內容 |
|---|---|
LZFG_SDTOP | FUNCTION-POOL 敘述與全域資料宣告(本 Group 內所有 FM 共用) |
LZFG_SDUXX | 系統自動產生,內含 INCLUDE LZFG_SDU01. INCLUDE LZFG_SDU02. ... |
LZFG_SDU01、U02… | 每個 FM 的實作原始碼(一個 FM 一個 Uxx) |
LZFG_SDF01 | 自行建立,放 Group 內共用的 FORM/本地類別 |
函數處理類型(Processing Type),在屬性頁籤選:
- Normal Function Module(一般函數模組):只能在本系統使用。
- Remote-Enabled Module(遠端模組,RFC):可被其他 SAP 或非 SAP 系統呼叫,呼叫時要指定
DESTINATION,目的地在 SM59 設定。 - Update Module(更新函數):用於資料庫資料更新,講義註「常用於系統導入之初的大量數據更新」。
介面元素(SE37 的五個頁籤):
| 頁籤 | 語意 | 是否可選 |
|---|---|---|
| Import | 呼叫時把值傳進 FM | 可勾 Optional;可設預設值(Default) |
| Export | FM 把結果送出 | 永遠是 Optional |
| Changing | 傳進去、改完傳回 | 可勾 Optional;可設預設值 |
| Tables | 用內表傳參,行為同 Changing | 已過時(見下方警告) |
| Exceptions | 錯誤情況;呼叫端用 sy-subrc 檢查 | — |
VBAP_T)。老師的提醒是:用表型別(Table Type)取代 TABLES 頁籤,才不會被系統標過時。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.
第二個案例 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_PARAMETER/GET_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.
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 同步 | DESTINATION | SM59(連線測試) |
| aRFC 非同步 | STARTING NEW TASK | SM50/SM66 |
| tRFC 交易式 | IN BACKGROUND TASK | SM58 |
| qRFC 佇列式 | IN BACKGROUND UNIT | SMQ1(出)/SMQ2(入) |
SYSTEM_FAILURE 與 COMMUNICATION_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 成功後才執行,獨立 LUW | V2 失敗不影響 V1;適合統計、次要資料 |
| Collective run | Collective 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.
COMMIT WORK AND WAIT 與 COMMIT WORK 的差別是後續程式能不能立刻讀到剛寫的資料 —— 呼叫 BAPI 後馬上要 SELECT 驗證,一定要加
AND WAIT。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.
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 ) ).
*_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 副程式/本地類別實作 | 報表主邏輯 |
| O01 | MODULE ... OUTPUT(PBO,Process Before Output) | Dialog 程式 |
| I01 | MODULE ... INPUT(PAI,Process After Input) | Dialog 程式 |
| E01 | AT SELECTION-SCREEN/事件區塊 | 選擇畫面事件 |
| C01 / CL1 | 本地類別定義(CLASS DEFINITION) | OO 化之後常用 |
講義 7-2 的 include 案例,改寫成 S/4HANA 版本(原文用 OCCURS 0 WITH HEADER LINE + REUSE_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_MERGE + REUSE_ALV_GRID_DISPLAY 這一套,在 S/4HANA 雖然還能跑,但:WITH HEADER LINE 與 OCCURS 0 已過時且 OO 環境禁用;REUSE_ALV_* 系列是 Function ALV,未列入 ABAP Cloud 的釋出 API。新程式一律用 CL_SALV_TABLE。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 |
| 可見範圍 | 只有本程式 | 全系統 |
| 取代對象 | FORM | FUNCTION 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.
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 時代做不到的事。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_ZERODIVIDE、CX_SY_CONVERSION_NO_NUMBER) |
CX_NO_CHECK | 不必宣告,會自動往上層傳播 | 無法在區域處理的系統性錯誤(如 CX_SY_NO_HANDLER、資源不足) |
三者都繼承自 CX_ROOT;CX_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.
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_BCS(cl_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.
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).
sy-datum/sy-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 就能直接看。
- 進入交易碼(例如 VA03)。
- 在指令欄輸入 /H → Enter,畫面出現「Debugging switched on」。
- 再按 Enter 執行下一個動作,就會進入 Debugger。
- 按 F9(設中斷點)→ 選 Breakpoint at Statement → 輸入 SELECT。
- 按 F8 執行,每遇到一句 SELECT 就停下來,看它讀哪一張表。
- 用 F5(單步)、F6(略過副程式)、F7(執行完目前副程式返回)、F8(繼續)搭配追蹤。
- 查完記得取消中斷點(Breakpoints 頁籤 → 刪除),否則之後每支程式都會停。
Statement(如 SELECT、CALL FUNCTION、MESSAGE、AUTHORITY-CHECK)/Function Module(追某支 FM 被誰呼叫)/Method/Subroutine/Exception(抓 dump 的來源)。
Watchpoint(監視點):對某個變數設條件,「當 sy-subrc <> 0」或「當 lv_matnr = '123'」時才停;若不指定條件,則該變數值一被改變就停下來。這是抓「值被誰改掉了」的唯一有效手段。
Debug 也可以事後補勾:進了 Debugger 才決定要不要進某段程式。
兩種 Debugger:Classic Debugger(同一個視窗,功能少)與 New Debugger(獨立視窗、多頁籤、可看物件與內表全貌)—— 在 Debugger 內 Settings 可切換。
提示: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)就是「訊息的模組化」。
| 類型 | 名稱 | 行為 | 顯示位置 |
|---|---|---|---|
| I | Information | 使用者確認後,程式從 MESSAGE 敘述之後繼續 | 對話框 |
| S | Status | 程式正常繼續 | 下一個畫面的狀態列 |
| W | Warning | 視程式情境,出現錯誤對話框、程式停止或終止,使用者須採取行動 | 狀態列 |
| E | Error | 視程式情境,程式停止或終止,使用者須採取行動 | 狀態列或對話框 |
| A | Termination | 使用者確認後程式終止,控制權回到上層區域功能表 | 對話框 |
| X | Exit | 程式以 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.
START-OF-SELECTION 中它會終止程式並回選擇畫面;在 AT SELECTION-SCREEN 中它會停在該欄位讓使用者改;在迴圈中用 E 會直接中斷整批處理 —— 批次程式請改用「收進 BAPIRET2 內表,跑完再一次顯示」的做法。在 Update FM 或 RFC FM 內用 E 更危險(見第 8 節)。
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.
建議做法:把每次寫過的通用邏輯(日期計算、檔案上下載、訊息收集、ALV 顯示、Excel 匯出)整理成自己的 Function Group(例如 Z_FG_000)或全域類別(ZCL_UTIL_*),並且寫上註解與範例。三年後你會發現,別人還在複製貼上,你已經有一整套工具箱。這正是「模組化」對個人職涯的意義。
練習與自我檢核
- 第 28 個練習:建立 Function Group
Z_FG_000與 Function ModuleZFM_CASPER_VBAP,輸入I_VBELN,輸出E_VBAK(結構)與T_VBAP(表型別),找不到時 RAISEVBAK_NOT_EXIST。再寫報表Z202202_28呼叫它。
檢核點:Function Group 與 FM 都要 ACTIVE;輸入 40 要能用 ALPHA 轉成 0000000040;EXCEPTIONS 一定要補 OTHERS。 - 建立
ZFM_GET_EBELP:輸入I_EBELN+I_EBELP,回傳最近一筆 EKKO/EKPO/EKES。
檢核點:「最近一筆」一定要 ORDER BY;sy-subrc 要在 SELECT 之後立刻檢查,不要被 LOOP 覆寫。 - 寫三支小程式(或一支程式三個 FORM)分別示範 call by reference、call by value、call by value and return result,用 WRITE 印出主程式與副程式中的變數值,說明為什麼結果不同。
答案要點:VALUE( ) 是影本;CHANGING VALUE( ) 在 ENDFORM 才回寫。 - 05-1:用 SE80 打開
SAPMV45A(VA03 主程式),找出 Macros 節點,列出三個 SAP 標準用的巨集名稱與用途。 - 把 ALV Field Catalog 的建立用
DEFINE add_col寫一次,再用VALUE lvc_t_fcat( )寫一次,比較行數與可讀性。
提醒:講義 05-3 的教訓 —— 少放一個 SORT 用到的欄位就 DUMP,且巨集無法 debug。 - 7-3:把
Z202202_26改成 include 模式:Z00_26_TOP(宣告)+Z00_26_FORM(副程式),主程式依序PERFORM GET_DATA / LAYOUT / FIELDCAT / EVENT / DISPLAY_ALV。進階:再把五個 FORM 改成本地類別方法。 - 第 6 節作業:用
/H+ F9(Breakpoint at Statement: SELECT)追出 VA03 讀了哪些資料表,至少列出 8 張。 - 09-3:用 SE91 建訊息類別
ZMESSAGE,把Z202202_26的輸出改成MESSAGE,並示範 I/S/W/E/A 五種類型的行為差異。 - 10-6:試用
RFC_READ_TABLE讀 MARA 的前 10 筆(注意 QUERY_TABLE、DELIMITER、FIELDS、OPTIONS 四個參數的用法,以及 512 字元的回傳限制)。 - 10-7:試用
BAPI_MATERIAL_GET_ALL讀一個物料的完整視圖,並比較它與直接 SELECT MARA/MARC 的差異(權限檢查、單位換算、多語系描述)。 - 把第 1 題的
ZFM_CASPER_VBAP重構成 SE24 全域類別ZCL_SD_ORDER:用 constructor 帶入 VBELN、用 RETURNING 方法回傳表頭與明細、用自建ZCX_SD_ORDER_NOT_FOUND(繼承 CX_STATIC_CHECK)取代 RAISE。
檢核點:呼叫端改用 TRY/CATCH;例外要能帶出 SE91 的訊息文字。 - 思考題:
CX_STATIC_CHECK、CX_DYNAMIC_CHECK、CX_NO_CHECK三者差在哪?自建業務例外應該繼承哪一個?為什麼?
答案要點:STATIC_CHECK 必須宣告在 RAISING、語法檢查會強迫呼叫端面對它,最適合業務錯誤。 - 思考題:為什麼 S/4HANA 到今天還不能完全廢掉 Function Module?
答案要點:RFC 協定只認 FM、Update Task(V1/V2)只能是 FM、部分 BAdI/Enhancement 的介面仍是 FM、BAPI 本身就是 FM。 - 第 11 節:規劃你自己的工具箱 —— 列出五個你已經重複寫過兩次以上的邏輯,把它們設計成
ZCL_UTIL_*的方法簽章(參數名稱、型別、例外)。
第 3-1 章 選擇畫面:PARAMETERS / SELECT-OPTIONS / SELECTION-SCREEN 全解
這一章把「使用者怎麼把條件交給你的程式」講到底:單值 PARAMETERS、複選 SELECT-OPTIONS 的四欄內表(SIGN/OPTION/LOW/HIGH)、SELECTION-SCREEN 的版面排版、七個選擇畫面事件的觸發順序、動態顯示與隱藏欄位、自製 F4 輔助說明、變式(Variant)與背景執行、以及用 SUBMIT 把條件丟給另一支程式。學完之後,你能做出一支「條件精準、畫面好看、錯誤擋得住、可存成變式排程」的正式報表,也會避開老師在專案上真的踩過的坑——條件用區間(BT)和用列舉(EQ)答案會不一樣。
1. 選擇畫面在 SAP 的定位
螢幕(Screen)是 ABAP 設計最重要的工作之一。SAP 的單據、主資料維護幾乎都靠螢幕,一支程式可以包含多個螢幕。螢幕上的 Dialog 就是使用者和程式之間的各種互動:輸入資料、選菜單、按按鈕、單擊或雙擊清單項目。
但選擇畫面(Selection Screen)不需要你去 SE51 建 Screen Number——它是由原始碼中的宣告語句自動產生的「簡易型螢幕」。這是它最大的優點:
- 簡單方便:幾行宣告就有一個可用的輸入畫面,不必畫 Layout、不必寫 PBO/PAI 模組。
- 移植方便:畫面定義就在程式裡,跟著程式一起傳輸,不會漏傳 Screen。
- 自動型別檢查:使用者輸入的值會自動與欄位型別比對。
- 自動接上字典:用 DDIC 資料元素宣告,F1 直接顯示欄位文件、F4 直接叫出字典掛的搜尋說明。
- 多語系文字:選擇文字(Selection Texts)可以多語維護,執行時自動用使用者的登入語言顯示。
- 可存成變式(Variant):重複使用,也是背景排程的必要條件。
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 的語法。
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.
兩條路:(a) 直接宣告 TYPE mara-matnr,字典上掛的搜尋說明 MAT1 會自動生效,按 F4 就有;(b) 若要自訂清單(例如只列本廠有庫存的料號),走第 14 節的 F4IF_INT_TABLE_VALUE_REQUEST。
4. AS LISTBOX 怎麼灌值,MATCHCODE 的搜尋說明怎麼找
LISTBOX 若欄位的 Domain 有固定值(Fixed Values)或檢查表,值會自動帶出來;否則要自己用 VRM_SET_VALUES 灌,時機放在 INITIALIZATION 或 AT 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.
四條實用路線:
1. 在任一有該欄位的標準畫面上按 F1 → 技術資訊(Technical Information),畫面下方會列出「搜尋說明」;若 GUI 沒顯示,在說明視窗的設定裡把 Technical Information 打開。
2. SE11 開該欄位的資料元素 → 「更多特性 / Further Characteristics」頁籤 → 看 Search Help 欄位;或看它的檢查表(Check Table),檢查表本身就會產生隱式搜尋說明。
3. SE11 選 Search help → 名稱輸入 *USER* 按 F4 模糊搜尋。
4. SE84(Repository Information System)→ ABAP Dictionary → Search Helps,可用描述文字反查。
5. SELECT-OPTIONS:四欄內表 SIGN / OPTION / LOW / HIGH
PARAMETERS 只能給一個值,實務上使用者要的是「這批料號」「這段日期」「排除這幾張單」。這就是 SELECT-OPTIONS。
SELECT-OPTIONS sel FOR dobj. " dobj 必須是已宣告的資料物件或表欄位
執行後,使用者輸入的內容會傳到一個自動產生的內表,這個內表的結構永遠是這四欄:
| 欄位 | 值域 | 意義 |
|---|---|---|
| SIGN | I / E | I = Include 包含;E = Exclude 排除。不可以留空白 |
| OPTION | EQ 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 | 介於(含頭含尾) | BETWEEN | MATNR BETWEEN 'DXTR1000' AND 'DXTR2000' |
| NB | 不介於 | NOT BETWEEN | MATNR NOT BETWEEN ... |
| CP | 模式比對 | LIKE | MATNR LIKE 'DXTR1*'(* 任意字串、+ 任意單一字元、# 逸出) |
| NP | 模式不比對 | NOT LIKE | MATNR NOT LIKE 'DXTR1*' |
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.
・NO INTERVALS=拿掉「到(HIGH)」欄位 → 只能單值,但可以按多重選擇輸入很多列。
・NO-EXTENSION=拿掉右邊那顆多重選擇按鈕 → 只能一列,但那一列還是可以填區間。
兩個一起加,才等於「只能填一個單值」。
7. RANGES 與自建 range 的正確填法
SELECT-OPTIONS 同時做兩件事:畫面元素 + 四欄內表。如果你只要那個四欄內表、不要畫面(例如程式內部自己組條件),那就是 RANGES。
| SELECT-OPTIONS | RANGES(舊) | 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[].
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)的答案為什麼會不一樣
這是老師在真實專案上付出代價換來的經驗:同一批資料,條件用「區間」跟用「列舉」,跑出來的答案不一樣,而用列舉的那個才是對的。
原因一: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[].
| 條件類型 | SIGN | OPTION | 範例 | 老師評語 |
|---|---|---|---|---|
| 包含一整群 | 'I' | 'CP' | DXTR1* | 圈範圍用這個,比 BT 安全 |
| 逐筆排除 | 'E' | 'EQ' | DXTR1201 … DXTR1210 | 正解:一筆一筆點名 |
| 區間排除 | 'E' | 'BT' | DXTR1201 ~ DXTR1210 | 會抓錯:長度/字尾不同的會被誤殺 |
同一筆業務資料在系統裡可能存在多種「角度」(例如成本估算就同時有 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 LINE … END 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.
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.
另外:在選擇畫面上請讀 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。
| # | 事件 | 觸發時機 | 典型用途 |
|---|---|---|---|
| 1 | LOAD-OF-PROGRAM | 程式載入記憶體時(一次) | 建立單例物件、初始化全域常數 |
| 2 | INITIALIZATION | 選擇畫面顯示之前(一次) | 設預設值、灌 LISTBOX 值、設按鈕文字、預填 SELECT-OPTIONS |
| 3 | AT SELECTION-SCREEN OUTPUT | 畫面 PBO;每次顯示與每次按 Enter/F8 都會跑 | LOOP AT SCREEN 動態顯示/隱藏/變灰 |
| 4a | AT SELECTION-SCREEN ON VALUE-REQUEST FOR f | 使用者按 F4 | 自製搜尋說明 |
| 4b | AT SELECTION-SCREEN ON HELP-REQUEST FOR f | 使用者按 F1 | 自製欄位說明 |
| 5a | AT SELECTION-SCREEN ON f | PAI,逐一欄位,依宣告順序 | 單一欄位檢核(錯誤時只有該欄可輸入) |
| 5b | AT SELECTION-SCREEN ON END OF sel | 多重選擇彈窗關閉之後 | 檢核整個 SELECT-OPTIONS 內表 |
| 5c | AT SELECTION-SCREEN ON RADIOBUTTON GROUP g | 單選鈕群組整體檢核 | 群組層級驗證 |
| 5d | AT SELECTION-SCREEN ON BLOCK b | 該 BLOCK 內任一操作 | 跨欄位、同框架的關聯檢核 |
| 5e | AT SELECTION-SCREEN ON EXIT-COMMAND | 按 Back / Exit / Cancel(功能碼型別 E) | 離開前提醒「資料未儲存」 |
| 6 | AT SELECTION-SCREEN | PAI 的最後,整張畫面 | 跨欄位總檢核、處理按鈕功能碼 |
| 7 | START-OF-SELECTION | 按下執行(F8)之後 | 抓資料(GET_DATA) |
| 8 | GET <node> | 邏輯資料庫逐筆讀取 | 舊式 LDB 程式(MB52 等標準報表) |
| 9 | END-OF-SELECTION | 所有資料選取完成、輸出畫面尚未顯示之前 | 結果檢驗、呼叫 ALV 顯示 |
| 10 | TOP-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 | 不輸出 | 輸出 | 較少單獨使用 |
" 依 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.
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'.
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.
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.
1. 這個事件只在功能碼型別為 E(Back/Exit/Cancel)時才觸發,按 Enter 或 F8 不會進來。
2. 原範例呼叫 POPUP_TO_CONFIRM_DATA_LOSS 時把 ANSWER 註解掉了,沒有接使用者的回答,所以不管按什麼都一樣「看起來沒作用」。
3. 這個事件的定位是「離開前做善後」(提示、清記憶體、寫 log),不要把關鍵控制邏輯壓在這裡。
四個終止指令的差別(老師特別列出來對照,很容易考也很容易寫錯):
| 指令 | 可用於 | 行為 |
|---|---|---|
| STOP | INITIALIZATION、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,但即使在迴圈內也會退出本次紀錄(含子節點)的處理,直接讀下一筆 |
16. 第 34、35 個練習:SAP 記憶體與 ABAP 記憶體
兩種記憶體常被混為一談,作用範圍完全不同:
| SAP Memory(SPA/GPA) | ABAP Memory | |
|---|---|---|
| 指令 | SET / GET PARAMETER ID | EXPORT / 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.
第 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 ), '筆'.
另外:IMPORT 之後一定要檢查 sy-subrc;記憶體是空的時候 IMPORT 不會報錯,變數就保持原值,很容易誤判。
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 |
反過來,要讀取自己這支程式當下的選擇條件(做 log 或做標題),用 FM RS_REFRESH_FROM_SELECTOPTIONS。
18. 變式(Variant)與背景執行
把選擇畫面填好的一整組條件存起來重複使用,就是變式。這是老師說的「條件變式的善用技巧」,也是背景排程的前提。
- 在選擇畫面填好條件 → 點儲存(Save)圖示 → 輸入變式名稱與描述 → 儲存。
- 執行時:Goto → Variants → Get(或畫面上的「取得變式」按鈕)。
- 維護/刪除: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 = 使用者出口 |
背景執行:
- 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.
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.
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.
它的用途是:當你用 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)處理日期。
練習與自我檢核
- 第 34 練習:寫 Z202202_34_SET 與 Z202202_34_GET,用 SET/GET PARAMETER ID 在兩個 Session 之間傳值。
要點:先跑 SET 再跑 GET;參數 ID 建議登錄到 TPARA;驗證開新視窗(/o)也讀得到。 - 第 35 練習:用 EXPORT/IMPORT MEMORY ID + SUBMIT 在兩支程式之間傳值。
要點:改用具名寫法 p_flag = lv_data,兩邊變數名可以不同;IMPORT 後檢查 sy-subrc;用完 FREE MEMORY ID。 - 第 36 練習:把 CHECKBOX、RADIOBUTTON、LISTBOX、MATCHCODE、STRING+VISIBLE LENGTH、OBLIGATORY、MEMORY ID 全部做一次。
延伸:讓 P_MATNR 可以下拉選擇物料編號(先試 DDIC 型別的內建 F4,再試自製 F4)。 - Search Help 找名字:在 S/4HANA 上找出 USER_COMP 這個搜尋說明的四種方法(F1 技術資訊、SE11 資料元素、SE11 模糊搜尋、SE84)。
- 第 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 變成排除區間。 - NO INTERVALS vs NO-EXTENSION:不看筆記說出兩者的差別,並說出「只准填一個單值」要怎麼寫。
- 區間 vs 列舉:舉出至少三個「用 BT 會抓錯、用 EQ 列舉才對」的實際理由,並把 E/BT 'DXTR1201' ~ 'DXTR1210' 改寫成正確的列舉版本。
- 第 38 練習:用 BEGIN OF BLOCK / WITH FRAME TITLE / BEGIN OF LINE / COMMENT / ULINE / SKIP 排出一個「三個方框、其中一行有兩個單選鈕加一個輸入框」的畫面,並用 CALL SELECTION-SCREEN 1001 STARTING AT … 做出彈出視窗。
- 事件順序:不看筆記寫出 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 有什麼不同」。
- 第 39 練習:用 MODIF ID + USER-COMMAND + LOOP AT SCREEN,做出「勾了核取方塊才出現兩個欄位」的效果;再改成「不勾時變灰(唯讀)而非消失」。
- F4 自製:用 F4IF_INT_TABLE_VALUE_REQUEST 做一個銷售訂單的 F4,LOW 與 HIGH 都要能用;再做一個「先選工廠、料號 F4 只列該廠料號」的連動 F4(用 DYNP_VALUES_READ)。
- 第 40 練習:實作 AT SELECTION-SCREEN ON BLOCK 與 ON EXIT-COMMAND,並解釋為什麼原範例的 EXIT-COMMAND「試了無效」。
要點:POPUP 的 ANSWER 沒接回來;只有功能碼型別 E 才觸發。 - 終止指令:說出 STOP/EXIT/RETURN/CHECK/REJECT 各自的作用範圍,特別是「EXIT 在巢狀迴圈中只跳一層,RETURN 直接離開整個模組」。
- SUBMIT:用 WITH SELECTION-TABLE(RSPARAMS)動態組條件呼叫另一支程式,並加上 VIA SELECTION-SCREEN 與 AND RETURN 觀察差別。
- 變式與排程:建立一個變式,把日期欄位設成 Selection variable = D「上個月的最後一天」,再用 SM36 排一個每月執行的 job,SM37 看結果。
- 10-1(自我驗收):把第 2 章的 Z202202_26 改寫成「使用者可以輸入區間條件,且建檔者預設為登錄者(sy-uname)」。
要點:SELECT-OPTIONS + INITIALIZATION 設 p_ernam = sy-uname。 - 10-2:打開標準程式 MB52(庫存清單)研究它的選擇畫面。老師的問題:「懂 ERP 模組的人來看程式,跟懂程式的人來學存貨評價,哪一個容易?」
- 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。 - 綜合題:配合第 3-2 章的 ZOOALV_2_GRID,加上「包含 DXTR1*、排除 DXTR1201~DXTR1210」的複雜條件,並用 CL_SALV_TABLE 顯示;比較用 BT 排除與用 EQ 逐筆排除的筆數差異,把差異的料號列出來。
- 結業自問:老師說「恭喜您學完模組顧問所需要的 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。
1. 為什麼一定要學 OO:ALV 三代演進與 S/4HANA 的現實
老師在講義裡把 ALV 的三個世代並排比較,這張表其實就是 ABAP 三十年的縮影:
| Function ALV | GRID ALV(OOALV) | SALV(OOALV) | |
|---|---|---|---|
| 年代 | 1996 | 2000 | 2004 |
| 平台 | SAP R/3 3.1I | SAP R/3 4.5 | SAP NetWeaver |
| 主要 API | REUSE_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 節 |
到了 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 TESTING、CL_ABAP_UNIT_ASSERT),沒有類別就沒有測試。
- Clean ABAP 官方風格指南預設所有新程式都以類別為單位,FORM/PERFORM 已被標記為 obsolete。
2. OO 基本觀念:類別、物件、封裝
講義開宗明義的定義,先背下來:
- 類(Class):用來描述「具有相同屬性和方法的物件」的集合。它定義了該集合中每個物件所共有的屬性(Attributes)和方法(Methods)。類別是藍圖。
- 物件(Object):是類的實例(Instance),是一個現實實體的抽象。一個物件可以被認為是「把資料(屬性)和程式(方法)封裝在一起」的實體。
- 方法(Methods):對物件的操作。
- 物件是動態的概念:其中的屬性反映了物件當前的狀態。
- OOALV 就是利用 ALV 的一個實例(Instance)來顯示的。
ABAP 的類別分兩種存放位置,兩者語法完全相同,差別只在「誰看得到」:
| 本地類別 Local Class | 全域類別 Global Class | |
|---|---|---|
| 建立工具 | 直接寫在程式(SE38/SE80)裡,或 Include *_CL1 | SE24 或 SE80 → Class/Interface;Eclipse ADT 建 Class |
| 命名 | 習慣用 LCL_ 開頭(Local CLass) | 客製一律 ZCL_ / YCL_;SAP 標準是 CL_ |
| 可見範圍 | 只有該支程式看得到 | 整個系統都能用,可被別的程式、BAdI、RAP 引用 |
| 什麼時候用 | ALV 事件 handler、只給這支程式用的小工具 | 共用邏輯、要被單元測試、要在 SE24 被 F4 找到 |
| 類比 | 程式內的 FORM | Function Module(SE37) |
類別的完整宣告骨架(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 | 只有自己(同一個類別) | 預設全部放這裡,需要才往上開。這就是「封裝」 |
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.
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.
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-> | 指向自己這個物件 | 區分同名的區域變數與屬性 |
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.
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.
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 ... sender | handler 可以額外收 sender(發送者參照) |
| SET HANDLER h FOR o | 登記單一物件 |
| SET HANDLER h FOR ALL INSTANCES | 登記所有同類別物件(含未來建立的) |
| ... ACTIVATION space | 取消登記 |
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_CHECK | CX_ROOT | 語法檢查會強制你 CATCH 或在方法上宣告 RAISING(自訂商業例外用這個) |
| CX_DYNAMIC_CHECK | CX_ROOT | 執行期才檢查(例:CX_SY_CONVERSION_NO_NUMBER) |
| CX_NO_CHECK | CX_ROOT | 不強制處理,一路往上傳(例:CX_SY_DYNAMIC_OSQL_SEMANTICS) |
7. Screen 設計入門:SE51、PBO / PAI、MODULE
OOALV 的第一步不是寫程式,而是畫一個螢幕。這一節把 Dialog 螢幕的基本結構講清楚,第 4-2 章會再深入 Tabstrip 與 Table Control。
7-1 一個 Screen 由四個部分組成
| 組成 | SE51 頁籤 / 工具 | 內容 |
|---|---|---|
| Attributes(屬性) | Attributes | Screen 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 logic | PROCESS 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-STATUS、SET 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.
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) | 明細視窗 |
・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.
8. OOALV 十大步驟:CL_GUI_CUSTOM_CONTAINER + CL_GUI_ALV_GRID
講義把整個流程拆成十個步驟,這是本章的主線。先看骨架,再看完整程式。
| 步驟 | 做什麼 | 關鍵 |
|---|---|---|
| 1 | SE51 建立螢幕 8000 | Screen type = Normal;Next screen 留空或填 8000 |
| 2 | Layout 上拉一個 Custom Control,命名 CONTAINER | 快捷鍵 Ctrl+Shift+F5(善用 C-S-F5);同時建立 OK_CODE 欄位 |
| 3 | 主程式定義四個變數 | Container、Grid、Fieldcat、Layout |
| 4 | SE24 觀察 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 | 設定 LAYOUT | zebra、grid_title、smalltitle、cwidth_opt |
| 9 | 讀取資料到內表 → SET_TABLE_FOR_FIRST_DISPLAY | 第二次進來改呼叫 REFRESH_TABLE_DISPLAY |
| 10 | CALL SCREEN 8000. + SE41 狀態列 + PAI 事件 | 可以試跑了 |
OO-ALV 的四大控制結構(講義特別列出,全部是 LVC_ 開頭的字典型別,可用 SE11 查):
| 控制項目 | 參考結構/型別 | 常用欄位 |
|---|---|---|
| 欄位目錄 Field catalog | LVC_T_FCAT(行結構 LVC_S_FCAT) | fieldname、ref_table、ref_field、coltext、seltext、outputlen、no_out、edit、hotspot、do_sum、just、checkbox |
| 佈局控制 Layout | LVC_S_LAYO | zebra、grid_title、smalltitle、cwidth_opt、sel_mode、no_toolbar、excp_fname、info_fname、stylefname、ctab_fname |
| 列印參數 | LVC_S_PRNT | prntgrplst、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 |
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.
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.
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.
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.
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.
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_changed | mc_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 | 自訂欄位的 F4 | e_fieldname、es_row_no、er_event_data |
| PRINT_TOP_OF_PAGE | 列印抬頭 | — |
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 ).
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.
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( ) 之後不能再改欄位設定。
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.
14. 記憶體洩漏、物件釋放與 CL_GUI_CFW
GUI 控制項(Container、Grid、Tree、Picture…)是前端物件:ABAP 這邊只有一個代理(proxy),真正的控制項活在 SAP GUI 裡。因此它們有一套自己的生命週期規則。
| 方法 | 做什麼 | 什麼時候呼叫 |
|---|---|---|
| cl_gui_cfw=>flush( ) | 把累積的前端指令一次送到 SAP GUI 並等待回應 | 建立/修改控制項之後、需要立即看到效果時。不要在迴圈裡呼叫(每次都是一次網路往返) |
| cl_gui_cfw=>dispatch( ) | 把前端事件分派給對應的 handler | PAI 中若事件沒被觸發時使用(多數情況 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_display 的 it_outtab 是 CHANGING,ALV 直接指向你的內表記憶體。不要在顯示中 REFRESH 或重新指派那個內表變數,會造成 dump 或顯示垃圾資料;要換資料就 CLEAR 後重新 APPEND,再 refresh_table_display( )。
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.
・純顯示、不編輯、要快 → CL_SALV_TABLE(首選)。
・要編輯、要 data_changed 檢核、要複雜工具列 → CL_GUI_ALV_GRID。
・要跑在 ABAP Cloud / BTP / Fiori → 都不能用,改走 RAP + Fiori Elements;ALV 只存在於 SAP GUI 世界。
・REUSE_ALV_* → 只維護舊碼時看得懂即可,不要寫新的。
16. 常見錯誤與除錯清單
| 症狀 | 原因 | 解法 |
|---|---|---|
| 畫面一片空白,看不到 ALV | Custom Control 的名稱與 container_name 不一致(大小寫也算) | SE51 元素名稱與程式字串必須完全相同,一律用大寫 |
| 執行後閃一下就結束 | 用 DEFAULT_SCREEN 卻沒有任何清單輸出 | 加 WRITE space. |
| 按 BACK 沒反應/回不去 | Next screen 沒清空;或狀態列沒有功能碼;或 PAI 沒接 OK_CODE | Next screen 留空、SE41 補功能碼、Attributes 的 OK code 欄填 OK_CODE |
| 第二次進畫面出現 CNTL_ERROR | 重複建立同名容器 | 包 IF go_grid IS NOT BOUND;離開時 free( ) |
| 使用者改了值,程式抓不到 | PAI 沒呼叫 check_changed_data( ) | PAI 一開始就呼叫 |
| 每按一次按鈕就重複執行上一次的動作 | OK_CODE 沒有 CLEAR | save_ok = ok_code. CLEAR ok_code. |
| 雙擊抓到錯的那一列 | 使用者排序過,e_row-index 與內表索引不同步 | 用 es_row_no-row_id 或自建 key 欄位 |
| refresh 後捲軸跳回第一列 | 沒傳 is_stable | is_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 包起來 |
練習與自我檢核
- 十大步驟複習:不看講義,說出從「SE51 建 8000 畫面」到「CALL SCREEN 8000 跑起來」的十個步驟,並指出哪一步是「原封不動貼上就好、但一定要懂」的核心程式(答案:第 5 節的 DISPLAY_ALV——建立 Container → 以它為 parent 建立 Grid → fieldcat → layout → 資料 → SET_TABLE_FOR_FIRST_DISPLAY)。
- SE24 觀察:用 SE24 打開 CL_GUI_CUSTOM_CONTAINER 與 CL_GUI_ALV_GRID,各找出三個 Attributes、三個 Methods、三個 Events,並說明 CL_GUI_ALV_GRID 繼承自哪個類別(提示:CL_GUI_CONTROL → CL_GUI_OBJECT)。
- 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)。 - 2024/12/27 補充題:把程式改成「只有按自己追加的按鈕才會結束」。注意 SE51 的 Next screen 也要設 8000(或留空),否則 PAI 跑完就自動離開。參考程式 Z2409_OO_300。
- 十項錯誤修正檢核表(講義原文的 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——版面設計完成。逐項打勾。 - OOP 基本功:寫一個本地類別 LCL_MATERIAL,屬性 MATNR 放 PRIVATE,提供 CONSTRUCTOR、GET_DESCRIPTION( )(讀 MAKT)、靜態方法 GET_COUNT( )。再加一個子類別 LCL_MATERIAL_FERT 覆寫 GET_DESCRIPTION,在描述前加上「【成品】」。用一個 LOOP 展示多型。
- 介面練習:定義 LIF_EXPORTABLE(方法 TO_CSV),讓 LCL_MATERIAL 與 LCL_ORDER 都實作它;寫一個接收 TYPE REF TO lif_exportable 的方法,證明「呼叫端不必知道實際類別」。
- 事件練習:把第 5 節的 LCL_STOCK 範例改成——出貨時若庫存不足直接 RAISE EXCEPTION TYPE zcx_no_stock,低於安全庫存則 RAISE EVENT。說明「例外」與「事件」在語意上的差別(提示:例外是「我做不下去了」,事件是「我做完了,順便通知你」)。
- ALV 事件實作:在第 9 節的完整範例上加三件事——(a) 雙擊 VBELN 跳 VA03;(b) 工具列加一顆「全部展開」按鈕;(c) NETWR 大於 100,000 的列整列變紅色(提示:fieldcat 加 stylefname 或 layout 的 info_fname + 內表加一個 char 3 的顏色欄位 'C610')。
- 可編輯 ALV:把 KWMENG 設成可編輯,在 DATA_CHANGED 事件檢核「不可為 0、不可小於已交貨量」,錯誤用 add_protocol_entry 回報;PAI 記得 check_changed_data( )。
- SALV 內嵌:把同一份資料改用 CL_SALV_TABLE 顯示在 SE51 的 Custom Control 裡,並用 CL_SALV_FORM_LAYOUT_GRID 做出四行抬頭(報表名稱、執行者、日期時間、筆數)。說明為什麼一定要 set_screen_status( )。
- 雙視窗連動:完成講義第 17 節的作業——上方 MARA,下方改成 VBAP-MATNR / VBAP-VBELN / VBAP-POSNR / VBAP-PSTYV。再進階:改成三個視窗(MARA → VBAP → VBEP 交期),用 rows = 3 或巢狀 SPLITTER。
- 容器比一比:同一支報表分別用 CUSTOM_CONTAINER、DOCKING_CONTAINER、DEFAULT_SCREEN 顯示,記錄三者在「要不要建螢幕、要不要畫元素、能不能和選擇畫面共存」上的差異。
- 記憶體檢核:故意把 SET HANDLER 寫在 PBO 的 IF 判斷之外(每次 PBO 都掛一次),觀察雙擊一次會跳出幾個訊息;再修正回來。這是理解「事件登記會累加」最直接的實驗。
- 例外處理:重現講義第 15 節的除以零範例,比較「不加 TRY 會 dump 到 ST22」與「加了 TRY 只印訊息」的差別;再加一個 CLEANUP 區塊,觀察它什麼時候才會被執行。
- 三代對照:把同一份 EKKO 資料,分別用 REUSE_ALV_LIST_DISPLAY、CL_GUI_ALV_GRID、CL_SALV_TABLE 寫三支程式,比較程式行數、可用功能、是否需要螢幕。最後回答:如果這支程式明年要搬到 ABAP Cloud,哪一支活得下來?
- 觀念總結題:用自己的話說明——為什麼老師說「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 章。
1. 增強的概念:什麼時候該用增強
增強(Enhancement)的概念其實非常廣:建在 SAP 標準系統之上的所有二次開發,都可以理解為增強。但 ABAP 開發講的「增強」,特指標準系統事先預留好的介面——SAP 在自己的程式裡寫了一個空的鉤子,我們把程式碼掛上去,執行時就會被呼叫。這種開發稱為增強,也叫出口(Exit)。
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 取得實例,再呼叫介面方法。支援多重實作與篩選器 | SE18/SE19(第 3-4 章) |
| 第四代 | Kernel BAdI(new BAdI) | 編譯時期就實體化(compile time),用 GET BADI / CALL BADI,效能較佳 | SE18/SE19(第 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.選用決策樹(實務順序)
- 先問:能不能用 Customizing 解決?(SPRO、條件技術、輸出控制、複製控制)→ 能就不要寫程式。
- 有沒有 new/classic BAdI?(SE18 / SE84 找 Enhancement Spot)→ 有就用 BAdI。BAdI 支援多重實作,最不容易和其他顧問打架,也是 SAP 在 S/4HANA 唯一持續增修的介面。
- 有沒有第二代 Customer Exit?(SMOD/CMOD)→ 有就用。缺點:一個增強組件在整個 client 只能被一個 CMOD 專案啟用,多顧問搶同一個出口會衝突。
- 有沒有第一代 User Exit?(MV45AFZZ 之類)→ SD 很多情境只有第一代做得到(例如 USEREXIT_SAVE_DOCUMENT_PREPARE、USEREXIT_MOVE_FIELD_TO_VBAP),該用還是要用。
- 有沒有隱式/顯式 Enhancement Point?→ 都沒有出口時的最後救援(比修改標準碼安全)。
- 都沒有 → 才考慮修正(Modification),要 Access Key,升級走 SPAU/SPDD。
3. 第一代 User Exit:原理與命名規則
第一代的做法是:SAP 提供一個空程式碼的子常式(FORM),在這個子常式中使用者可以加入自己的代碼。它的規律是:
- 需要修改的 SAP 標準碼,集中在一些名稱倒數第二個字元為 Z 的 INCLUDE 程式中(MV45AFZZ、MV45AOZZ、MV45AIZZ、MV45ATZZ…)。
- 子常式一般以 USEREXIT_ 起頭。
- 在出口裡,呼叫程式的所有全域資料都可以使用(XVBAP、XVBEP、VBAK、KOMK…直接拿來用),這是第一代最強、也最危險的地方。
- 螢幕增強以客戶螢幕(Include Screen)形式發佈,包含在標準程式中,沒有什麼特別規律,要看該模組的 IMG 說明。
- 表/結構的增強是 Append Structure。
- 原始碼增強和螢幕增強的說明,可以從 SPRO 後臺設定中相關模組的路徑裡面找到。
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 ← 最常用的一支
4. 怎麼找第一代出口:三種可操作的方法
4-1.方法一:利用 SPRO(最正統,附官方說明文件)
- 執行 SPRO → 按「SAP 參考 IMG(SAP Reference IMG)」。
- 走到目標模組的節點,找名稱含 System Modifications/系統修改/User Exits 的子節點。SD 的路徑是:Sales and Distribution → System Modifications → User Exits。
- 點該節點左邊的文件圖示(IMG 說明),SAP 會列出這個領域所有可用的 INCLUDE 與 USEREXIT_* 子常式,以及每一個出口的呼叫時機(例如「呼叫於 FORM BELEG_SICHERN 開頭」)。
- 把要用的 INCLUDE 名稱抄下來,到 SE38 開啟它,找到對應的 FORM 寫碼。
(依講義步驟整理)
4-2.方法二:利用 SE80 + Package VMOD
- 執行 SE80,物件類型下拉選 Package(套件/開發類別)。
- 輸入 VMOD(SD 使用者出口的專屬套件;講義寫成「vomd」是筆誤)→ Enter。
- 展開 Programs/Includes,就會看到 MV45AFZZ、MV45AFZA、MV45AFZB、MV45AOZZ、MV45AIZZ、MV50AFZ1、RV60AFZZ… 這一整批第一代出口 include。
- 雙擊任一支,用「其他功能 → 尋找(Ctrl+F)」搜 USEREXIT_,就能列出這支 include 內所有可用出口。
4-3.方法三:由 TCODE 找主程式,再從主程式找出口(最通用)
- 在目標交易畫面按 系統 → 狀態(System → Status),看「程式(畫面)」欄位,或直接跑 SE93 輸入交易碼看它掛的程式。例如 VA01 → SAPMV45A、ME21 → SAPMM06E、VL01N → SAPMV50A、VF01 → SAPMV60A。
- SE38 開啟該主程式 → 看它的 INCLUDE 清單,找倒數第二個字元是 Z 的 include(多半集中在註解 U S E R - E X I T S 底下)。
- 雙擊進入 include,搜尋 FORM USEREXIT_,挑選符合時機的那一支。
- 不確定會不會被呼叫?在 FORM 第一行設中斷點,跑一次交易,停下來就是對的;停不下來就換一支。
典型路徑:SAPMV45A ⇒ MV45AFZZ ⇒ FORM USEREXIT_SAVE_DOCUMENT。
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.
・TYPE 'I' / 'W' 會試圖跳出對話框 → 背景或 RFC 會 DUMP(MESSAGE_TYPE_X / 無畫面可顯示)。
・TYPE 'E' 在 BAPI 中不會回到 RETURN 表,而是直接讓程式中斷。
正確做法:先判斷 sy-batch/sy-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.
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.
6. SD 第一代出口大全(依 INCLUDE 分類)
下表整理自 IMG 說明「User Exits In Sales Document Processing」。這是查閱型的表,實作時對照時機挑一支就好。
6-1.涉及的程式元件
| INCLUDE | 用途 | 命名規定 |
|---|---|---|
| MV45ATZZ | 輸入銷售單據處理用的 metadata(全域資料宣告) | 使用者自訂 metadata 必須以 ZZ 開頭 |
| MV45AOZZ | 額外的 PBO 模組(畫面輸出前執行) | 模組名必須以 ZZ 開頭 |
| MV45AIZZ | 額外的 PAI 模組(資料輸入後執行,例如資料檢核) | 模組名必須以 ZZ 開頭 |
| MV45AFZZ 與 MV45EFZ1 | 輸入安裝特定的 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_REQ | 為 EBAN(採購申請)/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 程式的出口
| 程式 | 出口 | 用途 |
|---|---|---|
| MV45AFZD | USEREXIT_CONFIG_DATE_EXPLOSION | 決定 BOM 用哪個日期展開(標準用輸入日期,可改成需求交貨日) |
| FV45EFZ1 | USEREXIT_CHANGE_SALES_ORDER | 採購申請被指派後,標準會自動改排程行的數量與確認日期;用這支出口設定條件,保護訂單不被自動改 |
| RV45PFZA | USEREXIT_SET_STATUS_VBUK | 設定 VBUK(抬頭狀態)保留欄位(如 UVK01 當額外訂單狀態)。可用工作區:VBUK、FXVBUP、FXVBUV |
| RV45PFZA | USEREXIT_SET_STATUS_VBUP | 設定 VBUP(明細狀態)保留欄位。可用工作區:FXVBAP、FXVBAPF、FXVBUV |
| RV45PFZA | USEREXIT_STATUS_VBUK_INVOICE | 影響抬頭層的開票狀態 |
| MV45AFZ4 | USEREXIT_MOVE_FIELD_TO_KOMK / _KOMP | 免費商品判定(Free Goods)用的抬頭/明細通訊結構加欄位 |
| SAPFV45PF0E / SAPFV45PF0C | EXIT_SAPFV45P_001 | 跨公司代碼銷售時,決定獲利分析段(Profitability Segment)用公司間開票資料,還是用外部開票資料 |
| MV50AFZ1 | USEREXIT_SAVE_DOCUMENT_PREPARE、USEREXIT_SAVE_DOCUMENT、USEREXIT_MOVE_FIELD_TO_LIKP/LIPS… | 交貨單(VL01N/VL02N)的第一代出口。還有 MV50AFZ3(挑撿/過帳)、MV50AFZL(LIPS 相關) |
| RV60AFZZ | USEREXIT_NUMBER_RANGE、USEREXIT_FILL_VBRK_VBRP… | 開立發票(VF01/VF04)的第一代出口。搭配 RV60AFZC(會計文件)、RV60AFZD |
| MV45AFZH / RV61AFZA / RV61AFZB | USEREXIT_PRICING_PREPARE_TKOMK/TKOMP 等 | 定價通訊結構(開票端) |
6-7.螢幕上的第一代出口
SD 的抬頭附加資料在螢幕 SAPMV45A 0309,明細附加資料在 SAPMV45A 0459。這兩個螢幕分別內含 Include Screen SAPMV45A 8309 與 SAPMV45A 8459,就是給客戶用的螢幕出口。
- 要在銷售單據中新增欄位(例如 VBAK-ZZKUN),先用 SE11 Append Structure 把欄位加進 VBAK。
- 如果檢核是用資料字典定義的(固定值或檢查表),只要用 Screen Painter 全螢幕編輯器把欄位放進 SAPMV45A 8309,處理邏輯完全不用改。
- 如果需要應用專屬的檢核模組,把模組寫在 MV45AIZZ(模組名以 ZZ 開頭),並在 Include Screen 的處理邏輯中呼叫。
- 欄位傳輸(Field Transport)不需要做任何調整。
(依講義步驟整理)
7. VOFM 常式:SD 的另一種第一代增強
VOFM 是 SD/定價/輸出控制專用的「常式維護器」。它產生的也是 FORM 子常式(第一代的親戚),但是由 VOFM 統一產生、統一啟用,並在 Customizing 中被掛號使用。
7-1.VOFM 的四大類
| 選單 | 中文 | 用途 | 產生的 FORM / 程式 |
|---|---|---|---|
| Requirements → Pricing | 需求常式(定價) | 決定某個條件類型要不要被存取/計算。掛在 V/08 定價程序的 Requirement 欄 | FORM KOBED_9xx(含明細檢查)與 KOBEV_9xx(前置檢查,不可用明細欄位);程式 LV61A9xx |
| Formulas → Condition value | 條件值公式 | 自行計算條件的金額,結果放進 XKWERT。掛在 V/08 的 CalType(計算公式)欄 | FORM FRM_KONDI_WERT_9xx;程式 RV64A9xx |
| Formulas → Condition base value | 條件基值公式 | 自行決定計算基礎(基數)。掛在 V/08 的 BasType 欄 | FORM FRM_KOND_BASIS_9xx;程式 RV64A9xx |
| Formulas → Scale base / Group key | 級距基值/群組鍵 | 決定級距用什麼數量、群組條件如何彙總 | FORM FRM_STAFFELBAS_9xx/FRM_GRP_SCHL_9xx |
| Data Transfer | 資料轉移 | 複製控制(VTAA/VTLA/VTFA)中,決定來源單據的哪些欄位怎麼搬到目標單據 | FORM DATEN_KOPIEREN_9xx(程式名依子選單而異,雙擊 VOFM 該列即可看到) |
| Copying requirements | 複製需求 | 決定「這張單據可不可以被複製成下一張」(訂單→交貨→發票) | FORM KOBED_9xx 系列(複製控制專用編號段) |
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.
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.
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、編寫相應的程式碼即可。
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 | 類型 | 說明 |
|---|---|---|
| E | Function Exit(功能模組增強) | 以 EXIT_ 起頭的函數,可到 SE37 查看,也可在資料字典的 TFDIR(函數表)中查詢 EXIT_ 開頭的函數 |
| S | Screen Exit(子螢幕增強) | 第一代是「螢幕編號先預留好,但內容是空」;第二代要你自己建立螢幕。採購訂單 ME2*、銷售訂單 VA0*、製令單 CO0* 都允許使用者自訂輸入介面並編寫相應的輸入輸出處理常式 |
| C | Menu Exit(GUI 增強/功能碼) | GUI code:在標準的功能表/按鈕列加上自訂功能碼(以 + 開頭)與文字 |
| T | Structure Exit(表結構增強) | Include structure。例如 MM06E005 中允許使用者維護的兩個結構 CI_EKKODB 與 CI_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、子螢幕互動時必備) |
| 資料表 | 內容 | 重要欄位 |
|---|---|---|
| MODSAP | SAP 增強表:記錄了所有 enhancement 的增強內容,也就是增強點與函數的對應關係表 | NAME(增強名,如 MM06E005)、TYP(元件類型 E/C/S/T)、MEMBER(元件函數模組名,如 EXIT_SAPMM06E_007) |
| MODSAPT | MODSAP 的文字表(增強說明) | SPRSL、NAME、MODTEXT |
| TFDIR | 所有的函數表 | FUNCNAME(函數名)、MAND:值為 'C' 時才表示此出口函數已被啟動 |
| ENLFDIR | 函數模組的額外屬性(可由函數反查所屬函數群組 AREA) | FUNCNAME、AREA |
| TSDIR | Dynpro Areas:CALL CUSTOMER-SUBSCREEN 的登錄(螢幕增強) | PROG、DYNNR、AREA |
| CUATEXTS | GUI Interface:Menu Texts Changed(GUI 功能表文字增強) | PROGRAM、FCODE、TEXT |
| MODACT | CMOD 專案與增強組件的指派關係(哪個專案用了哪個 SMOD 增強) | NAME(專案)、MEMBER(增強) |
| TADIR | Repository 物件目錄;找出口靠它(OBJECT = SMOD 是第二代增強、SXSD 是 BAdI) | PGMID、OBJECT、OBJ_NAME、DEVCLASS |
9. 怎麼找第二代出口:六種可操作的方法
這一節是本章的核心。照順序試,一定找得到。
方法 A(首選):用 Z_FIND_USEREXIT 依 TCODE 一次列出
- SE38 執行 Z_FIND_USEREXIT(自建工具程式,見第 10 節)。
- 輸入交易碼(如 ME21N、VA01)或直接輸入程式名。
- 報表會分兩區列出:Enhancement(SMOD,第二代) 與 Business Add-in(SXSD,第三/四代),並顯示筆數。
- 雙擊清單中的名稱可直接跳到 SMOD(第二代)或 SE18(BAdI)。
方法 B:SE84 Repository Information System
- SE84 → 展開 增強(Enhancements)。
- Customer Exits → Enhancements:可用增強名稱(如 MM06E*)、Package、元件名(如 EXIT_SAPMM06E*)反查增強組件。
- Customer Exits → Function Exits:直接用 EXIT_* 樣式搜函數出口。
- Enhancement Spots / BAdIs:找第三、四代(第 3-4 章)。
方法 C:SE16N 查 MODSAP / MODSAPT
- SE16N → 表名 MODSAP。
- MEMBER 欄位輸入 EXIT_SAPMM06E*(用你猜到的主程式名組出來的樣式)→ 執行,即可看到所有相關的增強名與類型。
- 反過來也行:知道增強名(如 MM06E005)填 NAME,就能看到它底下所有元件(E/S/C/T 各是什麼)。
- 要中文/英文說明就查 MODSAPT(SPRSL = 語言)。
方法 D:在標準程式中直接搜尋 CALL CUSTOMER-FUNCTION(最可靠)
- SE93 由交易碼查出主程式(例:ME21N → SAPLMEGUI;ME21 → SAPMM06E;VA01 → SAPMV45A)。
- SE38 開該程式 → 功能表「其他功能 → 搜尋/取代」(或 Ctrl+F)。
- 搜尋字串輸入 CUSTOMER-FUNCTION,範圍選 「在所有 Include 中(In main program)」、勾選「所有出現位置」。
- 結果每一行像 CALL CUSTOMER-FUNCTION '007',把 主程式名 + 編號 組起來就是函數名:EXIT_SAPMM06E_007。
- 螢幕出口就搜 CALL CUSTOMER-SUBSCREEN;菜單出口搜 GUI Status 中以 + 開頭的功能碼。
- 拿函數名回到 MODSAP(方法 C)反查它屬於哪個增強組件(NAME),才知道 CMOD 要掛哪一個。
方法 E:SE37 直接猜/查 EXIT_ 函數
- SE37 函數名輸入 EXIT_SAPMM06E* → 按 F4,列出所有符合的出口函數。
- 雙擊進去看它的介面參數——這一步很關鍵:介面裡沒有的資料,你在出口裡就拿不到,決定了這支出口能不能用。
- 看它的原始碼(只有一行 INCLUDE ZX…),記下 INCLUDE 名稱。
方法 F:追不到時的 DEBUG/Trace 大絕
- 敘述中斷點:進入交易 → /h 開 Debug → 功能表「中斷點 → 中斷點於 → 敘述(Breakpoint at Statement)」→ 輸入 CALL CUSTOMER-FUNCTION。之後每次標準碼呼叫任何出口都會停下來,sy-repid 就是主程式、參數就是編號。同理可設 CALL CUSTOMER-SUBSCREEN、CALL BADI、CALL METHOD。
- 函數中斷點:「中斷點 → 中斷點於 → 函數模組」→ 輸入 MODX_FUNCTION_ACTIVE_CHECK。每次系統檢查「這個出口有沒有啟用」都會停,在 Local 頁籤看參數就知道正在檢查哪一支出口(第 12 節詳述)。
- 執行時期分析:SAT(舊版 SE30)錄一次交易,在結果的「呼叫階層/Hit List」中用 EXIT_ 或 ZX 過濾,可以看到實際被執行過的出口與先後順序——這是判斷「哪一支出口真的會跑」最省時間的方法。
- SQL Trace:ST05 看標準程式讀了哪些表,反推該從哪個時點切入。
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.
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 SAP 標準表繞過 CMOD,不會產生傳輸請求,DEV 改了 QAS/PRD 不會有,升級或重新產生時會被打回原形。
・沒有 CMOD 專案登錄,下一個顧問完全看不出這裡有增強,是稽核與交接的災難。
・正確做法永遠是:SMOD 查出增強組件 → CMOD 建專案 → 指派 → 寫碼 → 啟用專案。
11. SMOD 與 CMOD:查看與實現增強的完整操作
兩個交易碼的分工,一句話記住:SMOD 是「看有什麼出口」(SAP 交付的增強組件,唯讀);CMOD 是「我要用哪些出口」(客戶專案,可寫可啟用)。
11-1.SMOD:查看增強組件
- SMOD → 增強名稱輸入(如 MM06E005、V45A0003),或按 F4 用說明文字模糊搜尋。
- 選 Components(元件) → 顯示。畫面分區列出:Function exits(EXIT_*)、Screen areas(螢幕出口的程式+螢幕編號)、Menu entries、Include structures(CI_ 結構)。
- 按 Documentation(文件) 讀 SAP 的官方說明:這個增強什麼時候被呼叫、能改什麼、不能改什麼。這一步千萬別跳過。
- 雙擊某個 EXIT_ 函數即可進入 SE37 看介面與 INCLUDE 名稱。
(依講義步驟整理)
11-2.CMOD:建立專案並實現增強(標準八步驟)
- CMOD → 專案(Project)輸入自訂名稱(Z 開頭,例如 ZPO、ZSD01)→ 按建立。
命名策略:老師的做法是「ZPO 是指將所有的 PO 相關增強都放在此 PROJECT」——依模組/主題分專案,不要一個出口開一個專案,否則以後沒人管得動。 - 輸入簡短說明 → 存檔 → 指定套件與傳輸請求。
- 按 Enhancement assignments(增強指派) → 在清單中列舉由 SMOD 所找到的、想要增強的增強組件名稱(可一次填多個)→ 存檔。
- 按 Components(元件) → 可看到已經預設好的增強函數名稱(例如 EXIT_SAPMM06E_007)。
- 雙擊該函數名 → 進入函數群組(例如 XM06)→ 函數原始碼中只有一行 INCLUDE ZXM06U38.。
- 雙擊 INCLUDE 名稱 → 系統詢問「此 Include 尚不存在,要建立嗎?」→ 按是,指定套件與請求 → 建立 ZXM06U38,裡頭就可以自行新增程式碼了。
- 寫完程式碼 → 啟用(Ctrl+F3) → 退出回到 CMOD。
- 在 CMOD 專案畫面按 啟用(Activate,火柴人/閃電圖示)。啟用成功後,回到 Components 會發現增強函數名稱前面有一個綠色 ✔。
(依講義步驟整理)
・只寫程式碼、沒按 CMOD 的「啟用」=完全不會被呼叫(TFDIR-MAND 不會變成 'C')。這是初學者最常卡住的地方。
・要移除增強,順序是:先 Deactivate 專案 → 再刪除指派/程式碼。直接刪 Include 會讓函數變成語法錯誤,整個交易碼掛掉。
・一個增強組件(SMOD name)只能被一個 CMOD 專案指派。若已被別的專案佔用,系統會擋下來——這時要去談,把邏輯合併進既有專案,不要另開專案硬幹。
11-3.第一代與第二代在「建程式」上的差別
12. 第二代實作(一):採購單 MM06E005
練習題:在採購單上增加客制的欄位,並處理它。 這個練習一次用到第二代的三種類型:T(結構)→ S(螢幕)→ E(函數)。
12-1.步驟總覽
- SMOD 查 MM06E005(Customer fields in purchasing document)→ 看 Components。
- CMOD 建專案 ZPO → Enhancement assignment 填 MM06E005。
- 結構增強(T):SE11 維護 CI_EKKODB(抬頭)與 CI_EKPODB(明細),加上 Z 開頭的欄位並啟用。
- 螢幕增強(S):SE38/SE80 進 SAPLXM06,建立子螢幕 0101(抬頭)/0111(明細),畫上剛才的欄位。
- 函數增強(E):實作 EXIT_SAPMM06E_006/007/008 等,把資料在標準結構與子螢幕之間搬進搬出。
- CMOD 啟用專案 → ME21N/ME21 測試。
(依講義步驟整理)
12-2.欄位結構增強(EKKO)
- SE11 → 結構 CI_EKKODB → 若不存在則建立(它是 SAP 預留給客戶的 Customer Include,名稱固定,不要自己另取)。
- 加入欄位,欄位名稱以 Z 起頭(例如 ZZCONTRACT、ZZREMARK),指定資料元素。
- 啟用——講義特別提醒:「啟動要等待比較久」(因為 EKKO 是大表,要做結構調整/轉換)。
- 啟用完成,回頭用 SE11 看 EKKO,就會發現表中已加入增強的欄位了。
12-3.螢幕增強實作
- 用 SE38 進入 SAPMM06E,搜尋 CALL CUSTOMER-SUBSCREEN,確認 SAP 預留的子螢幕區域名稱與呼叫的程式(SAPLXM06)。
- 第二代增強不會預先新增程式或螢幕,因此要進入 SE38/SE80 開啟 SAPLXM06,自行新增螢幕編號 0101。
- 建立螢幕 0101 時,屬性中的螢幕類型要設定成 Subscreen(子螢幕)——設錯類型畫面不會出現。
- 進入 Layout 設計自己的子螢幕:用 Dict/Program fields 把 EKKO-ZZxxx 拉進來。
- 在該螢幕的 Flow Logic 寫 PBO/PAI 模組(模組要寫在函數群組 XM06 的客戶 include,例如 ZXM06O01/ZXM06I01)。
- 回到 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.
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 ).
13. 超級函數 MODX_FUNCTION_ACTIVE_CHECK 與出口觸發順序
MODX_FUNCTION_ACTIVE_CHECK 的參數是由功能模組名稱而來——標準程式在真正呼叫 EXIT_ 之前,會先用這支函數問「這個出口啟用了沒」。所以只要在它上面設一個中斷點,整個交易會呼叫哪些出口、以什麼順序呼叫,就全部無所遁形。這就是老師說的「超級函數」。
13-1.用它來反查出口(操作步驟)
- SE37 開啟 MODX_FUNCTION_ACTIVE_CHECK → 在第一行可執行敘述設外部中斷點(Session Breakpoint)。DEBUG 技巧:也可以直接由函數設定 DEBUG。
- 執行目標交易(例如 ME21N)。
- 每次停下來時,切到 Debugger 的 Locals(本地)頁籤,看傳進來的函數名參數——那就是系統正在檢查的出口。
- 把每次停下來的函數名依序抄下來,就得到「這個交易的出口觸發時序表」。
- 找到目標出口後,把中斷點移到該出口的 ZX… include 上,取消 MODX 的中斷點。
*--- 在自己的程式裡也能判斷某個出口是否已啟用(少用,但除錯很好用)
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_007 與 EXIT_SAPMM06E_008 |
| 點入 Material(物料)時 | EXIT_SAPLOMCV 系列 |
| Material 輸入完成按下 ENTER 時 | EXIT_SAPMM06E_016 |
| 整筆明細輸入完成之後 | EXIT_SAPMM06E_018 |
| 按下存檔之後 | EXIT_SAPLEBND_002 |
14. 第二代實作(二):客戶訂單 V45A0003
這是本章最容易在教學環境完成的實作,強烈建議親手做一次。
14-1.完整步驟
- SMOD 輸入 V45A0003 → 按 Components,看到元件 EXIT_SAPMV45A_003。
- CMOD 建立專案 ZSD01 → 輸入說明 → 存檔(指定套件與請求)。
- 按 Enhancement assignment(增強指派) → 填入 V45A0003 → 存檔。
- 按 Components → 雙擊 EXIT_SAPMV45A_003 → 進入 SE37。
- 函數原始碼只有一行 INCLUDE ZXVVAU05. → 雙擊 ZXVVAU05 → 建立它。
- 在 ZXVVAU05 中寫程式碼 → 啟用(要激活)。
- 回到 CMOD,專案也要激活(這步最常被忘記)。
- 執行 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.
15. 菜單出口與欄位出口
15-1.Menu Exit(菜單出口)
- SAP 在標準的 GUI Status 中預留了功能碼,以 + 開頭(例如 +CUS、+C01)。
- 在 SMOD 該增強的 Components 中,Menu entries 區塊會列出程式名、Status、功能碼。
- 在 CMOD 的 Components 中雙擊該功能碼,填入功能表文字(多語系可分別維護,文字存在 CUATEXTS)。
- 功能碼被按下時,會經由對應的函數出口(通常是同一個增強裡的某支 EXIT_,介面帶有 SY-UCOMM/FCODE)交給你處理。
- CMOD 專案啟用後,該功能表項目才會出現在交易畫面上。
(依講義步驟整理)
15-2.Field Exit(欄位出口)
欄位出口是掛在資料元素(Data Element)上的檢核/轉換:只要畫面上任何欄位用了那個資料元素,離開欄位時就會呼叫你的函數。
- CMOD → 功能表 Goto → Text enhancements → Field exits(或直接執行程式 RSMODPRF)。
- 建立 → 輸入資料元素名稱 → 系統自動產生函數 FIELD_EXIT_<資料元素>(可加尾碼 _A、_B 產生多個版本)。
- 在函數中處理:介面固定為 INPUT(使用者輸入值)與 OUTPUT(要寫回畫面的值)。
- 可以指定只對某一個程式/螢幕編號生效(全域 vs 區域欄位出口)。
- 啟用欄位出口,而且系統參數 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.
16. 傳輸、相依與版本管理
| 物件 | 傳輸方式 | 常見漏傳的地雷 |
|---|---|---|
| 第一代 include(MV45AFZZ…) | 以 PROG/INCLUDE 物件進 Request(會被標為對 SAP 物件的修改) | 宣告寫在 MV45ATZZ/MV45ATOP,但只傳了 MV45AFZZ → 目標系統語法錯誤 |
| CMOD 專案 | 物件類型 R3TR CMOD <專案名> | 只傳了 ZX… include 卻沒傳 CMOD 專案 → 目標系統程式在、但沒啟用,完全沒作用 |
| ZX… include | R3TR PROG(或函數群組 R3TR FUGR) | 只傳專案沒傳程式 → 目標系統啟用時語法錯誤 |
| 螢幕出口(SAPLXM06 0101) | 隨函數群組 FUGR XM06 一起傳 | 螢幕(Dynpro)沒被加進 Request;PBO/PAI 模組寫在別的 include 卻沒傳 |
| 結構增強(CI_EKKODB/Append) | R3TR TABL/R3TR TTYP 等字典物件 | 字典物件與程式在同一個 Request 但匯入順序造成短暫語法錯誤;建議字典先行、程式隨後 |
| VOFM 常式 | 產生的程式(LV61A9xx/RV64A9xx)+啟用登錄(TFRM/TFRMT) | 啟用旗標沒跟著過去 → 目標系統常式在但不執行 |
| Field Exit | 函數群組+登錄表 | 目標系統的 abap/fieldexit 參數沒開 |
17. S/4HANA 注意事項
・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 驗證出口有沒有跑到。
・RV45PFZA 的 USEREXIT_SET_STATUS_VBUK / _VBUP 在 S/4HANA 上因 VBUK/VBUP 被移除(改為 CDS 相容視圖),行為與 ECC 不同,升級專案必須逐一重測。
・出口裡若有 SELECT … FROM vbuk、FROM bseg、FROM mseg,升級時要改成新表或 CDS View。
・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 類別,出口只留一行呼叫。
⑦ 每一段增強都要在程式開頭寫註解:誰、哪一天、哪張需求單、為什麼。
練習與自我檢核
- 四代對照:不看筆記寫出四代增強的名稱、技術本質與對應的 TCODE。
要點:第一代=原始碼 FORM(SPRO/SE80 VMOD/SE38);第二代=函數 CALL CUSTOMER-FUNCTION(SMOD/CMOD);第三代=Classic BAdI,執行時期產生實體,用 CL_EXITHANDLER=>GET_INSTANCE(SE18/SE19);第四代=Kernel BAdI(編譯時期實體化)+ Enhancement-Point(顯式/隱式)。 - 找出口(第一代):用三種方法各找一次「VA01 存檔前檢查」的出口,寫下你走的每一步。
答案:SPRO → SD → System Modifications → User Exits 的說明文件/SE80 Package VMOD/SE93 查出 SAPMV45A → MV45AFZZ → USEREXIT_SAVE_DOCUMENT_PREPARE。 - 第一代實作:在 MV45AFZZ 的 USEREXIT_SAVE_DOCUMENT_PREPARE 寫一段「只有自己帳號 + 自己 client 才生效」的訊息,用 VA01 驗證,再改成「訂單金額超過 100 萬且客戶信用凍結時擋單」。測完務必註解掉。
- 為什麼 Z_FIND_USEREXIT 找不到第一代?
答案:它掃的是 TADIR 中 OBJECT IN ('SMOD','SXSD') 的物件;第一代是 FORM 子常式,不是 Repository 物件,不在 TADIR 裡。 - 出口挑選:需求是「訂單明細儲存時,把客戶主檔的客戶群組寫進 VBAP 的自訂欄位」。該用哪一支出口?為什麼不是 USEREXIT_SAVE_DOCUMENT?
要點:USEREXIT_MOVE_FIELD_TO_VBAP(VBAP_FUELLEN 結尾,每筆明細都會呼叫);SAVE_DOCUMENT 太晚且要自己 LOOP XVBAP。 - 第二代查表:用 SE16N 查 MODSAP,找出 MM06E005 底下所有元件,並說明 TYP 欄位 E/S/C/T 各是什麼。再查 TFDIR 確認 EXIT_SAPMM06E_007 的 MAND 是不是 'C'。
- CALL CUSTOMER-FUNCTION 命名規則:某標準程式 SAPMV45A 中有一行 CALL CUSTOMER-FUNCTION '003',請寫出函數名、要在 SMOD 查哪個增強、程式碼寫在哪個 INCLUDE。
答案:EXIT_SAPMV45A_003/增強組件 V45A0003/INCLUDE ZXVVAU05。 - 第二代實作(必做):完成第 14 節的 CMOD ZSD01 + V45A0003 + ZXVVAU05,用 VA01 測到訊息跳出來,然後取消啟用並確認訊息消失。說出「程式啟用」與「CMOD 專案啟用」的差別。
- 採購單三合一:描述在採購單加一個客制欄位的完整步驟(結構 → 螢幕 → 函數 → 啟用),並回答:為什麼 EXIT_SAPMM06E_008 才可以更動 EKKO?
要點:出口分「送資料給畫面」與「收資料回標準結構」,寫在只讀的出口會被下一個 PBO 覆蓋。 - 超級函數:在 MODX_FUNCTION_ACTIVE_CHECK 設中斷點,跑一次 ME21N,把觸發到的出口依序記錄下來,和第 13-2 節的表比對。你的系統多了或少了哪幾支?為什麼?
- CO02 練習:用 Z_FIND_USEREXIT 輸入 CO02,找出「SAP 記錄生產訂單修改日誌」的增強,讀它的 SMOD 文件並寫出它的呼叫時機。
- VOFM:說明 KOBED 與 KOBEV 的差別,並解釋為什麼在 KOBEV 裡存取 KOMP 欄位會導致「定價結果不穩定」。再說出定價值公式 FRM_KONDI_WERT_9xx 的回傳欄位是什麼。
答案:KOBEV 是前置檢查、此時沒有明細資料;回傳欄位是 XKWERT。 - VOFM 存取碼:新建一支 901 的定價公式,記錄系統是否要求 Access Key、常式編號的客戶命名空間是多少、以及「Active」旗標在哪裡打勾。
- 反例分析:說明為什麼 ZACTEXITFUN(直接 UPDATE tfdir SET mand = 'C')雖然「有效」卻絕對不能用在專案上,至少列出三個理由。
- S/4HANA 檢核:你接手一支 ECC 時代寫的 MV45AFZZ 出口,裡面有 SELECT … FROM vbuk 和 MESSAGE … TYPE 'I'。列出你在 S/4HANA 升級時要做的所有修改,並說明 SPDD/SPAU/SPAU_ENH 各在什麼時機用。
- 架構題:把第 14 節的 ZXVVAU05 重構成「出口只留一行、邏輯放 Z 類別」的寫法,並替 Z 類別寫一支 ABAP Unit 測試。說明這樣做對升級與多顧問協作各有什麼好處。
- 銜接:把本章找到的每一個需求,回頭到 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 時代,哪些寫法還能用、哪些已經是死路。
1. 四代增強技術總覽:先搞清楚你在第幾代
SAP 的「增強(Enhancement)」演進了四代。每一代都還活著、都還會在標準程式裡遇到,所以你必須一眼認出現在看到的是哪一代,才知道要去哪個交易碼做。
| 代 | 名稱 | 在標準程式裡長什麼樣 | 工具 | 特性 |
|---|---|---|---|---|
| 第一代 | User Exit(客戶出口/原始碼出口) | PERFORM userexit_xxx、SAPMV45A 的 MV45AFZZ 等 Z 開頭 Include | SE38 直接寫 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_instance | SE18 定義、SE19 實作 | 物件導向;可多重使用(Multiple Use)、可過濾(Filter);一個定義可有多個實作 |
| 第四代 | New BAdI + Enhancement Framework | GET BADI / CALL BADI;以及原始碼中的 ENHANCEMENT-POINT | SE18/SE19(新式)、SE80/SE38 的 Enhance 模式、ADT | Enhancement Spot 為容器;支援 Fallback Class、實作排序、Switch Framework;隱式增強點幾乎無所不在 |
① 先問「標準設定能不能做到?」(IMG/SPRO、容差、欄位選擇、訊息控制 OBA5)
② 做不到 → 找 BAdI(第三/四代)
③ 沒有 BAdI → 找 Enhancement Point(隱式/顯式)
④ 都沒有 → 才考慮 Modification(修改標準),需要 Access Key,且每次升級都要在 SPAU 對帳。
換句話說:能設定就不寫程式,能掛勾就不改標準。
2. 第三代 Classic BAdI 的運作原理
Classic BAdI 是一組互相搭配的物件,理解它們的關係,你才知道 SE18/SE19 每個畫面在做什麼:
| 物件 | 命名慣例 | 由誰產生 | 作用 |
|---|---|---|---|
| BAdI 定義(Definition) | BADI_MATERIAL_CHECK、Z_BADIDEF_BADITEST2 | SE18 建立 | 宣告「這裡有一個掛勾點」,資料存在 SXS_ATTR |
| BAdI 介面(Interface) | IF_EX_<定義名> | SE18 自動產生(可在 SE24 看) | 規範方法名稱與參數;實作類別必須實作它所有方法 |
| 介面卡類別(Adapter Class) | CL_EX_<定義名> | SE18 自動產生 | 執行期由它負責把呼叫轉發給所有作用中的實作 |
| BAdI 實作(Implementation) | Z_BADI_ERP000_CHECK | SE19 建立 | 對應一個實作類別 ZCL_IM_*,資料存在 SXC_EXIT/SXC_CLASS |
| Exit Handler | CL_EXITHANDLER | SAP 標準 | 靜態方法 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.
3. SE18:建立 Classic BAdI 定義(含多重使用與過濾器)
進入 SE18,輸入定義名稱(自建一律 Z/Y 開頭),按建立。畫面上有四個頁籤要填:(依講義步驟整理)
- Attributes(屬性):
- Type:Multiple Use(多重使用)/Filter-Dependent(依過濾器)
- Filter Type:勾了 Filter-Dependent 才要填,通常填一個 Data Element(例如 BUKRS、WERKS_D、LAND1)
- Instance Creation:預設每次呼叫產生新實例
- Interface(介面):填介面名稱,預設就是 ZIF_EX_<定義名>。存檔後按介面名稱會跳到 SE24,在那裡定義方法與參數。
- Subscreens(子畫面):需要在標準畫面上加自己的頁籤時才用。
- 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 而非例外 |
Classic BAdI 的實作類別必須實作介面的每一個方法。你只想寫 PROCESS_ITEM,但介面還有 10 個方法,那 10 個方法也必須存在(可以是空的 METHOD ... ENDMETHOD.)。只要有一個方法語法錯誤,整個類別無法啟動,於是整個 BAdI 實作等於沒作用——而且不會有任何錯誤訊息告訴你,你會以為「BAdI 沒被呼叫」。這是初學者最常卡住的地方。
4. SE19:建立 Classic BAdI 實作
進入 SE19,在「Create Implementation」區選 Classic BAdI,輸入 BAdI 定義名稱,再給實作名稱(Z 開頭),系統會產生實作類別 ZCL_IM_<實作名>。(依講義步驟整理)
- 輸入短文字說明。
- 切到 Interface 頁籤,會看到介面的所有方法列表。
- 雙擊要寫的方法 → 進入類別編輯器 → 寫程式 → 存檔。
- 回到 SE19 按 Activate(啟動)。沒有啟動的實作等於不存在。
- 指定傳輸請求(Workbench Request)。
實作的骨架就是這樣,記得第一行永遠是帳號/範圍防護:
METHOD if_ex_z_badidef_baditest2~method.
"★★ 防護:只對自己的帳號生效,避免干擾同一台機器的其他人 ★★
CHECK sy-uname = 'ERP000'.
"把傳進來的字串轉大寫後寫回去(CHANGING 參數)
parameter = to_upper( parameter ).
ENDMETHOD.
5. 自建第三代增強完整實例:Z_BADIDEF_BADITEST2
講義第 03 節的完整流程,把「定義 → 實作 → 呼叫」三件事一次走完,這是理解 Classic BAdI 最好的沙盤。
步驟 1:SE18 建立定義 Z_BADIDEF_BADITEST2,介面 ZIF_EX__BADIDEF_BADITEST2,在 SE24 為介面新增一個方法 METHOD,參數 PARAMETER 型別 STRING,方向 CHANGING。
步驟 2:SE19 建立實作,方法內容如下。
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 }|.
如果這個 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:從畫面找到程式,再在程式裡搜尋關鍵字(講義主線)
- 在交易畫面(例如 MIGO)按 F1 → 技術資訊,或選單 System → Status,取得程式名稱(MIGO 是 SAPLMIGO)。
- 進 SE38 / SE80 開啟該程式 → 選單 Utilities → Find in Source Code(或 Ctrl+F 勾「In main program」+「In includes」)。
- 搜尋這四組關鍵字,就能把四代增強一網打盡:
- CL_EXITHANDLER → 第三代 Classic BAdI
- GET BADI 或 CALL BADI → 第四代 New BAdI
- CUSTOMER-FUNCTION → 第二代 Customer Exit
- USEREXIT → 第一代 User Exit
- 找到後,看 exit_name = '...' 的字面值,那就是 BAdI 名稱(講義在 MIGO 找到的是 MB_MIGO_BADI)。
- 把候選 BAdI 拿到 SE18 看它的方法清單與文件,確認哪個方法在你要的時間點被呼叫。
找法 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_ATTR | Classic BAdI 定義的屬性(multiple use、filter type、介面名稱) |
| SXS_ATTRT | Classic BAdI 定義的文字說明(多語) |
| SXC_EXIT | Classic 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:第四代
找法 D:Debug 中設中斷點(最可靠,一定找得到「真正會被呼叫」的那一個)
- 執行交易 → /h 進入除錯器。
- 選單 Breakpoints → Breakpoint at → Method,輸入類別 CL_EXITHANDLER、方法 GET_INSTANCE。
- 按 F8 繼續,每停一次就看變數 exit_name,把過程中出現的 BAdI 名稱全部抄下來。
- 第四代則設 Breakpoint at Statement,輸入 GET BADI 或 CALL 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)在 MM01/MM02 存檔前檢查物料主檔資料。這是第三代 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.
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:沒有明確排序的多重來源,就會有不穩定的答案。
9. 第四代:Enhancement Spot 與 New BAdI 架構
第四代把「增強」抽象成兩個容器,觀念上就是「定義端」與「實作端」各有一個資料夾:
| 層級 | 定義端(SAP 或你建立) | 實作端(你建立) |
|---|---|---|
| 容器 | Enhancement Spot(增強點) | Enhancement Implementation(增強實作) |
| 內容 | BAdI Definition(可多個) | BAdI Implementation(可多個) |
| 最底層 | Interface(IF_*,名稱自由,不必 IF_EX_) | Implementing Class(ZCL_*) |
| 工具 | SE18 / SE80 / ADT | SE19 / SE80 / ADT |
| 另有一種 | Composite Enhancement Spot(把多個 Spot 群組起來) | Composite Enhancement Implementation |
New BAdI 相對第三代多出來的能力,每一個都很實用:
| 能力 | 說明 | 在哪裡設 |
|---|---|---|
| Fallback Class(後備類別) | 當沒有任何作用中的實作時,框架自動改叫這個類別。等於「內建的預設行為」 | BAdI 定義的屬性頁 |
| Default Implementation | 實作標成「預設」,只要有其他非預設實作作用中,它就自動退場 | BAdI 實作的屬性 |
| Implementation Sequence(排序) | 多重使用時可指定執行順序,解決第三代「順序不保證」的老問題 | BAdI 定義勾「Sortable」,在實作上填順序號 |
| Filter(過濾器) | 可定義多個過濾欄位,支援 =、BETWEEN、CP 等運算子與 AND/OR 組合 | BAdI 定義的 Filter 頁 + 實作上填值 |
| Instance Creation Mode | Newly(每次新建)/Reusing(重用同一實例)/Context-dependent | BAdI 定義屬性 |
| Switch Framework | 整個 Enhancement Implementation 可掛在 Switch 上,靠 Business Function 開關 | 建立時指定 Switch ID |
| Internal / External 呼叫控制 | SAP 可宣告某 BAdI 只供內部使用(客戶不得實作) | BAdI 定義的 Contract |
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 obj | Context-dependent BAdI | 少見,SAP 內部使用居多 |
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.
11. 自建第四代增強完整實例:ZBADI_DEMO01
講義第 04 節的完整清單,把四層物件的名稱都列出來了,這正是第四代的四層結構:
| 層級 | 物件名稱 | 建立位置 |
|---|---|---|
| Enhancement Spot | ZBADI_DEMO01 | SE18(新式) |
| BAdI Definition(Element definition) | ZBADI_DEMO01 | SE18 Spot 內 |
| Interface | ZBADI_DEMO_ITF01 | SE24(由 SE18 帶出) |
| Enhancement Implementation | ZBADI_DEMO01_IMP | SE19 |
| Implementing Class | ZBADI_DEMO01_CLASS | SE19 自動建立 |
步驟:(依講義步驟整理)
- SE18 → 輸入 Enhancement Spot 名稱 ZBADI_DEMO01 → Create。指定 Composite Spot(可留空)與 Package。
- 在 Spot 的 Enh. Spot Element Definitions 節點按右鍵 → Create BAdI,輸入定義名稱、勾選 Multiple Use/Limited Filter Use,填入介面名稱 ZBADI_DEMO_ITF01。
- 雙擊介面名稱進 SE24,新增方法 PRINT:參數 INPUT(IMPORTING, STRING)、OUTPUT(EXPORTING, STRING)。啟動。
- 回 SE18 啟動 Spot;再進 SE19 → New BAdI → 輸入 Enhancement Spot → 建立 Enhancement Implementation ZBADI_DEMO01_IMP → 建立 BAdI Implementation 與實作類別 ZBADI_DEMO01_CLASS。
- 雙擊方法寫程式 → 啟動。三個東西都要 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.
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。所以你原本寫好的程式碼可以原封不動貼過去。
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 個。講義原文把兩段長字串用 & 接成一個變數傳,會被截掉一半。
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 允許) | 加自訂欄位 |
操作步驟(在 SE38 或 SE80 開啟標準程式):(依講義步驟整理)
- 顯示模式開啟程式後,按工具列的 Enhance(螺絲起子+扳手圖示,或 Shift+F4),程式碼底色轉為灰/黃,進入增強模式。
- 選單 Edit → Enhancement Operations → Show Implicit Enhancement Options,程式碼中會出現一條條虛線——每一條虛線就是一個隱式增強點。
- 把游標放在要用的虛線上 → 右鍵 Enhancement Implementation → Create。
- 選 Code(Source Code Plug-In),輸入 Enhancement Implementation 名稱(Z 開頭)與短文字,選 Package 與傳輸請求。
- 系統在該位置產生一組 ENHANCEMENT ... ENDENHANCEMENT 區塊(底色亮黃),在裡面寫程式。
- 存檔 → 啟動(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 就不要用它。
14. 顯式增強點:ENHANCEMENT-POINT 與 ENHANCEMENT-SECTION
顯式(Explicit)增強點是 SAP 或你自己在原始碼裡明寫出來的掛勾點,比隱式安全得多,因為位置是被官方承諾的。它有兩種:
| 種類 | 語法 | 行為 |
|---|---|---|
| ENHANCEMENT-POINT | 單一敘述,標出一個位置 | 你的程式碼插入在該位置,原始碼照常執行 |
| ENHANCEMENT-SECTION | 一對 ENHANCEMENT-SECTION … END-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.
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.
16. 三、四代增強全圖解與 MM 常用 BAdI
講義 02-22 問「能圖解整個第三四代增強嗎?」。用表格把兩代的對應關係並排,比畫圖更好查:
| 面向 | 第三代 Classic BAdI | 第四代 New BAdI |
|---|---|---|
| 容器 | 無(定義即最上層) | Enhancement Spot → BAdI Definition |
| 介面命名 | 強制 IF_EX_<定義名> | 自由命名 |
| 介面卡 | CL_EX_<定義名>(自動產生) | 框架內部處理,看不到介面卡 |
| 取得實例 | CL_EXITHANDLER=>GET_INSTANCE | GET 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_EXIT | ENHSPOTHEADER / ENHHEADER / ENHOBJ |
| 建立工具 | SE18 / SE19(Classic 模式) | SE18 / SE19(New 模式)、SE80、ADT(Eclipse) |
| ABAP Cloud | 不可用 | 只有 SAP Released 的 BAdI 可用 |
MM 模組最常用的 BAdI(講義 02-23):
| BAdI | 觸發交易 | 常用方法 | 用途 |
|---|---|---|---|
| MB_MIGO_BADI | MIGO | INIT、PBO_DETAIL、PAI_DETAIL、CHECK_ITEM、CHECK_HEADER、POST_DOCUMENT | MIGO 畫面層:加自訂頁籤、逐行檢查、過帳後處理 |
| MB_DOCUMENT_BADI | MIGO、MB01、MB1A/B/C、BAPI 過帳 | MB_DOCUMENT_BEFORE_UPDATE(對話中,可擋)、MB_DOCUMENT_UPDATE(更新工作,不可發 E) | 物料文件文件層:整份檢查、寫 Z 表 |
| ME_PROCESS_PO_CUST | ME21N/ME22N/ME23N | PROCESS_HEADER、PROCESS_ITEM、CHECK、POST | 採購單抬頭/項目的改值與檢查 |
| ME_PROCESS_REQ_CUST | ME51N/ME52N | 同上(請購單版) | 請購單的改值與檢查 |
| ME_GUI_PO_CUST | ME21N 系列 | SUBSCRIBE、MAP_DYNPRO_FIELDS | 在採購單畫面加自訂頁籤/欄位(須與 ME_PROCESS_PO_CUST 搭配) |
| BADI_MATERIAL_CHECK | MM01/MM02 | CHECK_DATA | 物料主檔存檔前檢查 |
| MB_MIGO_ITEM_BADI | MIGO | — | MIGO 行項目層的額外處理 |
| MB_CIN_MM07MFB7 | MIGO/MB* | — | 過帳前對 MSEG 的最後調整 |
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 溝通 |
・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 建立。
① 在 ADT 開啟該物件,看 Properties → API State,顯示「Released for Cloud Development / Use System-Internally」。
② 在 SAP 的 API Business Hub 或 Released Object 清單查詢。
③ 系統內可查 CDS View I_APIRELEASESTATE 之類的釋出狀態視圖(版本相依)。
On-Premise 的 S/4HANA 仍可用 ABAP 的標準語言版本(Standard ABAP)寫所有舊寫法,但 SAP 建議新開發一律用 ABAP Cloud 語言版本,只有真的做不到時才降級——這叫 Tiered Extensibility Model(三層優先序)。
・新做的增強,先在 SE18 查該 BAdI 是不是 Released;是的話盡量用 ADT 做,未來搬雲端不必重寫。
・隱式增強點只在沒有 BAdI 且時程緊迫時使用,並在增強文件裡寫下「這是技術債,未來要換成 BAdI」。
・自訂欄位優先用 Key User 的 Custom Fields(會自動延伸到 CDS/OData/Fiori),而不是 Append Structure + 手改畫面。
18. 升級與傳輸:SPAU、SPAU_ENH、SPDD 與傳輸注意事項
| 交易碼 | 處理對象 | 什麼時候跑 |
|---|---|---|
| SPDD | DDIC 物件的修改(表、資料元素、Domain) | 升級中(Phase:ACT_UPG 期間),不做會掉資料 |
| SPAU | Repository 物件的修改與修復(程式、FM、類別、畫面) | 升級/SP 之後 |
| SPAU_ENH | Enhancement Framework 的增強(增強點實作、BAdI 實作、Class Exit) | 升級/SP 之後,第四代增強專用 |
| SE95 | Modification 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( ).
① BAdI 定義與實作是兩個不同的物件,可能落在不同的傳輸單。只傳實作、忘了傳定義(自建 BAdI 時),目標系統會啟動失敗。
② Enhancement Implementation 與它產生的 Class 也是兩個物件;SE19 通常會一起放進同一個請求,但手動改過名稱/Package 之後就不一定。傳輸前用 SE09 展開請求內容逐項確認。
③ 啟動狀態不會自動帶過去:在開發機是 Active,到品保機若相依物件缺漏就會變成 Inactive,而且不會報錯,只是不生效。傳完一定要在目標系統用 SE19 確認狀態。
④ 隱式增強相依於標準程式的版本。開發機是 SP10、正式機是 SP08,同一個隱式增強點可能根本不存在,傳輸會失敗或位置跑掉。
⑤ Classic BAdI 實作在同一個 BAdI 上,非 Multiple Use 時只能有一個作用中。目標系統若已經有別人(或 SAP Note)的實作,你的傳過去會啟動失敗。
練習與自我檢核
- (講義 01 節) 建立一個 Classic BAdI 實作,在 ME22N 存檔時,把料號 CHLK1300 的採購單項目自動打上交貨完成旗標。
提示:BAdI = ME_PROCESS_PO_CUST,方法 PROCESS_ITEM,用 im_item->get_data( ) 取值、改 ELIKZ、im_item->set_data( ) 寫回。用 MD04 前後對照驗證。別忘了 CHECK sy-uname = 'ERP000'。 - 為什麼「同一個 interface 底下的所有 method 都要正確,否則會無效」?
答案要點:實作類別必須實作介面的全部方法才能啟動;任一方法語法錯誤 → 類別無法啟動 → BAdI 實作形同不存在,而且執行時不會報錯,只是靜靜地沒作用。 - 你在 SE38 開啟 SAPLMIGO,要找出所有第三代與第四代的增強點,分別搜尋哪些關鍵字?
答案:第三代搜 CL_EXITHANDLER;第四代搜 GET BADI 與 CALL BADI;順便搜 CUSTOMER-FUNCTION(第二代)與 USEREXIT(第一代)。 - 在除錯器裡要一次列出「這筆交易到底呼叫了哪些 Classic BAdI」,要怎麼設中斷點?
答案:Breakpoints → Breakpoint at → Method,類別 CL_EXITHANDLER、方法 GET_INSTANCE,每次停下來看變數 exit_name。 - 寫一段 ABAP,從 SXS_ATTR + SXS_ATTRT 撈出所有說明文字含「Purchase」的 BAdI 定義,用 CL_SALV_TABLE 顯示。
提示:見第 6 節「找法 B」的程式碼;記得用 @sy-langu 過濾語言,並用 LEFT OUTER JOIN(有些 BAdI 沒有中文說明)。 - (講義 02 節) 為什麼講義最後選了 MB_DOCUMENT_BADI 而不是 MB_MIGO_BADI?兩者的分工是什麼?
答案要點:需求是「整張收貨單累加後判斷是否超交」,屬文件層檢查;而且 MB_DOCUMENT_BADI 連 BAPI 與背景過帳都會經過,MB_MIGO_BADI 只在 MIGO 畫面生效。 - MB_DOCUMENT_BEFORE_UPDATE 與 MB_DOCUMENT_UPDATE 有什麼致命差別?
答案:前者在對話工作程序,可以發 MESSAGE E 擋下使用者;後者在更新工作,發 E 會造成更新中止、資料不一致,只能寫 Log 或用 MESSAGE ... TYPE 'E' INTO。 - 第 12 節那段超交檢查程式裡,如果把 IF lt_po IS INITIAL. RETURN. ENDIF. 拿掉會發生什麼事?
答案:FOR ALL ENTRIES 遇到空的驅動表,會忽略整個 WHERE 條件,把整張 EKPO/EKBE 撈回來,在正式環境等於系統癱瘓。 - 解釋隱式增強點與顯式增強點的差別,並各舉一個 MIGO 情境。
答案要點:隱式是 SAP 在每支程式的固定位置(程式/FORM/METHOD/FUNCTION 的頭尾)自動提供、看不見、不保證上下文;顯式是 SAP 在原始碼裡明寫 ENHANCEMENT-POINT/ENHANCEMENT-SECTION、位置被承諾。優先用顯式與 BAdI,隱式是最後手段。 - ENHANCEMENT-POINT 與 ENHANCEMENT-SECTION 最重要的行為差異是什麼?
答案:POINT 是插入(原始碼照常執行);SECTION 是取代(被包住的原始碼不再執行),所以要自己把原邏輯抄一份回來。 - Pre-Exit、Post-Exit、Overwrite-Exit 各在什麼時候用?哪一個最不該用?
答案:Pre 改輸入、Post 改輸出、Overwrite 整個換掉。Overwrite 會讓 SAP 對該方法的所有後續修正失效,最不該用。 - New BAdI 的 Fallback Class 解決了 Classic BAdI 的什麼痛點?
答案:Classic BAdI 沒有實作時回傳空參照,呼叫端必須自己寫 IS BOUND 判斷與預設邏輯;Fallback Class 讓框架在無實作時自動執行預設行為,呼叫端程式碼變乾淨。 - 為什麼 GET BADI 不能放在迴圈裡?
答案:GET BADI 要查詢實作清單、計算過濾條件,成本高。正確做法是迴圈外 GET 一次、迴圈內 CALL 多次。 - 如果你的 BAdI 實作需要在 OPEN 撈一批資料快取,給後面每次 PROCESS_ITEM 使用,第四代要怎麼設定?第三代只能怎麼做?
答案:第四代把 Instance Creation Mode 設成 Reusing;第三代沒這個選項,只能用 CLASS-DATA 靜態屬性,但要小心跨交易的髒資料,記得在 OPEN 時先清空。 - 升級後要跑哪三個交易碼來處理修改與增強?各自負責什麼?
答案:SPDD(DDIC,升級中就要做)、SPAU(Repository 修改)、SPAU_ENH(Enhancement Framework 增強)。 - 在 ABAP Cloud(ABAP for Cloud Development)裡,第 5 節那支 Z_BUSINESSADDIN 測試程式為什麼一行都跑不動?
答案要點:CL_EXITHANDLER 不是 Released API、Classic BAdI 不被允許、WRITE 清單輸出不被允許。正確做法是實作 SAP Released 的 Enhancement Spot,或用 RAP + Fiori。 - 綜合題:需求是「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 開請購單。」
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 傳 BDoc | CRM 與 ERP 之間 | BDoc 的本質是 qRFC;流程是 CRM → BDoc → IDoc/XML/BAPI 結構 → 外部系統 |
IDoc 的出站與入站四步驟(面試常考):
- 出站:① 應用層產生應用資料 → ② ALE 層建立出站 IDoc → ③ 通訊層尋找夥伴與埠 → ④ 發送 IDoc。
- 入站:① 通訊層接收入站埠資料 → ② ALE 層建立入站 IDoc、依夥伴參數檔找 IDoc 處理類型 → ③ 應用層寫入文件。
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 屬性,就表示能讓外部程式執行。這是整章的核心句。
3. RFC 的五種型態:sRFC / aRFC / tRFC / qRFC / bgRFC
RFC 是 SAP 系統之間、以及 SAP 與外部系統之間程式通訊的基本介面技術,BAPI、ALE 等更高層技術都是基於 RFC 實現。依通訊方向分三種情境:
- 兩個獨立的 SAP 系統之間通訊。
- SAP 作為呼叫端,去呼叫外部遠端系統(非 SAP ABAP 系統)。
- 外部系統作為呼叫端,來呼叫 SAP。
依同步性與可靠度,共有五種型態。選錯型態是介面專案最貴的錯誤:
| 型態 | 全名 | 特性 | 監控 TCODE | 典型用途 |
|---|---|---|---|---|
| sRFC | Synchronous RFC | 同步,呼叫端等回應;目標系統必須在線,否則直接短拋(DUMP) | ST22 | 即時查詢:查庫存、查價格、查客戶信用 |
| aRFC | Asynchronous RFC | 非同步,不等回應(STARTING NEW TASK);可用 RECEIVE RESULTS 回收結果;目標仍須在線 | SM50 | 平行處理、把大批資料切成多工同時跑 |
| tRFC | Transactional RFC | 加 IN BACKGROUND TASK;先寫進 ARFCSSTATE / ARFCSDATA,COMMIT WORK 後才送出;保證只執行一次,但不保證順序 | SM58 | ALE/IDoc 傳輸、目標系統可能離線的情境 |
| qRFC | Queued RFC | tRFC + 佇列,保證 只執行一次且照順序;分出站佇列(Outbound)與入站佇列(Inbound) | SMQ1 / SMQ2 / SMQR | CRM Middleware 的 BDoc、必須保序的主資料變更 |
| bgRFC | Background RFC | NetWeaver 7.0 之後推出,取代 tRFC/qRFC;分 Type T(無序單元)與 Type Q(有序單元);用類別 API 而非語法附加 | SBGRFCMON / SBGRFCCONF | S/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.
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 Host | Java/.NET/Python 註冊型伺服器、SAP Gateway 外掛 |
| G(HTTP to External Server) | 對外部 HTTP/REST 端點 | ABAP 呼叫外部 REST API(配 CL_HTTP_CLIENT) |
| H(HTTP to ABAP System) | 對另一套 ABAP 系統的 HTTP | OData/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 Test 與 Remote 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.
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 value | Import/Export/Changing 全部要勾 Pass Value;不能用 reference | SE37 介面畫面把 值傳遞 欄位打勾 |
| 型別必須是 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 表 |
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)
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.NET | Windows 端 WinForm/ASP.NET 常用;需注意版本與 x86/x64 一致 |
| RFC SDK | C / C++ | 所有連接器的底層;新版為 NW RFC SDK(舊 Classic RFC SDK 已停止支援) |
| PyRFC | Python | 依賴 NW RFC SDK;適合資料工程、排程抓數、FastAPI 包裝成 REST |
| Lotus Connector | Lotus 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_method、rfc/reject_expired_passwd;UCON(Unified Connectivity)也可能把函數擋在外面,要在 UCONCOCKPIT 把函數放進 Communication Assembly。
- 症狀:外部呼叫回 Function module ... is blacklisted 或 not released for remote calls,但在 SE37 本機測試完全正常。
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 | 用途 |
|---|---|
| BAPI | BAPI Explorer:依應用元件階層瀏覽所有已釋出(Released)的 BAPI,可看文件、參數、範例 |
| SWO1 | Business Object Builder:顯示/維護業務物件類型,看它的 Key、Method、Attribute、Event |
| SWO2 | BOR 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 為例:
- 進入 VA01 畫面 → 功能表 System → Status。
- 在交易代碼欄位上雙擊,進入交易定義,找到它的 Package(開發類別)= VA。
- 用 SE80 打開 Package VA。
- 展開 Business Engineering → Business Object Types(業務工程 → 業務物件類型)。依業務需求判斷,要建立銷售訂單,BUS2032(Sales Order) 可能性最大。
- 雙擊 BUS2032 打開 Methods,尋找和「建立銷售訂單」同名的方法——這裡就是 SalesOrder.CreateFromDat2。把游標停在該行,點上方的「程式」按鈕,就能看到原始碼,找到 BAPI_SALESORDER_CREATEFROMDAT2。同樣方法可以找到修改(CHANGE)、刪除的 BAPI。
- 也可以雙擊 SalesOrder.CreateFromDat2 這一行,在彈出視窗中找到 ABAP 子畫面:如果單選鈕是「API 功能」,名稱欄就是我們要找的 BAPI;如果是「函數模組」,那就是一支一般 FM。
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 查完整記錄 |
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.
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.
11. 請購單 BAPI:BAPI_PR_CREATE 與 BAPI_REQUISITION_CREATE
請購單(Purchase Requisition,資料表 EBAN)有兩支建立 BAPI,這是新手最容易搞混的地方。老師在程式註解裡直接寫了官方參考:Note 499627 — FAQ BAPIs for purchase requisitions。
| 比較 | BAPI_PR_CREATE | BAPI_REQUISITION_CREATE |
|---|---|---|
| 推出時間 | ECC 6.0 之後的新版(ME_REQUISITION 系列) | 舊版(4.6 時代) |
| 結構 | PRHEADER / PRHEADERX / PRITEM / PRITEMX(有 X 結構) | REQUISITION_ITEMS(無 X 結構) |
| 回傳單號 | EXPORTING NUMBER | EXPORTING 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.
舊版 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.
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.
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_UPDATE | BP 建立或修改時的 BAdI/Exit |
| ADDRESS_UPDATE | BP 地址(通訊)資料維護 |
13-3 BP / 客戶 / 供應商 的資料表清單
| 群組 | 資料表 | 內容 |
|---|---|---|
| BP | BUT000 | BP 一般資料(BP 的核心表) |
| BUT020 | BP 地址(連到 ADRC) | |
| BUT100 | BP 角色(FLCU01、FLVN01⋯) | |
| BUT0BK | BP 銀行 | |
| BUT05* | BP Relation(夥伴關係) | |
| BUT0CC | Payment(付款卡/付款資料) | |
| 客戶 | KNA1 | 客戶基本資料 |
| KNB1 | 客戶公司代碼 | |
| KNVV | 客戶 SD 資料 | |
| KNVI | 客戶稅別標識 | |
| KNAS | 加值稅登記號(VAT) | |
| KNB5 | 催款資料 | |
| KNBK | 銀行明細 | |
| KNVA | Loading Point | |
| KNVD | 銷售查詢表(Output for Document) | |
| KNVL | 許可證(Licenses) | |
| KNVS | Shipping Point | |
| KNEX | Legal Control - Sanctioned Party List | |
| KNZA | 備選付款人 | |
| KNKK | 信貸管理(S/4 已改 FSCM,見警告) | |
| KNC1 | 客戶註記錄(過帳金額) | |
| VCKUN | 信用相關 | |
| VCNUM | 信用相關 | |
| 供應商 | LFA1 | 供應商主資料 |
| LFB1 | 供應商公司代碼 | |
| LFM1 | 供應商採購視圖 | |
| LFAS | Tax Number |
① BAPI_CUSTOMER_CREATE / BAPI_VENDOR_CREATE 已不可用。建客戶/供應商一律走 BP:優先用 CL_MD_BP_MAINTAIN=>maintain( ) 或 CVI_EI_INBOUND_MAIN/CMD_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_CUSTOMERNO | BAPICUSTOMER_ID-CUSTOMER | 客戶代碼(供應商也用同一個代碼查) |
| I_PI_SALESORG | BAPIKNA102-SALESORG | 銷售組織 |
| I_VEN | CHAR1(選填) | = 'X' 時一併抓供應商資料 |
| LT_CUSTOMERADDRESS | BAPICUSTOMER_04 | 客戶地址 |
| LT_CUSTOMERGENERALDETAIL | BAPICUSTOMER_KNA1 | 客戶一般資料 |
| LT_RETURN | BAPIRET1 | 客戶查詢狀態 |
| LT_PE_PERSONALDATA | BAPIKNA101_1 | 聯絡人個人資料 |
| LT_PE_COMPANYDATA | BAPIKNA106 | 聯絡人公司資料 |
| LT_VENDORGENERALDETAIL | BAPIVENDOR_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_LIST | ATP 可用量 / MD04 庫存需求表 | 見第 16 節 | |
| SD | BAPI_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) | — | |
| FI | BAPI_ACC_DOCUMENT_POST | 過帳會計憑證(FB01/FB50) | S/4 資料落在 ACDOCA |
| BAPI_INCOMINGINVOICE_CREATE | 採購發票驗證(MIRO) | 另有 _PARK、_CHANGE、_CANCEL | |
| BAPI_AR_ACC_GETOPENITEMS / BAPI_AP_ACC_GETOPENITEMS | 應收/應付未清項 | — | |
| PP | BAPI_PRODORDCONF_CREATE_TT | 生產工單確認(時間事件 Time Ticket,CO11N) | 另有 _CREATE_HDR(抬頭確認)、_CANCEL |
| BAPI_PRODORD_CREATE / _RELEASE / _CHANGE | 生產工單建立/下達 | — | |
| BAPI_PLANNEDORDER_CREATE / _CHANGE | 計畫訂單 | — | |
| BP | BAPI_BUPA_CREATE_FROM_DATA 系列 | 商業夥伴 | 見第 13 節 |
| CL_MD_BP_MAINTAIN / CMD_EI_API / VMD_EI_API | S/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.
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. "★ 客製欄位由這裡進去
另外 VALUEPART 的切段位置一定要對齊結構長度,錯位會造成欄位內容整段偏移。
16. MD04 / MMBE 與 BOM 反查:讀取型 API
講義第 03 節列了三支「MD04 / MMBE」相關的 API,這是做 PP 缺料表(老師專案要點的第一項)最常用的:
| 函數 | 對應交易 | 說明 |
|---|---|---|
| MD_STOCK_REQUIREMENTS_LIST_API | MD04 | 庫存需求表,回傳 MDPSX(明細行)、MDEZX(含日期彙總)、MDSUX(彙總)。非 RFC-enabled,要對外要自己包一層 |
| BAPI_MATERIAL_STOCK_REQ_LIST | MD04 | 官方 BAPI 版本,有 RFC 屬性,可直接被外部呼叫 |
| BAPI_MATERIAL_AVAILABILITY | CO09 / ATP | 可用量檢查(講義寫成 API_MATERIAL_AVAILABILITY,正確函數名是 BAPI_MATERIAL_AVAILABILITY) |
| CS_WHERE_USED_MAT | CS15 | 物料被用在哪些 BOM——會找多階 |
| CSEP_MAT_BOM_SELECT_WHERE_USED | CS15 | 同上,但僅找上一階 |
*&-------------------------------------------------------------*
*& 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' ...
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 單元失敗 | SBGRFCMON | Supervisor Destination 沒設(SBGRFCCONF) |
| 外部呼叫被拒 | SU53、RS_RFC_BLACKLIST_CUSTOM、UCONCOCKPIT | S_RFC 權限不足、函數在黑名單、UCON 未開放 |
| BAPI 回 E 但看不懂 | SLG1(用 RETURN 的 LOG_NO)、SE91(用 ID+NUMBER) | 訊息變數 V1~V4 沒帶出來 |
| 資料建好了卻查不到 | 檢查 COMMIT 的 WAIT = 'X' | 更新任務還沒完成就去 SELECT |
| 更新任務失敗 | SM13 | COMMIT 之後在 update task 才炸掉,前台看不到錯誤 |
| 介面錯誤要給業務看 | /AIF/ERR | AIF(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 ).
18. S/4HANA 的介面策略:BAPI 之後該用什麼
BAPI 仍然可用,但 S/4HANA 的官方優先順序已經改變。做新專案時建議按這個順序選:
| 優先序 | 技術 | 說明 | 入口 |
|---|---|---|---|
| 1 | Released OData API | SAP 官方釋出、有版本承諾(C1 Release Contract),例如 API_BUSINESS_PARTNER、API_PURCHASEREQ_PROCESS_SRV、API_PURCHASEORDER_PROCESS_SRV、API_PRODUCT_SRV、API_SALES_ORDER_SRV、API_MATERIAL_STOCK_SRV | SAP Business Accelerator Hub(原 API Business Hub) |
| 2 | Released SOAP / Event | 非同步大量傳輸;SAP Event Mesh 可訂閱業務事件 | SOAMANAGER |
| 3 | Released RFC / BAPI | 仍是效能最好的地端整合;看 BAPI 是否標為 Released(ADT 中的 API State = Use System-Internally / Released) | BAPI、SE37 |
| 4 | CDS View / ABAP RAP | 自己開介面:CDS + Service Definition + Service Binding,一鍵產生 OData V2/V4 | ADT(Eclipse) |
| 5 | AIF(Application Interface Framework) | 介面的監控、錯誤處理、重送、對帳框架;把介面訊息、Log、權限統一管理,業務單位可自行重送 | /AIF/ERR、/AIF/CUST |
| — | SAP Cloud Connector | BTP 雲端 App 要打地端 S/4 時的安全通道;逐一白名單資源與函數 | SCC 管理介面 |
① 客戶/供應商 → 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 Check 與 Custom Code Migration(ATC 的 S4HANA_READINESS 檢查)掃一遍。
練習與自我檢核
- 介面選型:一個電商網站要「即時查 SAP 庫存」和「每天晚上把 5 萬筆訂單丟進 SAP」。這兩個需求各該用哪種 RFC 型態?為什麼?
要點:即時查用 sRFC(同步、要立刻回答);大批丟單用 qRFC/bgRFC(保證只做一次且保序、目標忙碌時可排隊),或者用 IDoc/AIF。 - tRFC 沒動靜:程式寫了 CALL FUNCTION ... IN BACKGROUND TASK DESTINATION ...,SM58 看到一筆記錄卻永遠不執行,最可能是什麼?
要點:忘了 COMMIT WORK。次要可能:目的地帳號鎖住、目標系統沒有那支函數。 - RFC-enabled FM 的三個硬性限制是什麼?如果我想用 class-based exception 當介面例外,該怎麼辦?
要點:① 所有參數 pass by value;② 型別必須是 DDIC 型別;③ 不可有 Dynpro 互動(WRITE/CALL SCREEN)。class-based exception 不能當介面例外,改成內部 TRY/CATCH、外部用 BAPIRET2 表回傳。 - 判斷 BAPI 成功失敗:為什麼不能用 IF sy-subrc NE 0?講義第 08 節的 ZPO_CHANGE 就犯了這個錯,請改寫。
要點:BAPI 呼叫後 sy-subrc 幾乎永遠是 0。要判 RETURN 表有沒有 TYPE = 'E' 或 'A'(用 line_exists( lt_return[ type = 'E' ] ))。 - COMMIT 的 WAIT:什麼情況一定要給 WAIT = 'X'?什麼情況可以省略?
要點:COMMIT 後馬上要用產生的單號去 SELECT 或做下一個 BAPI,必須 WAIT='X';純批次寫入且後續不立即讀取,可省略以換取速度。 - X 結構:BAPI_MATERIAL_SAVEDATA 的 PLANTDATAX-PLANT 應該填 'X' 還是填工廠代碼?為什麼?
要點:填工廠代碼。X 結構中 KEY 欄位填真值,只有非 KEY 的資料欄位才填 'X'。 - 找 BAPI:只知道交易碼 ME51N,請用老師教的方法找出建立請購單的 BAPI。
要點:System → Status 雙擊 TCODE 找 Package → SE80 開 Package → Business Object Types → 找 BUS2105(Purchase Requisition)→ 看 Method → 對應 FM。或直接在 SE37 搜 BAPI_PR_*。 - 兩支請購 BAPI:BAPI_PR_CREATE 與 BAPI_REQUISITION_CREATE 有何差異?該查哪一份 SAP Note?
要點:前者有 X 結構、欄位較新,S/4 建議用;後者是舊版無 X 結構。Note 499627 - FAQ BAPIs for purchase requisitions。 - 客製欄位:客戶要在採購單項目加一個 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 步不會有任何錯誤訊息。 - RFC 黑名單:外部呼叫回報「function module is blacklisted」,但 SE37 測試完全正常,怎麼處理?
要點:執行報表 RS_RFC_BLACKLIST_CUSTOM 把該 FM 從黑名單剔除;另檢查 UCON(UCONCOCKPIT)與 S_RFC 權限。 - 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 同步出來的。 - 缺料表:要做一支「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。 - 條件的坑:老師強調「條件用區間跟用列舉時答案會不同,用列舉的才正確」,這件事在設計 RFC 介面時要怎麼落實?
要點:介面參數開成 SIGN/OPTION/LOW/HIGH 的 range 表,外部系統以 OPTION = 'EQ' 一筆一筆列舉;不要在介面內部自作聰明把使用者給的清單壓成 BT 區間。 - 不穩定的結果:一支介面函數每天跑出來的數字都不一樣,程式邏輯卻沒改,最可能的兩個原因是什麼?
要點:① 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。
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 OUTPUT | PBO(PROCESS BEFORE OUTPUT) |
| 輸入後程式 | AT SELECTION-SCREEN | PAI(PROCESS AFTER INPUT) |
| 預設螢幕編號 | 1000(標準選擇畫面) | 自己指定,慣例 0100、0200… |
| Dynpro Type | SELECTION-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. " 也能把第二個畫面做成彈窗
2. 程式骨架與 INCLUDE 命名慣例
Dialog 程式的程式碼會被拆成多支 INCLUDE,這不是規定而是SAP 的強慣例(SE80 建 Module Pool 時會自動照這個規則產生,Screen Painter 精靈也會照這個規則塞程式)。
| INCLUDE 尾碼 | 內容 | 另一種常見寫法 |
|---|---|---|
| ZXXX_TOP | 全域宣告:DATA、CONSTANTS、CONTROLS、TABLES、CLASS 定義 | ZXXX_TOP |
| ZXXX_O01 | PBO 的 MODULE … OUTPUT | ZXXX_PBO / ZXXX_O99 |
| ZXXX_I01 | PAI 的 MODULE … INPUT | ZXXX_PAI / ZXXX_I99 |
| ZXXX_F01 | FORM 副程式(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
3. SE51 Screen Painter:螢幕屬性與元素
設計一個螢幕固定四個步驟:
- 在屬性(Attributes)畫面設置常規螢幕屬性(編號、類型、Next Screen、大小)。
- 在佈局編輯器(Layout)設計版面,把元素拖上去。
- 在欄位清單(Element list)設置欄位屬性。
- 在流邏輯編輯器(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 表 |
螢幕欄位可控制的四組屬性群
PBO 中用 LOOP AT SCREEN 可以動態改的屬性,共有四個群組欄位可用:
| 群組欄位 | 可控屬性 | 值 |
|---|---|---|
| GROUP1(修改群組) | INPUT 是否可輸入 | 0 / 1 |
| GROUP2 | OUTPUT 是否輸出 | 0 / 1 |
| GROUP3 | REQUIRED 必輸與否 | 0 / 1(S/4 亦支援 2=必輸但不顯示打勾) |
| GROUP4 | INVISIBLE 顯示與否(打星號,密碼用) | 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.
4. 第一支 Dialog:Flow Logic、OK_CODE 與 CALL SCREEN
第 41 個練習:從螢幕設計到實作。目標:在螢幕上貼一個「NCU SAP!」的 Text 和一個按鈕,按下按鈕就退出整個程式。
- 建立螢幕:1. 定編號(2001)、2. 設 NORMAL 屬性、3. 設 Next Screen 屬性。
- 在 Layout 貼上 Text Field「NCU SAP!」與一個 Pushbutton,Pushbutton 的 FctCode = 'EXIT'。
- 在 Element list 加一個欄位 OK_CODE(型別 SY-UCOMM),這是接收功能碼的出口;同名變數也要在主程式宣告。
- Flow logic 裡把 MODULE USER_COMMAND_2001 的註解拿掉,雙擊建立模組。
- 主程式用 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.
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 screen | Dynpro Type = Normal 的全螢幕 |
| Dialog box | Dynpro Type = Modal Dialog Box 的彈出視窗(工具列會自動縮簡) |
| Context menu | 右鍵選單 |
Function Type(功能碼型別)——最容易被忽略卻最關鍵
| Function Type | 行為 | 用途 |
|---|---|---|
| (空白) | 一般功能碼。會完整跑完 PAI(包含所有 FIELD 檢查)才進到 USER_COMMAND | 存檔、執行、查詢 |
| E | Exit command。會跳過所有欄位檢查,直接執行 MODULE … AT EXIT-COMMAND | BACK / EXIT / CANCEL |
| S | 系統功能(不進使用者程式) | 系統保留 |
| T | 直接呼叫另一個 T-CODE | 跳轉 |
| P | 本地處理(不觸發 PAI 到伺服器) | Tabstrip 本地捲動、Table Control 欄寬調整 |
*&---------------------------------------------------------------------*
*& 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.
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
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.
在選擇畫面(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.
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.
寫法二:自宣告工作區(現代寫法,建議)
*&---------------------------------------------------------------------*
*& 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.
鎖定與資料一致性
" 進入編輯模式前一定要鎖,離開時一定要解
" 前提: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.
9. 欄位合理性檢查:FIELD、CHAIN、ON INPUT / ON REQUEST
第 05 節。PAI 裡最重要的兩個關鍵字是 FIELD 與 CHAIN。它們決定了「檢查失敗時,哪些欄位會保持可輸入狀態」——這是 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 Values | Domain 值域 | SE11 Domain 的 Value Range |
| Foreign key check | 外來鍵值 | SE11 欄位的 Foreign Key |
| (第五層)自訂邏輯 | 跨欄位、跨表的商業規則 | PAI 的 FIELD / CHAIN MODULE |
10. 訊息控制 SE91 與 Dialog 中的訊息行為
第 06 節。自定義訊息類別(Message Class)要以 Y 或 Z 起頭,用 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)中的行為 |
|---|---|---|
| S | Success 成功 | 訊息顯示在下一個畫面的狀態列。假設由 Screen 100 跳到 200,訊息會等到 200 開啟時才出現 |
| I | Information 訊息 | 即時彈出對話方塊,按確定後繼續原流程 |
| W | Warning 警告 | 顯示在狀態列,欄位保持可輸入,按 Enter 可以繼續 |
| E | Error 錯誤 | 停在原畫面(Screen 100)且不重跑 PBO,只有 FIELD/CHAIN 涵蓋的欄位可再輸入 |
| A | Abend 異常終止 | 終止交易,回到 SAP Easy Access(Dialog 少用) |
| X | Exit | 產生 short dump(Dialog 少用) |
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.
12. Subscreen 子畫面
第 08 節、第 19 節。Dynpro(Dynamic Program)= 一個螢幕 + 它的 flow logic + controls。Subscreen 讓你把畫面切成可替換的區塊,是 Tabstrip 與 SAP 標準增強(Screen Exit)的基礎。
作法
- 在主畫面 Layout 貼上 Subscreen Area(命名例如 SUB1)。
- 新增螢幕 2002,Dynpro Type 一定要選 Subscreen;貼上要顯示的元素後啟動。
- 同法建立第二個子螢幕 2003。
- 主程式宣告一個變數決定要顯示哪一個:DATA gv_screenid TYPE sy-dynnr VALUE '2002'.
- 主畫面 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.
13. Tabstrip 表頁籤控制項
第 09 節。Tabstrip 就是「多個 Subscreen + 一排頁籤按鈕」。強烈建議用精靈(Wizard)產生,它會自動幫你建常數、CONTROLS 宣告、PBO/PAI 模組,並把程式塞進三支 INCLUDE。
精靈第一個選項:兩種捲動模式(一定要選對)
| 模式 | Subscreen Area | Function Type | 特性 |
|---|---|---|---|
| Scrolling local in tabstrip control(scroll in SAP GUI) | 一個頁籤一個 Subscreen Area | P | 切換頁籤不送伺服器,速度快;但所有頁籤的資料一次全部載入 |
| 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.
14. Table Control:觀念與精靈
第 12、13、20 節。Table Control 就是畫面上的「可編輯表格」。它跟 ALV Grid 的差別在於:Table Control 是 Dynpro 原生元件,欄位就是螢幕欄位,可以逐格控制屬性、逐格做 FIELD 檢查;而 ALV Grid 是 GUI Control(畫面外掛)。
| 比較 | Table Control | CL_GUI_ALV_GRID |
|---|---|---|
| 本質 | Dynpro 元件 | GUI Control(Custom Container) |
| 編輯 | 天生可編輯,欄位級檢查最細 | 要設 edit=X,檢查靠事件 |
| 排序/篩選/匯出 | 要自己寫 | 內建 |
| 資料量 | 幾百到幾千列還好(只送可見列) | 上萬列較佳 |
| 逐列不同屬性 | 非常容易(迴圈內改 screen) | 要用 style 表 |
| 建議 | 單據明細維護(表頭+明細) | 報表、大量清單 |
什麼時候會觸動 PAI?(可用 /h Debug 實測)
- 不會觸動 PAI:水平捲動、調整欄寬、移動欄位順序、選取行列。
- 會觸動 PAI:垂直捲動、儲存版面設定(Save layout)、按任何功能鍵。
Table Control Wizard 的九個步驟
- 建立程式與螢幕,並在 Element list 加 OK_CODE。
- 在 Layout 拉出 Table Control Wizard(範圍不要拉太小,否則欄位塞不下)。
- Name of the table control:命名(例如 TC_MARD)。
- Table / Structure:指定 DDIC 來源(選 Dictionary),以及對應的內表名。
- Definition of Columns:選取要顯示在畫面上的欄位。
- Table control attributes:可否編輯、可否選取列、標題列等。
- Select additional table maintenance functions:勾選可捲動、插入列、刪除列、全選、取消全選——勾了精靈才會產生對應的 FORM。
- Set Includes:指定四支 INCLUDE(TOP/O01/I01/F01)。
- Finished:完成後回主程式,把資料撈進內表,再 CALL SCREEN。
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.
(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 型別):
| 欄位 | 型別 | 說明 |
|---|---|---|
| MARK | CHAR1 | 選取欄(給刪除/全選用) |
| NEWROW | CHAR1 | 是否為新增列(自己加的旗標) |
| MATNR | MATNR | 物料 |
| WERKS | WERKS_D | 工廠 |
| LGORT | LGORT_D | 儲存地點 |
| LABST | LABST | 非限制使用庫存 |
| INSME | INSME | 品檢庫存 |
| SPEME | SPEME | 凍結庫存 |
| ICON | ICON-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.
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.
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.
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.
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.
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.
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.
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.
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 GUI | Module Pool + Table Control + Tabstrip | Dialog 仍是 GUI 上最完整的解 |
| 要在瀏覽器 / 手機上用 | RAP + Fiori Elements(CDS + Behavior Definition + OData V4) | Dialog 不可能上 Fiori Launchpad |
| 要擴充標準交易的畫面 | Screen Exit / BAdI 的 Subscreen | 本章第 12 節的跨程式 Subscreen 機制 |
| 雲端 ABAP(BTP / Public Cloud) | 只能 RAP | Dynpro 相關語法在 ABAP Cloud 中不在白名單內,語法檢查直接擋掉 |
21. 除錯與檢查清單
| 症狀 | 最可能的原因 |
|---|---|
| 按鈕按下去沒反應 | Pushbutton 沒填 FctCode;或畫面上沒有 OK_CODE 欄位;或 PAI 沒有 MODULE user_command |
| 按了 A 功能卻執行 B | ok_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 |
練習與自我檢核
- Type 1 vs Type M:不看筆記說出兩者在「副程式、顯示前、輸入後、螢幕編號、首末頁」五項的差別,並解釋為什麼老師說「都用 TYPE 1 寫程式比較方便」。
- 第 41 練習(螢幕設計到實作):建立一支空程式,在螢幕 2001 上貼「NCU SAP!」的 Text 與一個按鈕,按下就 LEAVE PROGRAM。
要點:畫面要有 OK_CODE 欄位、主程式要宣告同名變數、按鈕要填 FctCode、用 CALL SCREEN 2001 進入。 - 第 02 節(螢幕功能表):用 SE41 建 GUI Status 與 GUI Title,做出 MENU11/MENU12/MENU21/MENU22/ZCAS/EXIT 六個功能碼,並讓 ZCAS 拋 E 類訊息。
延伸:驗證「兩個 PF-STATUS 中同名的 Function Code 會走同一段程式」。 - E 類按鈕:故意把 CANCEL 的 Function Type 留空白,再讓某個欄位檢查失敗,體驗「跳不出去」;然後改成 E 並把 AT EXIT-COMMAND 移到 PAI 最前面。
- 第 03 節(Radio / Box / Group):貼兩個以上 Radio 並 Define group;把 FIELD2、FIELD3 設 Group1 = 'SEL' 使其不可輸入,FIELD1 設為必輸。
要點:LOOP AT SCREEN + MODIFY SCREEN,判斷值大寫。 - 第 04 節(維護 Z00_VBAK):用 Screen 顯示 Z00_VBAK,先用 TABLES 寫法、再改成自宣告工作區的寫法,比較「欄位抬頭是否自動出現」。
思考:為什麼老師說「筆數大的表直接放 Screen 上是不妥的做法」? - 第 05 節(合理性檢查):加入 FLD1/FLD2/FLD3 三個欄位,分別用 ON REQUEST、ON INPUT、無附加條件觸發 CHECK1~CHECK3,觀察三者觸發時機的差異。
延伸:把三個欄位包成 CHAIN,改用 ON CHAIN-REQUEST,比較「檢查失敗時哪些欄位還能輸入」。 - 第 06 節(SE91):建立訊息類別 YMESS,寫出 MESSAGE e005(ymess) WITH … 與 MESSAGE ID … TYPE … NUMBER … DISPLAY LIKE 'I' 兩種寫法,並說明 S/I/W/E 在「Screen 100 → Screen 200」情境下各自何時出現。
- 第 07 節(下拉選單):用 VRM_SET_VALUES 做出 TWD/USD 的 Listbox with key,再改成直接從 TCURC(或自建的 Z00_CURR)撈值。
思考:什麼時候該改用 Search Help 而不是 Dropdown? - 第 08 節(Subscreen):建立 2002/2003 兩個 Subscreen,用功能表 MENU11/MENU12 切換 gv_screenid。
要點:CALL SUBSCREEN 不可放進 LOOP/CHAIN;PBO 要帶 INCLUDING、PAI 不帶。 - Subscreen 七條鐵律:不看筆記默寫出來(不可放 LOOP/不可放 CHAIN/不必 OK_CODE/名稱不可重複/不會有 AT EXIT-COMMAND/不會有自己的 Titlebar 與 PF-STATUS 與 SET/LEAVE SCREEN/Dynpro Type 必須是 Subscreen)。
- 第 09 節(Tabstrip):用精靈建立 Tabstrip,觀察自動產生的三支 INCLUDE(常數 C_TAB、CONTROLS TAB、G_TAB 結構、TAB_ACTIVE_TAB_SET/GET)。
延伸:分別用「本地捲動(Function Type = P,一頁籤一區)」與「伺服器捲動(Function Type 空白,共用一區)」各做一次,比較差異。 - 第 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 各一次。 - 第 11 節(SMW0):用 SMW0 上傳一張公司 Logo,並用 DP_PUBLISH_WWW_URL + CL_GUI_PICTURE 顯示在畫面上。(需 BASIS 先配置。)
- 第 12 節(TC Wizard):建立 Z202202_41_TABLE,用 Table Control Wizard 以 SPFLI 為例做出可捲動的表格,並在主程式撈 1000 筆後 CALL SCREEN 2000。
要點:精靈的第 7 步一定要勾「插入/刪除/全選/取消全選」,才會產生對應 FORM。 - 第 13 節(不靠精靈):再建一個 dynnr 3000,改用一般的 Table Control(不是 Wizard),從 Dictionary 選欄位貼進去,然後手動把 TOP/PBO/PAI/FORM 四段程式補齊。
- 第 14 節(驗證所學):打開標準交易 OB58(財務報表版本維護),用 F1 → 技術資訊找出它的程式與螢幕編號,再用 SE51 觀察它的 Layout 與 flow logic,指出它用了哪些本章學過的元件。
- 第 15 節(Z00_VBAK Table Control):把 Z00_VBAK 做成表控制項,支援新增、刪除、存檔,並加上 ENQUEUE/DEQUEUE。
要點:先在 SE11 建一個含 MARK 欄的 Structure,Table Control 才吃得下。 - 第 16 節(ICON):完成 ZICON,用 ICON_CREATE 顯示 ICON_FAILURE,並找出講義原程式的錯誤(name 前面多一個空白)。
延伸:改用型別池 icon 的常數(icon_led_green 等),並到 SE16N 查資料表 ICON 找出五個常用圖示的 ID。 - 第 18 節(Group / MODIF ID):完成 ZRADIO 與 ZMODIFYID,做出「選自製 → 開放物料欄位;選採購 → 開放採購單號;勾核取方塊 → 才出現備註」。
要點:Modif ID 最多 3 碼、判斷值一律大寫、要有 USER-COMMAND 才會即時重繪、SCREEN-INPUT 與 SCREEN-ACTIVE 的差別。 - 第 20 節(PAI 觸發實測):用 /h 進 Debug,逐一測試「水平捲動、調欄寬、移動欄位、選取行列、垂直捲動、存版面」哪些會觸動 PAI,把結果寫下來。
- Table Control 功能碼:說出 TC 名稱為 TC_MARD 時,插入列、刪除列、下一頁、全選的功能碼各是什麼,並解釋 USER_OK_TC 是怎麼用 SEARCH + STRLEN 把動作切出來的。
- 三大 bug:不看筆記說出 Table Control 的三個經典 bug(沒設 lines/用 sy-tabix/沒 MODIFY)以及各自的症狀。
- 永久 vs 暫時:寫出一個範例,讓 Table Control 的「主鍵欄整欄唯讀」(永久性,改 tc-cols)而「偶數列的數量欄必填」(暫時性,LOOP 內改 screen)。
- 綜合實作:完成本章第 16 節的 ZTC_MARD_MAINT,並補上:(a) 排序按鈕、(b) 未存檔離開時的 POPUP_TO_CONFIRM、(c) 紅綠燈欄位、(d) ENQUEUE/DEQUEUE。
加分:把存檔對象改成自建 Z 表,並比對 gt_orig 只送真正異動的列。 - 拆解 ME21N:打開 ME21N,指出畫面上哪些區塊是 Subscreen、哪些是 Tabstrip、哪些是 Table Control,並說明「左側導航欄 + HEADER + ITEM OVERVIEW + ITEM DETAIL」的四層結構分別對應本章哪一節的技術。
- 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、可自動寄信的正式採購單,也知道遇到中文字型缺失、空白頁、金額不對齊、傳輸後函數名跑掉這些坑要怎麼救。
1. SAP 表單技術的演進與選用建議
SAP 從 R/2 時代到 S/4HANA,列印技術換過三代半。實務上你四種都會遇到,要能一眼分辨並知道該用哪個。
| 技術 | TCODE | 年代/定位 | 優點 | 缺點 |
|---|---|---|---|---|
| SAPscript(Form / Layout Set) | SE71(維護) SE72(樣式) SE78(圖檔) I18N(多語入口) | 最老。SAP 標準單據(發票、送貨單)很多還是它 | 執行快、資源少、跨語言穩、標準表單多 | 編輯器像文字模式、版面靠「視窗+段落」硬排、Client 相依(要 RSTXTRAN 才能傳輸)、多語言要一份份複製 |
| SmartForms | SMARTFORMS SMARTSTYLES | 4.6C 起。目前 ECC/S/4 客製單據的主力 | 圖形化設計、Table 節點自動分頁、不用寫排版指令、Client 獨立、產生 ABAP 函數效能好、可直接輸出 XSF/HTML | 格式太細緻時很難調、複雜表格會卡、沒有互動式表單、設計本身很花時間 |
| Adobe Forms(Interactive Forms by Adobe) | SFP | NW 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 不需插件,但太複雜的格式很難調校。」
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(存取方式,本機測試常用 L/C,前端印表機用 F)。
- 裝置類型的選擇是中文能不能印出來的關鍵:Windows 前端印表機用 SWIN/SAPWIN;繁中/簡中環境用 CNSAPWIN、CNHP4;要輸出 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 才選得到。
2-2 RSCPSETEDITOR:取消 MS Word 編輯器
SmartForms 的文字節點有兩種編輯器:PC Editor(SAP 內建)與 MS Word 編輯器。SAP HANA 平台之後預設強制走 MS Word。
意思是:知道有 RSCPSETEDITOR 這支程式(SE38 執行,可切換文字編輯器預設值)就好,正式系統不要亂動——它是 client 全域設定,改了全公司的 SO10、SmartForms、SAPscript 編輯器都跟著變,出問題很難回頭。真的要改請走變更流程並由 Basis 執行。
為什麼會想關掉 MS Word?因為用了 MS Word 編輯器之後:
- 字體大小、段落格式會被 Word 蓋掉,不好設,跟 SMARTSTYLES 的段落格式打架。
- 插入欄位(&FIELD&)的灰底標示行為不一樣,容易插成純文字而印不出值。
- 沒裝 Office 或版本不合的 client 端會開不起來。
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」來源引用。
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),改成 PDFUC 或 CNSAPWIN 就好。
3-4 條碼(Barcode)
- 條碼在 SAP 是「特殊的字元格式」:SE73 → System bar codes 建條碼定義 → 在 SMARTSTYLES 的字元格式裡的 Barcode 欄位選它 → 文字節點把那段欄位套上這個字元格式。
- 舊式條碼(BC_CD39、BC_EAN13…)是「印表機條碼」,靠 Device Type 的印表機指令產生,換印表機就可能印不出來。
- 新式條碼(New Bar Code Technology):SE73 建立時選 New,可用 BC_C128(Code128)、BC_C39、BC_EAN13、BC_PDF417、BC_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
| 欄位 | 資料元素 | 說明 | 來源 |
|---|---|---|---|
| EBELN | EBELN | 採購編號 | EKKO-EBELN |
| BUKRS | BUKRS | 公司代碼 | EKKO-BUKRS |
| BSART | ESART | 文件類型 | EKKO-BSART(值域來自 T161) |
| BATXT | BATXT | 類型名稱 | T161T-BATXT(要用 SPRAS 過濾) |
| LIFNR | ELIFN | 廠商編號 | EKKO-LIFNR |
| MC_NAME1 | AD_MC_NAME | 廠商名稱(搜尋用大寫名) | ADRC/VIEW USER_ADDR |
4-2 檔身結構 Z00_EKPO
| 欄位 | 資料元素 | 說明 | 對齊 |
|---|---|---|---|
| EBELP | EBELP | 項次 | 靠中 |
| MATNR | MATNR | 物料編號 | 靠左 |
| MAKTX | MAKTX | 物料名稱(MAKT 取) | 靠左 |
| MENGE | BSTMG | 採購數量 | 靠右 |
| MEINS | BSTME | 單位 | 靠中 |
| WERKS | EWERK | 工廠 | 靠中 |
| LGORT | LGORT | 儲存位置(值域參考 T001L) | 靠中 |
| LGOBE | LGOBE | 位置說明(T001L-LGOBE) | 靠左 |
沒設會有兩個後果:(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 Options | Page format(A4 / A4 橫 / LETTER)、字元寬度 Characters per inch、行距 Lines per inch、Style(可覆寫) |
| Language Attributes | Translate(是否納入翻譯)、語言相依設定。做多國表單這裡要開 |
| Form Interface | 下一小節 |
5-2 Form Interface(表單接口)
這就是「Smart Form 產生出來的那支函數,長什麼樣子」。四個頁籤完全對應 SE37 的函數介面:
| 頁籤 | 本案例填什麼 | 說明 |
|---|---|---|
| Import | I_EKKO TYPE Z00_EKKO | 檔頭一筆。Pass Value 建議打勾(值傳遞,比較安全) |
| Export | (本案例不用) | 要把表單裡算出來的東西回傳給程式時才用 |
| Tables | I_EKPO TYPE Z00_EKPO | 檔身多筆。注意:這裡填的是「列型別」,不是表型別 |
| Exceptions | (可自訂) | 系統自動附加 FORMATTING_ERROR、INTERNAL_ERROR、SEND_ERROR、USER_CANCELED |
如果你想避開 Tables,也可以把內表包成 Import 參數(型別=一個 TABLE TYPE),Smart Form 一樣能 LOOP,這是比較現代的做法。
5-3 Global Definitions(全域定義)
五個頁籤,是 Smart Form 裡「寫程式」的地方:
| 頁籤 | 作用 | 本案例 |
|---|---|---|
| Global Data | 宣告表單內共用的變數/工作區。等同 FORM 內的 DATA | WA_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 } 項)|.
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 才印的存查聯」 |
6-2 Window 的四種類型
| 類型 | 特性 | 用途 |
|---|---|---|
| MAIN(主視窗) | 每頁最多一個;內容印不完會自動續到下一頁的 MAIN;同一份表單所有頁的 MAIN 寬度必須一致 | 放明細 Table 節點 |
| Secondary(次視窗) | 固定位置固定大小,內容超出就被截掉,不會續頁 | 表頭資訊、廠商地址、表尾簽核欄、備註 |
| Copies Window(副本視窗) | 可設定「只在正本印」「只在副本印」 | 「客戶聯/會計聯/存查聯」的差異文字 |
| Final Window(最終視窗) | 整份表單處理完之後才輸出——此時總金額、總頁數才算得出來 | 總計金額、「共 x 頁」、大寫金額 |
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 三段 |
| 典型用途 | 表頭欄位框、簽核欄、固定格式的收據格 | 明細清單 |
| 放哪個 Window | Secondary Window(表頭/表尾) | MAIN Window |
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 節點的四個頁籤
| 頁籤 | 設定內容 |
|---|---|
| Data | Internal Table:I_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-MENGE → LV_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&。
課堂為了簡化,明細只顯示六欄(省略物料名稱與位置說明):
| # | 欄位 | 資料元素 | 抬頭 | 對齊 |
|---|---|---|---|---|
| 1 | WA_EKPO-EBELP | EBELP | 項次 | 靠中 |
| 2 | WA_EKPO-MATNR | MATNR | 物料編號 | 靠左 |
| 3 | WA_EKPO-MENGE | BSTMG | 採購數量 | 靠右 |
| 4 | WA_EKPO-MEINS | BSTME | 單位 | 靠中 |
| 5 | WA_EKPO-WERKS | EWERK | 工廠 | 靠中 |
| 6 | WA_EKPO-LGORT | LGORT | 儲存位置 | 靠中 |
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 text | 讀 SAPscript 標準長文(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)、F03、ST(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.
相對地,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 Data 或 Form 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 | 執行列印的使用者登入名稱 |
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 numbering、Spool control(新增 spool request/改 spool 屬性)、Print control(送印表機指令) |
| Program Lines(程式行) | 在表單中間寫 ABAP | 必填 Input parameters / Output parameters;不能寫 CALL SCREEN、COMMIT WORK 等會破壞輸出流程的語句 |
| Complex Section | Loop + Program Lines + Template 的組合節點 | 較少用,能用 Table 就用 Table |
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 ).
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.
11. 完整的呼叫程式(現代 ABAP 改寫版)
講義的原程式 Z202202_60 用了 SELECT *、INTO CORRESPONDING FIELDS OF TABLE、READ 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.
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 | 指定從哪一頁開始輸出 |
12-2 OUTPUT_OPTIONS(型別 SSFCOMPOP)
| 欄位 | 作用 |
|---|---|
| TDDEST | 輸出裝置(印表機代號,如 LOCL、LP01) |
| TDIMMED | 'X'=立即列印(Print immediately) |
| TDDELETE | 'X'=列印後刪除 Spool |
| TDNEWID | 'X'=建立新的 Spool Request(不要跟前一份併在一起) |
| TDCOPIES | 份數 |
| TDNOPRINT | 'X'=只產生 Spool 不真的印 |
| TDFINAL | Spool 是否標記為已完成 |
| TDTITLE | Spool 標題(在 SP01 顯示,強烈建議帶單號方便追查) |
| TDLIFETIME | Spool 保留天數(預設 8) |
| TDCOVER / TDCOVTITLE | 是否印封面頁與封面標題 |
| TDARMOD | 1=只印、2=只歸檔、3=印+歸檔(ArchiveLink) |
| TDPRINTER | Device type('SWIN'、'CNSAPWIN'、'PDFUC'…) |
| XSF / XSFFORMAT / XSFOUTMODE / XSFOUTDEV | XSF/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'.
12-4 幣別/數量欄位在不同表時的對應
SAP 官方規則:值欄位與幣別/單位欄位在同一張表時,Smart Forms 會自動辨識並正確格式化;不在同一張表時,你必須明講。做法(在 Global Definitions → Currency/Quantity Fields 頁籤):
- 在 Global Data 定義一個與「存幣別的那張表」同型別的工作區,例如 WA_CURTAB;以及輸出值欄位的工作區 WA_VALTAB。
- 在 Currency/Quantity Fields 填:Field name = WA_VALTAB-VALUE,Reference field = WA_CURTAB-CURRENCY。
- Table/Loop 節點的 Data 頁籤用 WA_VALTAB 當工作區。
- 在輸出該欄位之前插入一個 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.
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.
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.
" === 背景可用:寫到應用伺服器 ===
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.
14. NACE 輸出決定與 SPOOL 管理
正式專案裡,很少讓使用者「執行一支 Z 程式來印單據」,而是在單據存檔時自動觸發列印。這靠的是 Output Determination(輸出決定)。
14-1 NACE 的五層設定
| 層次 | 內容 | 說明 |
|---|---|---|
| 1. Application | EF=採購訂單、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)+ Program + FORM routine + Form / PDF-Form | SmartForm 填在「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.
14-2 SPOOL 相關 TCODE
| TCODE | 用途 |
|---|---|
| SP01 | Spool Request 總覽(看內容、重印、刪除、看輸出狀態) |
| SP02 | 只看自己的 Spool |
| SPAD | Spool 管理:輸出裝置、裝置類型、紙張格式、字元集 |
| SPIC | Spool 一致性檢查 |
| SP12 | TemSe 管理(Spool 實體資料的儲存區) |
| TSP01 / TSP02 / TST01 | Spool 的資料表,程式要查 Spool ID 時用 |
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 Footer 或 Final Window |
| 「共 y 頁」印成 0 或空白 | SFSY-FORMPAGES 只能在 Final Window 使用 | 把「第 x 頁/共 y 頁」放進 Final Window |
| 中文變方框/問號 | Device Type 沒掛中文字型;或用了 PDF1 而非 PDFUC | SE73 檢查字型;換 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' 蓋掉了 TDDEST | 設 user_settings = abap_false |
| 背景 Job 一直不結束 | NO_DIALOG 沒設,卡在列印對話框 | ls_ctrl-no_dialog = abap_true |
| 列印非常慢 | Logo 是高解析彩色 BMP;或 Program Lines 在迴圈內 SELECT | Logo 換黑白小檔;資料一次撈好放 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.
| 比較 | SmartForms | Adobe Forms |
|---|---|---|
| 維護 TCODE | SMARTFORMS | SFP |
| 取函數名 | SSF_FUNCTION_MODULE_NAME | FP_FUNCTION_MODULE_NAME |
| 工作包裝 | SSF_OPEN/SSF_CLOSE(選用) | FP_JOB_OPEN/FP_JOB_CLOSE(必要) |
| 拿 PDF | GETOTF → CONVERT_OTF | GETPDF → FORMOUTPUT-PDF(一步到位) |
| 介面/內容分離 | 介面與版面綁在一起 | Interface 與 Form 是兩個獨立物件,一個介面可給多張表單用 |
| 設計工具 | SAP GUI 內建的樹狀編輯器 | Adobe LiveCycle Designer(所見即所得) |
| 前置需求 | 無 | ADS(Adobe Document Services)必須先安裝設定 |
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 的印表機是兩條路。
17-3 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_PurchaseOrderItem、I_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 標準單據時還是會撞到它,所以留一個最小知識集:
| 項目 | 內容 |
|---|---|
| TCODE | SE71 Form/SE72 Style/SE73 Font/SE78 Graphic/SO10 標準文字/I18N 國際化入口 |
| 結構 | Header → Paragraph Formats → Character Formats → Windows → Pages → Page Windows → Text 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) |
練習與自我檢核
- 【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) 欄位一定要是灰色的。 - 為什麼「直接寫 CALL FUNCTION '/1BCDWB/SF00000009'」在開發機會成功、上線就 dump?請說出 SSF_FUNCTION_MODULE_NAME 的兩個 EXCEPTION(NO_FORM=1、NO_FUNCTION_MODULE=2)分別代表什麼情況。
答案要點:產生的函數名是各系統自行編號的流水號,傳輸不會帶過去;重新 Active 也會換號。1=表單根本不存在,2=表單存在但函數還沒產生(需重新啟動表單)。 - Template 與 Table 的差別是什麼? 表頭的四個欄位(單據編號、公司代碼、單據類型、供應廠商)應該用哪一個?明細清單應該用哪一個?為什麼明細一定要放在 MAIN Window?
答案要點:Template 固定列數不會 LOOP 也不會跨頁;Table 動態列數、自帶 Header/Main Area/Footer、會自動續頁並重印表頭。續頁機制由 MAIN Window 觸發,沒有 MAIN 的頁面不會往下翻。 - Table 的 Main Area 裡,格子要取 I_EKPO-MENGE 還是 WA_EKPO-MENGE?取錯會發生什麼事?
- 合計數量 LV_MENGE 有兩種算法:(a) Table 節點的 Calculations 頁籤設 TOTAL;(b) Program Lines 用 REDUCE。各寫一次,並說明「合計要顯示在哪個節點才不會是 0」。
答案要點:要放在 Table Footer 或 Final Window,放在一般 Window 時明細還沒跑完。 - 寫一個欄位輸出,要求:採購數量寬 13 位、小數 3 位、負號在左、不要千分位。
答案:&WA_EKPO-MENGE(13.3<T)& - 「第 x 頁/共 y 頁」要用哪兩個系統欄位?為什麼 SFSY-FORMPAGES 只能放在 Final Window?大量列印時為什麼不建議用 SFSY-JOBPAGES?
- 把採購單轉成 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。 - 使用者反映「單據都印到會計部的印表機」。程式明明有設 OUTPUT_OPTIONS-TDDEST = 'LP01',為什麼?
答案:USER_SETTINGS 預設 'X',會用 SU3 的使用者預設印表機蓋掉。設 abap_false。 - 採購單抬頭長文(ME23N 的 Texts)要印在表單上,Include Text 節點的 Text Object / Text ID / Text Name 各要填什麼?如果改用程式端的 READ_TEXT,四個參數又是什麼?為什麼這段文字傳到測試機會不見?
- 你的表單在開發機預覽中文完全正常,印出來全是方框。列出三個要檢查的地方。
答案要點:(1) SPAD 輸出設備的 Device Type 是否支援中文(CNSAPWIN/PDFUC);(2) SE73 該 Device Type 是否掛了中文字型;(3) Smart Style 的字元格式選的字體家族(ANDALE_T/CNSONG)在該裝置有沒有對應。 - 一次列印 500 張出貨單,要求:只產生一個 Spool Request、背景執行不跳對話框、不使用使用者預設印表機。請寫出關鍵的三個設定與兩支函數。
答案要點:SSF_OPEN/SSF_CLOSE 包住迴圈,中間每次呼叫設 no_open = no_close = 'X';ls_ctrl-no_dialog = abap_true;user_settings = abap_false。 - 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 上哪些寫法該換掉。
1. 這一章的地圖:一支程式串起五件事
講義開頭老師就說了:「這支工時回報上傳,應該算寫得非常好,有 FTP、有發 MAIL、有 EXCEL 上傳,也是先放暫存檔 T_XLS,然後才用 BDC 做完整的檢查,連訊息也回寫得很清楚。」這句話其實就是介面程式的標準骨架,值得抄下來當口訣:
- 取得來源:從 FTP 抓檔,或從 PC 上傳 Excel/CSV。兩條路走完都倒進同一張暫存內表。
- 暫存 + 正規化:全部先進 T_XLS 這種「寬鬆型別(全部字元欄)」的暫存表,再做去引號、ALPHA 補零、日期轉換。
- 驗證:查主檔存不存在(工單查 AUFK)、必填欄位、數值可不可轉。錯的先標 STATUS = 'E',不要丟掉。
- 過帳:只處理 STATUS = SPACE 的那些,用 BDC 呼叫 CO11N,訊息收回來寫進 MSG。
- 留痕與通知:整批結果寫進自建紀錄表 ZPPWORKMA0001、顯示 ALV、必要時把 Spool 轉 PDF 寄給群組。
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.
暫存表 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.
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_GROUP → BDC_INSERT → BDC_CLOSE_GROUP | 非同步,之後在 SM35 執行 | SM35 自動保留錯誤 session,可補跑 | 大量資料、夜間跑、需要人工介入修正 |
| LSMW / LTMC | 設定式工具,底層仍可能用 BDC/BAPI/IDoc | 專案期一次性搬資料 | 工具本身管 | 導入期主檔/期初資料遷移 |
4. SHDB 錄製與 BDCDATA 結構
不會寫 BDC 的人,第一步永遠是 SHDB。(依講義步驟整理)
- SHDB → New recording,輸入錄製名稱(例:Z_CO11N)與要錄的交易碼(CO11N)。
- 按 Start recording,系統就會把你接下來的每一個畫面、每一個欄位、每一次按鍵記下來。像真的使用者一樣把單開完、按存檔。
- 回到 SHDB 清單,選這筆錄製 → Program,系統直接吐出一支含 bdc_dynpro / bdc_field 的完整程式範本。
- 把裡面固定的值換成內表欄位,就完成了 90%。
錄製結果就是一張 BDCDATA 表。它只有四個欄位,非常好記:
| 欄位 | 長度 | 意義 |
|---|---|---|
| PROGRAM | 40 | 畫面所屬的程式名(例:SAPLCORU_S) |
| DYNPRO | 4 | 畫面編號(例:0100) |
| DYNBEGIN | 1 | 'X' 表示「這一列是新畫面的開頭」 |
| FNAM | 132 | 欄位名(結構-欄位,例 AFRUD-AUFNR)或控制欄位 |
| FVAL | 132 | 欄位值(一律是「使用者會敲進去的那個字串」) |
三個特殊控制欄位一定要背:
- 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.
5. CALL TRANSACTION USING:MODE / UPDATE / MESSAGES INTO
講義的 CO11N 報工就是 CALL TRANSACTION。先看資料怎麼組——注意 SAPLCORU_S 的 0100 畫面、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 INTO | BDCMSGCOLL 內表 | 把交易產生的所有訊息(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'。
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.
*&--- 回寫紀錄表:先刪同鍵、再整批寫入(可重跑)
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.
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(背景):完全不顯示,交給背景作業跑。
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 |
| 3 | SU3 使用者參數:Parameter ID 會自動帶預設值進畫面欄位(例如工廠 WRK、公司代碼 BUK) | BDC 要明確送出每一個欄位值,不要依賴預設值;或替背景作業建立參數乾淨的專用帳號 |
| 4 | 前導零 / ALPHA:Excel 的 1000123 和資料庫的 001000123 對不起來 | 丟進 BDC 前先跑 CONVERSION_EXIT_ALPHA_INPUT;顯示回使用者時用 ..._OUTPUT |
| 5 | Excel 帶來的引號與空白:CSV 匯出常變成 "1000123" | 講義用 TRANSLATE t_xls-aufnr USING '" ' 把雙引號換成空白,再 CONDENSE。現代寫法用 REPLACE ALL OCCURRENCES OF '"' IN lv_x WITH space |
| 6 | Table Control 捲動:畫面一次只顯示 N 列,第 N+1 列必須先翻頁 | 見下方程式;核心是每 N 筆送一次 BDC_OKCODE = '=P+',並用 BDC_CURSOR 定位 |
| 7 | 畫面大小:Table Control 可見列數隨 GUI 視窗大小改變 | defsize = 'X',或改用「每頁固定第 1 列輸入」的定位欄位(很多交易有 Position 欄) |
| 8 | Enhancement / User Exit 跳出額外畫面:正式機有客製檢查,開發機沒有 | 錄製一定要在「有客製的環境」錄;上線前用 MODE 'A' 在 QAS 完整跑一次 |
| 9 | SAP 升級後畫面改版:欄位改名、畫面編號改變 → 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.
9. BDC vs BAPI:S/4HANA 的選用原則
這是本章最重要的觀念題。老師的立場很明確:能用 BAPI 就不要用 BDC。但 BDC 沒有死,它在 S/4HANA 上仍有無可取代的場合。
| 比較 | BDC(CALL TRANSACTION) | BAPI / Function Module | OData / RAP |
|---|---|---|---|
| 執行對象 | 畫面(PBO/PAI) | 函數介面 | 服務介面 |
| 效能 | 慢(每筆都跑一次畫面) | 快 | 快 |
| 檢查完整度 | 最完整(含所有 Exit、客製) | 只有該 BAPI 實作的檢查 | 依 Behavior Definition |
| 升級穩定度 | 差,畫面一改就壞 | 好,SAP 保證介面相容 | 好 |
| 能否背景執行 | 可(MODE 'N') | 可 | 可 |
| Fiori 畫面 | 完全不能錄 | — | 原生支援 |
| 回傳訊息 | BDCMSGCOLL | BAPIRET2 | Message container |
- Fiori/SAP GUI for HTML 的畫面錄不了 BDC。SHDB 只能錄傳統 SAP GUI Dynpro。如果那支交易在 S/4HANA 只剩 Fiori App,就沒有 BDC 這條路,只能找 OData 或 BAPI。
- 很多經典交易在 S/4HANA 被廢止或改版(例如客戶/供應商主檔統一成 Business Partner,XD01/MK01 已不建議使用,改用 BP 與 BAPI_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.
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.
- codepage 傳錯,中文會變亂碼。常見 SAP codepage:1100 Latin-1、4110 UTF-8、4103 UTF-16LE、8300 繁體中文 BIG5。實際可用值請查 TCP00 / TCP0C。
- Excel 另存 CSV UTF-8 時會在檔頭放 BOM(EF 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.
- TEXT_CONVERT_XLS_TO_SAP:講義用的,簡單但只穩定支援舊 .xls,且需要 GUI。
- ALSM_EXCEL_TO_INTERNAL_TABLE:老牌,但上限 9999 列 / 欄寬有限、需要 GUI、逐格回傳很慢。
- CL_FDT_XL_SPREADSHEET:標準內建、可在背景跑,讀 .xlsx 首選。缺點是只能讀不能寫。
- abap2xlsx(開源 ZCL_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 MODE | PDF、XLSX、圖片,配 TYPE xstring |
| 舊式文字 | IN LEGACY TEXT MODE CODE PAGE '8300' | 要產生 BIG5 等非 Unicode 編碼給舊系統吃 |
| 附加 | FOR APPENDING | 寫入 log 檔不覆蓋 |
*--- 用邏輯檔案名取得實體路徑(跨環境安全)
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).
12. FTP:SAP 標準函數群完整流程
講義的 FTP 流程設計得很完整,值得整套背下來:連線 → 檢查來源/備份目錄 → 設 binary 模式 → 列檔名 → 逐檔 get → 備份到 BAK 目錄 → 刪來源 → 斷線。這個「取走後立刻搬到備份目錄並刪除來源」的設計,可以保證同一個檔不會被處理兩次。
| 函數 | 用途 | 重點參數 |
|---|---|---|
| HTTP_SCRAMBLE | 把密碼加擾(FTP_CONNECT 只吃加擾過的密碼) | source/sourcelen/key(慣用 26101957) |
| FTP_CONNECT | 建立連線 | user/password(已加擾)/host/rfc_destination → 回 handle |
| FTP_COMMAND | 下任意 FTP 指令:cd、bin/asc、nlist/dir、get、put、del、mkdir | handle/command,結果回 TABLES data |
| FTP_SERVER_TO_R3 | 把 FTP 上的檔案抓進 ABAP 內表 | fname;TABLES blob(二進位)或 text(文字);回 blob_length |
| FTP_R3_TO_SERVER | 把 ABAP 內表寫回 FTP | fname/blob_length/TABLES blob |
| FTP_DISCONNECT | 斷線 | handle |
| RFC_CONNECTION_CLOSE | 釋放 RFC 連線(斷線後一定要做) | destination |
- 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_TEXT(ENCODING = '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.
- SFTP:用 SM69 定義外部指令、SXPG_COMMAND_EXECUTE 呼叫 OS 上的 sftp/scp(配金鑰認證),SAP 端只讀寫應用伺服器目錄(AL11)。
- HTTPS / REST:用 CL_HTTP_CLIENT 或 CL_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.
- 忘了 COMMIT WORK:create_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.
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.
15. 背景作業:SM36 / SM37 與 JOB_OPEN / SUBMIT / JOB_CLOSE
介面程式的終點是「排成背景作業,每天/每小時自己跑」。兩條路:手動用 SM36 排,或程式自己建 Job。
手動排程(SM36)的步驟:(依講義步驟整理)
- SM36 → 輸入 Job Name、Job Class(A 最高/B 中/C 一般,一般客製程式用 C)、Target server(可指定跑在哪台 App Server)。
- Step:選 ABAP program,填程式名 + Variant(變式)。背景作業一定要有變式,因為沒有人可以在畫面上輸入條件。變式在 SE38 →「以變式執行」→ Save 建立。
- Start condition:Immediate(立即)/Date-Time(指定時間)/After job(某個 Job 跑完之後)/After event(事件觸發,SM62 定義事件、SM64 手動觸發)/At operation mode change。
- 勾 Periodic job 並設定週期(每小時/每天/每週/每月)。
- Save。到 SM37 查狀態:Scheduled/Released/Ready/Active/Finished/Cancelled;點進去看 Job log 與 Spool。
*&--- 程式內建立背景作業(例如:使用者按下按鈕後排一支長時間的程式去背景跑)
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_CONFIRM、CALL SCREEN、WRITE 之後的互動式清單,全部不行。背景遇到 TYPE 'I'/'A' 會直接讓 Job Cancel。
- 不可用任何 CL_GUI_* / GUI_UPLOAD / GUI_DOWNLOAD / OLE。用 sy-batch = abap_true 判斷分流。
- ALV 在背景會自動印到 Spool:CL_SALV_TABLE 的 display( ) 在背景會輸出成清單,可從 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.
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:跨系統的排程,屬於雲端方案。
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 ENTRIES 與 MODIFY ... FROM TABLE。
18. 快速查表:本章重點速記
| 要做什麼 | 用什麼 | 注意 |
|---|---|---|
| 錄畫面操作 | SHDB | Fiori 畫面錄不了 |
| 同步跑交易 | CALL TRANSACTION ... USING ... MODE ... UPDATE ... MESSAGES INTO | 背景 MODE 用 'N';defsize='X' |
| 非同步批次 | BDC_OPEN_GROUP / BDC_INSERT / BDC_CLOSE_GROUP → SM35 | 背景消化用 RSBDCSUB |
| 訊息轉文字 | MESSAGE ... INTO/FORMAT_MESSAGE/MESSAGE_TEXT_BUILD | 優先用語法內建的 INTO |
| PC 檔案 | CL_GUI_FRONTEND_SERVICES | 背景不能用 |
| 伺服器檔案 | OPEN DATASET / TRANSFER / READ / CLOSE、AL11 | 路徑用 FILE 邏輯檔名 |
| 讀 XLSX | CL_FDT_XL_SPREADSHEET | 純 ABAP,背景可用 |
| 編碼轉換 | CL_ABAP_CONV_CODEPAGE、CL_ABAP_CHAR_UTILITIES | UTF-8 CSV 記得 BOM |
| FTP | FTP_CONNECT / FTP_COMMAND / FTP_SERVER_TO_R3 / FTP_R3_TO_SERVER / FTP_DISCONNECT | 背景用 SAPFTPA;密碼要 HTTP_SCRAMBLE |
| 寄信 | CL_BCS(新)/SO_DOCUMENT_SEND_API1(舊) | 一定要 COMMIT WORK;SCOT 設定、SOST 查詢 |
| Spool 轉 PDF | CONVERT_ABAPSPOOLJOB_2_PDF + SX_TABLE_LINE_WIDTH_CHANGE | sy-spono 前景可能是空的 |
| 背景作業 | SM36/SM37、JOB_OPEN / SUBMIT VIA JOB / JOB_CLOSE | AND RETURN 不能忘;要有變式 |
| 使用者自助排程 | Fiori「Application Jobs」+ Job Catalog Entry | 需實作 IF_APJ_DPC_RUN / IF_APJ_RT_RUN |
練習與自我檢核
- SHDB 錄一支 MM02。錄「修改物料的基本資料視圖描述」,把產生的程式改成從 CSV 讀 MATNR + MAKTX 批次修改。提示:注意 MM02 有「選擇視圖」的彈出畫面(SAPLMGMM 的 0060),要用 MSICHTAUSW-KZSEL(01) 勾選視圖。
- MODE 三選一。同一支 BDC 程式,前景執行時要讓使用者看到錯誤畫面、背景執行時完全不顯示,程式該怎麼寫?答案要點:DATA(lv_mode) = COND #( WHEN sy-batch = abap_true THEN 'N' ELSE 'E' ).
- UPDATE 'A' 為什麼危險?請說明「程式回報成功但資料庫沒有資料」的可能原因,以及該去哪個交易碼查(提示:SM13)。
- 日期陷阱重現。把使用者 SU3 的日期格式從 YYYY.MM.DD 改成 MM/DD/YYYY,再跑同一支 BDC,觀察會發生什麼。寫出修正的程式碼(提示:WRITE lv_d TO lv_c 或 CONVERT_DATE_TO_EXTERNAL)。
- Table Control 捲動。寫一段 BDC,在 VA01 的項目 Table Control 輸入 12 個項目,每頁只能顯示 5 列。畫出 OKCODE 的順序表。
- FOR ALL ENTRIES 空表陷阱。把第 17 節的 DELETE ... FOR ALL ENTRIES 在 gt_xls 為空時執行,會發生什麼?請寫出正確的防呆。
- CSV 中文亂碼。使用者說「你們產的 CSV 用 Excel 開中文都是亂碼」。列出三個可能原因與對應解法(提示:codepage、BOM、Excel 的地區設定)。
- 背景不能用 GUI_UPLOAD。把第 10 節的 upload_csv 改寫成「前景讀 PC 檔、背景讀 AL11 目錄」的雙軌版本。
- FTP 改 SFTP。公司資安要求禁用 FTP。請提出至少兩種替代方案,並說明各自需要 Basis 配合什麼(提示:SM69 外部指令、SAP Integration Suite)。
- CL_BCS 寄不出去。程式跑完 send( ) 回傳 abap_true,但收件人沒收到信。列出四個檢查點(提示:COMMIT、SOST、SCOT、RSCONN01)。
- 把 ALV 結果寄出去。寫一支程式:背景執行 → CL_SALV_TABLE 輸出到 Spool → 取 sy-spono 轉 PDF → 用 CL_BCS 寄給 SBWP 通訊群組。這題就是把第 13、14 節合起來。
- 排成每小時的 Job。用 SM36 排定第 17 節那支程式每小時跑一次,並說明為什麼一定要建變式、Job Class 該選什麼、以及如何確保「同一個檔不會被處理兩次」。
- BDC 還是 BAPI?下列情境各該選哪一個,並說明理由:(a) 期初匯入 5 萬筆客戶主檔;(b) 每天 200 筆生產訂單報工;(c) 客戶自建的 Z 交易,沒有任何 API;(d) 要在 S/4HANA Cloud 上做。
- 紀錄表設計。替第 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 line_exists( it[ k = v ] ). | 表格運算式;找不到會丟 CX_SY_ITAB_LINE_NOT_FOUND |
| 迴圈逐筆 APPEND | DATA(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 = ... ] 或 JOIN | N+1 查詢是最常見的效能地雷 |
| 撈回內表後 LOOP 累加 | SELECT ... SUM( x ) ... GROUP BY ... | Code pushdown:算給資料庫做 |
FOR ALL ENTRIES IN it | 先 IF it IS NOT INITIAL,或改用 JOIN / CDS / WITH CTE | 驅動表為空會撈全表;重複值會被自動去重 |
EXEC SQL ... ENDEXEC. | cl_sql_statement(ADBC)或 AMDP | EXEC SQL 已過時 |
A-4 報表輸出
| 舊寫法 | 現代寫法 | 說明 |
|---|---|---|
WRITE / ULINE / SKIP 清單 | CL_SALV_TABLE | WRITE 只留給極簡的除錯輸出 |
REUSE_ALV_GRID_DISPLAY | CL_SALV_TABLE=>factory( ) | 不需編輯時最省事 |
REUSE_ALV_GRID_DISPLAY_LVC | CL_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-subrc) | TRY ... 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( )、VALUE、FOR、REDUCE、COND、SWITCH、CORRESPONDING、NEW、字串範本、表格運算式、內表 GROUP BY | 7.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 sorting | 7.54 |
DEFINE VIEW ENTITY(免 sqlViewName 的 CDS)、RAP Managed Scenario | 7.55 / S/4HANA 2020 |
PCRE 正規表示式(PCRE 附加) | 7.55 |
RSABAPVERSION,或直接看 系統 → 狀態 → 元件資訊 中的 SAP_BASIS 版本:7.52 = S/4HANA 1809/1909,7.54 = 2020,7.55 = 2021,7.56 = 2022,7.57 = 2023。