SAP-ABAP:SAP MM 模块物料主数据批量导入与增强开发:字段扩展、校验逻辑与批量导入工具开发

SAP MM 模块物料主数据批量导入与增强开发:字段扩展、校验逻辑与批量导入工具开发

博客简介:针对物料主数据治理场景,讲解物料主数据自定义字段配置、BADI_MATERIAL_CHECK 校验逻辑开发、LSMW/CATT 批量导入工具配置、自定义批量导入程序开发的核心方法,附"物料编码自动生成""物料描述规范校验""视图扩展"的实现代码,提升物料主数据维护效率与数据准确性。


"采购订单增强你会了,MM 模块另一个高频场景------物料主数据------也少不了增强。本篇从物料主数据字段扩展(CI_MARA/CI_MARC)→ BADI_MATERIAL_CHECK 校验 → LSMW 批量导入 → 自定义导入程序,完整覆盖物料主数据治理需求,附物料编码自动生成、描述规范校验、视图扩展的可复用代码。"

本篇针对物料主数据治理场景,讲解物料主数据自定义字段配置、BADI_MATERIAL_CHECK 校验逻辑开发、LSMW/CATT 批量导入工具配置、自定义批量导入程序开发的核心方法,附"物料编码自动生成""物料描述规范校验""视图扩展"的实现代码。


📖 写在前面

本篇定位

MM 模块物料主数据是整个供应链的基础数据------采购、生产、销售、仓储都要用到物料号。物料主数据的质量直接影响业务执行效率。本篇聚焦物料主数据的两个核心增强场景:①字段扩展 + BAdI 校验 (提升物料数据质量);②批量导入工具开发(提升数据维护效率),所有代码基于 SAP NetWeaver 7.51。

本篇适合谁读

  • 需要在 MM 模块做物料主数据增强的开发者
  • 需要批量导入物料数据的业务分析师
  • 负责物料主数据治理的产品经理

贯穿项目概览

复制代码
贯穿项目:物料主数据增强 + 批量导入

  业务需求(来自物料管理部):
  ┌─────────────────────────────────────────────────────────┐
  │ 1. 字段扩展:                                            │
  │    - 基本数据:旧物料号(ZOLD_MATNR)、安全库存级别(ZSAFETY_LEVEL)│
  │    - 工厂数据:批次管理开关(ZBATCH_FLAG)、效期管理开关(ZEXPIRY_FLAG)│
  │                                                         │
  │ 2. 校验规则:                                            │
  │    - 物料编码必须符合命名规范(前缀+类别+流水号)          │
  │    - 物料描述不能为空,且长度≥5个字符                     │
  │    - 旧物料号不能重复                                    │
  │    - 危险品类(物料组 Z001~Z009)必须开启批次管理         │
  │                                                         │
  │ 3. 批量导入:                                            │
  │    - LSMW 批量导入工具(支持多视图同步创建)               │
  │    - 自定义导入程序(带完整日志和错误回写)               │
  └─────────────────────────────────────────────────────────┘

#mermaid-svg-DtHJyJB6mWKD1ew2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DtHJyJB6mWKD1ew2 .error-icon{fill:#552222;}#mermaid-svg-DtHJyJB6mWKD1ew2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DtHJyJB6mWKD1ew2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .marker.cross{stroke:#333333;}#mermaid-svg-DtHJyJB6mWKD1ew2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DtHJyJB6mWKD1ew2 p{margin:0;}#mermaid-svg-DtHJyJB6mWKD1ew2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .cluster-label text{fill:#333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .cluster-label span{color:#333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .cluster-label span p{background-color:transparent;}#mermaid-svg-DtHJyJB6mWKD1ew2 .label text,#mermaid-svg-DtHJyJB6mWKD1ew2 span{fill:#333;color:#333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .node rect,#mermaid-svg-DtHJyJB6mWKD1ew2 .node circle,#mermaid-svg-DtHJyJB6mWKD1ew2 .node ellipse,#mermaid-svg-DtHJyJB6mWKD1ew2 .node polygon,#mermaid-svg-DtHJyJB6mWKD1ew2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DtHJyJB6mWKD1ew2 .rough-node .label text,#mermaid-svg-DtHJyJB6mWKD1ew2 .node .label text,#mermaid-svg-DtHJyJB6mWKD1ew2 .image-shape .label,#mermaid-svg-DtHJyJB6mWKD1ew2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-DtHJyJB6mWKD1ew2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DtHJyJB6mWKD1ew2 .rough-node .label,#mermaid-svg-DtHJyJB6mWKD1ew2 .node .label,#mermaid-svg-DtHJyJB6mWKD1ew2 .image-shape .label,#mermaid-svg-DtHJyJB6mWKD1ew2 .icon-shape .label{text-align:center;}#mermaid-svg-DtHJyJB6mWKD1ew2 .node.clickable{cursor:pointer;}#mermaid-svg-DtHJyJB6mWKD1ew2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .arrowheadPath{fill:#333333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DtHJyJB6mWKD1ew2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DtHJyJB6mWKD1ew2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DtHJyJB6mWKD1ew2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DtHJyJB6mWKD1ew2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DtHJyJB6mWKD1ew2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DtHJyJB6mWKD1ew2 .cluster text{fill:#333;}#mermaid-svg-DtHJyJB6mWKD1ew2 .cluster span{color:#333;}#mermaid-svg-DtHJyJB6mWKD1ew2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DtHJyJB6mWKD1ew2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DtHJyJB6mWKD1ew2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-DtHJyJB6mWKD1ew2 .icon-shape,#mermaid-svg-DtHJyJB6mWKD1ew2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DtHJyJB6mWKD1ew2 .icon-shape p,#mermaid-svg-DtHJyJB6mWKD1ew2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DtHJyJB6mWKD1ew2 .icon-shape .label rect,#mermaid-svg-DtHJyJB6mWKD1ew2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DtHJyJB6mWKD1ew2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DtHJyJB6mWKD1ew2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DtHJyJB6mWKD1ew2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 字段扩展

CI_MARA / CI_MARC
BADI_MATERIAL_CHECK

CHECK_DATA 校验
物料编码

Z_MATNR_GENERATE
批量导入

LSMW / 自定义程序
BAPI_MATERIAL_SAVEDATA

批量创建物料

版本说明:本篇内容基于 SAP NetWeaver 7.51,BADI_MATERIAL_CHECK 在 ECC 6.0+ 版本中均可用。


一、物料主数据增强架构

1.1 物料主数据核心增强点

增强类型 增强名称 触发时机 适用事务码 推荐场景
BAdI BADI_MATERIAL_CHECK 物料保存前校验 MM01/MM02 核心校验、字段合法性检查
BAdI BADI_MATERIAL_REF 物料引用/删除时 MM06 物料删除前检查、引用数据清理
BAdI BADI_MATERIAL_UI_CUST MM01/MM02 界面 MM01/MM02 自定义界面逻辑、增强标签页
用户出口 EXIT_SAPLMGMU_001 物料保存前(老方式) MM01/MM02 老项目兼容
显式增强 ENHANCEMENT-POINT SAP 预留标记 MM01/MM02 灵活插入逻辑

推荐优先用 BADI_MATERIAL_CHECK:功能最完善、参数最规范、支持多实现。

1.2 物料主数据的关键表与追加结构

SAP 表 说明 追加结构 扩展对象
MARA 物料基本数据 CI_MARA 基本视图自定义字段
MARC 工厂级别物料数据 CI_MARC 工厂视图自定义字段
MBEW 会计评估数据 CI_MBEW 会计视图自定义字段
MARD 存储位置数据 CI_MARD 存储位置视图字段
MVKE 销售数据 CI_MVKE 销售视图自定义字段

1.3 BADI_MATERIAL_CHECK 接口方法

⚠️ 技术要点 :BADI_MATERIAL_CHECK 的接口 IF_EX_BADI_MATERIAL_CHECK 只有一个核心方法 CHECK_DATA 。所有校验逻辑都写在这一个方法中,通过参数 IM_ACTIVITY 区分操作类型。

方法名 功能 关键参数
CHECK_DATA 物料主数据综合校验 IM_ACTIVITY(活动类型)、IM_MARA(基本数据)、IM_MARC(工厂数据)

参数说明

参数 类型 说明
IM_ACTIVITY TRTYP 活动类型:H=创建、V=修改、A=显示
IM_MARA MARA 物料基本数据(含 CI_MARA 追加字段)
IM_MARC MARC 物料工厂数据(含 CI_MARC 追加字段)

二、自定义字段扩展

2.1 基本数据视图扩展(CI_MARA)

复制代码
SE11 → 输入结构名 CI_MARA → 创建(Append 结构)

CI_MARA 是 SAP 预留的物料基本数据追加结构容器。

在 CI_MARA 中定义自定义字段:

  ┌──────────────┬──────────┬──────────┬──────────────────────────────┐
  │ 字段名       │ 数据类型  │ 长度     │ 说明                          │
  ├──────────────┼──────────┼──────────┼──────────────────────────────┤
  │ ZOLD_MATNR   │ CHAR      │ 18       │ 旧物料号(系统切换前的编号)  │
  │ ZSAFETY_LEVEL│ CHAR      │ 1        │ 安全库存级别(1/2/3)         │
  │ ZDESIGNER    │ CHAR      │ 12       │ 设计人员                      │
  │ ZDESIGN_DATE │ DATS      │ 8        │ 设计日期                      │
  │ ZMAT_TYPE    │ CHAR      │ 10       │ 自定义物料分类                │
  └──────────────┴──────────┴──────────┴──────────────────────────────┘

激活 CI_MARA → 自动追加到 MARA 表

2.2 工厂视图扩展(CI_MARC)

复制代码
SE11 → 输入结构名 CI_MARC → 创建(Append 结构)

在 CI_MARC 中定义自定义字段:

  ┌──────────────────┬──────────┬──────────┬──────────────────────────────┐
  │ 字段名           │ 数据类型  │ 长度     │ 说明                          │
  ├──────────────────┼──────────┼──────────┼──────────────────────────────┤
  │ ZBATCH_FLAG      │ FLAG      │ 1        │ 批次管理开关(关联 XCHPF)    │
  │ ZEXPIRY_FLAG     │ FLAG      │ 1        │ 效期管理开关(关联 XDATE)    │
  │ ZMAX_STORAGE_DAYS│ INT4      │ 10       │ 最大存储天数                  │
  │ ZMIN_BATCH_QTY   │ QUAN      │ 13       │ 最小批次数量                  │
  └──────────────────┴──────────┴──────────┴──────────────────────────────┘

激活 CI_MARC → 自动追加到 MARC 表

2.3 视图扩展(MM01/MM02 界面显示)

SAP 标准的 MM01/MM02 界面会自动显示 CI_MARA/CI_MARC 等追加结构的字段:

复制代码
MM01 创建物料 → 基本数据视图
  → 自定义字段自动出现在底部的"客户字段"区域

MM01 创建物料 → 工厂视图
  → 自定义字段自动出现在底部的"客户字段"区域

把自定义字段放到主界面(而非底部区域)的三种方式:

方式 说明 难度
Enhancement 向导 SE80 → 屏幕增强 → 创建自定义子屏幕
BADI_MATERIAL_UI_CUST 修改界面逻辑
Screen Painter 自定义子屏幕 直接画屏幕控件

三、BADI_MATERIAL_CHECK 校验逻辑开发

3.1 配置表设计

物料编码规则配置表

复制代码
表名:ZT_MATNR_RULE
  MANDT         TYPE MANDT       " 客户端
  MATKL         TYPE CHAR9       " 物料组(*=通配)
  PREFIX        TYPE CHAR2       " 编码前缀(如 RM/FG/SG)
  CATEGORY      TYPE CHAR2       " 物料类别(01/02/03)
  SERIAL_FROM   TYPE NUMC6       " 流水号下限
  SERIAL_TO     TYPE NUMC6       " 流水号上限
  LENGTH        TYPE NUMC2       " 编码总长度
  ACTIVE        TYPE FLAG        " 是否激活

物料黑名单表

复制代码
表名:ZT_MATERIAL_BLACKLIST
  MANDT         TYPE MANDT       " 客户端
  MATNR         TYPE MATNR       " 物料号
  BLOCK_FLAG    TYPE FLAG        " 黑名单标记
  REASON        TYPE CHAR50      " 黑名单原因
  CREATE_BY     TYPE UNAME       " 创建人
  CREATE_DATE   TYPE DATUM       " 创建日期

3.2 完整 BAdI 实现代码

⚠️ 关键修正

  1. 接口方法名是 CHECK_DATA ,不是 CHECK
  2. IM_ACTIVITY 的值是 H/V/A ,不是 01/02/03
  3. 自定义字段通过 ASSIGN COMPONENT 动态访问(因追加结构字段可能在编译期不可见)
abap 复制代码
"=======================================================================
" BAdI 实现:ZIMPL_MATNR_CHECK
" BAdI:BADI_MATERIAL_CHECK
" 功能:物料编码规范校验 + 描述规范 + 旧物料号重复检查 + 批次管理校验
" 触发:MM01/MM02 保存前
" 创建日期:2026.09.15
"=======================================================================

CLASS cl_impl_matnr_check DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES if_ex_badi_material_check.
  PRIVATE SECTION.
    METHODS get_custom_field
      IMPORTING iv_field_name TYPE string
                is_mara       TYPE mara
      RETURNING VALUE(rv_value) TYPE string.
ENDCLASS.


CLASS cl_impl_matnr_check IMPLEMENTATION.

  METHOD if_ex_badi_material_check~check_data.

    "-------------------------------------------------------------------
    " 步骤0:活动类型判断(只对创建/修改校验)
    " H=创建, V=修改, A=显示
    "-------------------------------------------------------------------
    IF im_activity NOT IN ('H', 'V').
      RETURN.
    ENDIF.

    DATA: lv_matnr    TYPE matnr,
          lv_matkl    TYPE matkl,
          lv_biskey   TYPE biskey,
          lv_old_matnr TYPE char18.

    lv_matnr  = im_mara-matnr.
    lv_matkl  = im_mara-matkl.
    lv_biskey = im_mara-biskey.

    " 通过动态 ASSIGN 访问自定义字段 ZOLD_MATNR
    lv_old_matnr = me->get_custom_field(
      iv_field_name = 'ZOLD_MATNR'
      is_mara       = im_mara ).

    " ================================================================
    " 校验1:物料编码命名规范(仅创建时校验)
    " 规则:前缀2位 + 类别2位 + 流水号6位 = 10位
    " 示例:RM01000001
    " ================================================================
    IF im_activity = 'H' AND lv_matnr IS NOT INITIAL.

      " 长度检查(物料号内部格式为18位,前导补0)
      DATA(lv_matnr_trim) = lv_matnr.
      CALL FUNCTION 'CONVERSION_EXIT_MATN1_OUTPUT'
        EXPORTING input  = lv_matnr
        IMPORTING output = lv_matnr_trim.

      IF STRLEN( lv_matnr_trim ) < 10.
        MESSAGE e001(zmm_mat_enh) WITH lv_matnr.
      ENDIF.

      " 从配置表读取该物料组的编码规则
      DATA: ls_rule TYPE zt_matnr_rule.
      SELECT SINGLE *
        FROM zt_matnr_rule
        INTO @ls_rule
        WHERE matkl  = @lv_matkl
          AND active = 'X'.

      IF sy-subrc = 0.
        " 校验前缀
        IF lv_matnr_trim(2) <> ls_rule-prefix.
          MESSAGE e002(zmm_mat_enh)
            WITH lv_matnr ls_rule-prefix.
        ENDIF.
      ENDIF.

    ENDIF.

    " ================================================================
    " 校验2:物料描述规范
    " 规则:不能为空,长度≥5,不能只包含数字
    " ================================================================
    IF lv_biskey IS INITIAL.
      MESSAGE e004(zmm_mat_enh).

    ELSEIF STRLEN( lv_biskey ) < 5.
      MESSAGE e005(zmm_mat_enh) WITH lv_biskey.

    ELSEIF lv_biskey CO '0123456789'.
      MESSAGE e006(zmm_mat_enh) WITH lv_biskey.

    ENDIF.

    " ================================================================
    " 校验3:旧物料号不能重复
    " ================================================================
    IF lv_old_matnr IS NOT INITIAL.

      SELECT SINGLE matnr FROM mara
        INTO @DATA(lv_existing_matnr)
        WHERE matnr = @lv_old_matnr.

      IF sy-subrc = 0 AND lv_existing_matnr <> lv_matnr.
        MESSAGE e007(zmm_mat_enh) WITH lv_old_matnr.
      ENDIF.

      " 检查黑名单
      SELECT SINGLE * FROM zt_material_blacklist
        INTO @DATA(ls_black)
        WHERE matnr      = @lv_old_matnr
          AND block_flag = 'X'.

      IF sy-subrc = 0.
        MESSAGE e008(zmm_mat_enh)
          WITH lv_old_matnr ls_black-reason.
      ENDIF.

    ENDIF.

    " ================================================================
    " 校验4:危险品类(Z001~Z009)必须开启批次管理
    " ================================================================
    IF im_marc IS NOT INITIAL AND lv_matkl IS NOT INITIAL.
      " 判断物料组是否在 Z001~Z009 范围(前4位匹配 Z00)
      IF lv_matkl(4) = 'Z00'.
        IF im_marc-xchpf <> 'X'.
          MESSAGE e009(zmm_mat_enh) WITH lv_matnr lv_matkl.
        ENDIF.
      ENDIF.
    ENDIF.

  ENDMETHOD.


  METHOD get_custom_field.

    FIELD-SYMBOLS: <fs_value> TYPE any.

    ASSIGN COMPONENT iv_field_name OF STRUCTURE is_mara
      TO <fs_value>.

    IF sy-subrc = 0 AND <fs_value> IS ASSIGNED.
      rv_value = <fs_value>.
    ELSE.
      rv_value = ''.
    ENDIF.

  ENDMETHOD.

ENDCLASS.

3.3 消息类配置(SE91)

复制代码
消息类:ZMM_MAT_ENH

  001:物料号 &1 长度不符合规范(应≥10位)
  002:物料号 &1 前缀不符合配置规则(应为 &2)
  003:物料号 &1 建议使用前缀 &2(未配置规则时的默认前缀)
  004:物料描述不能为空
  005:物料描述 &1 长度不足5位
  006:物料描述 &1 不能只包含数字
  007:旧物料号 &1 已存在于系统中,不能重复
  008:旧物料号 &1 在黑名单中(原因:&2)
  009:危险品类 &1(物料组 &2)必须开启批次管理

四、物料编码自动生成

4.1 编码规则配置表

复制代码
表名:ZT_MATNR_SEQ(物料编码流水号)
  MANDT         TYPE MANDT       " 客户端
  PREFIX        TYPE CHAR2       " 编码前缀(RM/FG/SG)
  CATEGORY      TYPE CHAR2       " 物料类别(01/02/03)
  SERIAL_NUM    TYPE NUMC10      " 当前流水号
  LAST_USED     TYPE TIMESTAMPL  " 最后使用时间
  PRG_CREATED   TYPE PROGNAME    " 创建者程序
  DATE_MODIFIED TYPE DATUM       " 修改日期

4.2 编码生成函数

abap 复制代码
"=======================================================================
" 函数:Z_MATNR_GENERATE
" 功能:根据规则自动生成物料编码(并发安全)
" 返回:生成的物料号 + 消息类型
"=======================================================================

FUNCTION z_matnr_generate.
  IMPORTING
    iv_matkl    TYPE matkl       " 物料组
    iv_prefix   TYPE char5       " 编码前缀(可选)
    iv_category TYPE char2       " 物料类别(可选)
  EXPORTING
    ev_matnr    TYPE matnr       " 生成的物料号(含前导零18位)
    ev_msgtype  TYPE char1       " S=成功 / E=错误
    ev_message  TYPE char100     " 消息内容
  EXCEPTIONS
    no_rule_defined.

  DATA: lv_prefix    TYPE char5,
        lv_category  TYPE char2,
        lv_serial    TYPE numc10,
        ls_seq       TYPE zt_matnr_seq,
        lv_matnr_raw TYPE char18.

  "-------------------------------------------------------------------
  " 步骤1:确定编码前缀
  "-------------------------------------------------------------------
  IF iv_prefix IS NOT INITIAL.
    lv_prefix = iv_prefix.
  ELSE.
    SELECT SINGLE prefix
      FROM zt_matnr_rule
      INTO @lv_prefix
      WHERE matkl  = @iv_matkl
        AND active = 'X'.

    IF sy-subrc <> 0.
      " 未配置规则,用默认前缀
      CASE iv_matkl(2).
        WHEN '01'. lv_prefix = 'RM'.
        WHEN '02'. lv_prefix = 'FG'.
        WHEN '03'. lv_prefix = 'SG'.
        WHEN OTHERS. lv_prefix = 'OT'.
      ENDCASE.
    ENDIF.
  ENDIF.

  "-------------------------------------------------------------------
  " 步骤2:确定物料类别
  "-------------------------------------------------------------------
  IF iv_category IS NOT INITIAL.
    lv_category = iv_category.
  ELSE.
    lv_category = iv_matkl+2(2).
  ENDIF.

  "-------------------------------------------------------------------
  " 步骤3:获取流水号(FOR UPDATE 锁行,保证并发安全)
  "-------------------------------------------------------------------
  SELECT SINGLE *
    FROM zt_matnr_seq
    INTO @ls_seq
    WHERE prefix   = @lv_prefix
      AND category = @lv_category
    FOR UPDATE.

  IF sy-subrc = 0.
    lv_serial = ls_seq-serial_num + 1.

    UPDATE zt_matnr_seq
      SET serial_num    = @lv_serial,
          last_used     = @( zcl_helper=>get_timestamp( ) ),
          date_modified = @sy-datum
      WHERE prefix   = @lv_prefix
        AND category = @lv_category.
  ELSE.
    lv_serial = 1.

    ls_seq-prefix        = lv_prefix.
    ls_seq-category      = lv_category.
    ls_seq-serial_num    = lv_serial.
    ls_seq-last_used     = zcl_helper=>get_timestamp( ).
    ls_seq-prg_created   = sy-cprog.
    ls_seq-date_modified = sy-datum.
    INSERT zt_matnr_seq FROM @ls_seq.
  ENDIF.

  "-------------------------------------------------------------------
  " 步骤4:拼接编码(前缀 + 类别 + 流水号)
  " 示例:RM01000001
  "-------------------------------------------------------------------
  lv_matnr_raw = |{ lv_prefix }{ lv_category }{ lv_serial ALPHA = IN }|.

  " 转换为内部物料号格式(前导补零到18位)
  CALL FUNCTION 'CONVERSION_EXIT_MATN1_INPUT'
    EXPORTING
      input  = lv_matnr_raw
    IMPORTING
      output = ev_matnr.

  ev_msgtype = 'S'.
  ev_message = |编码生成成功:{ ev_matnr }|.

ENDFUNCTION.

五、批量导入工具开发

5.1 方案选型:LSMW vs CATT vs 自定义程序

方案 适用场景 优点 缺点 推荐度
LSMW 标准事务批量操作 配置化、无代码、内置批处理 不够灵活、复杂校验难实现 ★★★☆☆
CATT/eCATT 标准事务回放、回归测试 支持录制、回放 性能差、数据量有限 ★★☆☆☆
自定义导入程序 复杂校验、批量创建、带日志 灵活、完整控制、性能好 需要开发 ★★★★★
BAPI 批量导入 大批量、高性能 性能好、支持 BAPI 扩展 需要理解 BAPI 参数 ★★★★★

推荐:自定义导入程序 + BAPI_MATERIAL_SAVEDATA

5.2 LSMW 配置简要步骤

复制代码
LSMW 创建物料批量导入:

  Step1: 维护对象属性 → 对象 Z_MATNR_IMPORT / 子对象 MM01
  Step2: 维护源结构 → 定义源字段(MATNR/WERKS/MATKL/MEINS/...)
  Step3: 维护目标结构 → BAPI_MATERIAL_SAVEDATA 接口结构
  Step4: 维护字段映射 → 源字段 → 目标字段
  Step5: 规格说明 → 指定 BAPI:BAPI_MATERIAL_SAVEDATA
  Step6: 批量导入程序 → 生成标准程序
  Step7: 读取数据 → 转换 → 执行 → 长文本

5.3 自定义批量导入程序(完整代码,已修正 BAPI 参数)

⚠️ BAPI_MATERIAL_SAVEDATA 正确参数

  • HEADDATACLIENTDATACLIENTDATAXPLANTDATAPLANTDATAXVALUATIONDATAVALUATIONDATAX 都是 IMPORTING 参数(结构)
  • MATERIALDESCRIPTIONRETURNTABLES 参数
  • 注意:每个 DATA 结构都需要对应的 X 结构来标记"哪些字段需要更新"
abap 复制代码
"=======================================================================
" 自定义批量导入程序:ZMM_MATERIAL_IMPORT
" 功能:从CSV文件导入物料主数据,调用BAPI_MATERIAL_SAVEDATA
" 特点:带完整日志、错误回写、事务控制、支持测试模式
"=======================================================================

REPORT zmm_material_import.

"-----------------------------------------------------------------------
" 选择屏幕
"-----------------------------------------------------------------------
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE text-001.
  PARAMETERS: p_file  TYPE rlgrap-filename OBLIGATORY,  " 上传文件路径
              p_test  AS CHECKBOX,                        " 测试运行
              p_limit TYPE i DEFAULT 0.                   " 限量(0=全部)
SELECTION-SCREEN END OF BLOCK b1.

SELECTION-SCREEN BEGIN OF BLOCK b2 WITH FRAME TITLE text-002.
  PARAMETERS: p_werks TYPE werks_d.                       " 默认工厂
SELECTION-SCREEN END OF BLOCK b2.

"-----------------------------------------------------------------------
" 类型定义
"-----------------------------------------------------------------------
TYPES: BEGIN OF ty_source,
         matnr      TYPE matnr,        " 物料号(空则自动生成)
         matkl      TYPE matkl,        " 物料组
         mtart      TYPE mtart,        " 物料类型
         meins      TYPE meins,        " 基本单位
         werks      TYPE werks_d,      " 工厂
         brgew      TYPE brgew,        " 重量
         biskey_de  TYPE char40,       " 描述(德)
         biskey_en  TYPE char40,       " 描述(英)
         old_matnr  TYPE char18,       " 旧物料号
       END OF ty_source.

TYPES: BEGIN OF ty_log,
         guid       TYPE sysuuid_c,
         lineno     TYPE numc5,
         matnr      TYPE matnr,
         status     TYPE char1,        " S=成功 / E=错误 / W=警告
         message    TYPE char255,
         create_ts  TYPE timestampl,
       END OF ty_log.

DATA: gt_source TYPE TABLE OF ty_source,
      gt_log    TYPE TABLE OF ty_log,
      gv_count  TYPE i VALUE 0,
      gv_error  TYPE i VALUE 0,
      gv_success TYPE i VALUE 0.

"-----------------------------------------------------------------------
" 主逻辑
"-----------------------------------------------------------------------
START-OF-SELECTION.

  " 步骤1:读取CSV文件
  PERFORM upload_file USING p_file CHANGING gt_source.

  IF gt_source IS INITIAL.
    MESSAGE '文件为空,请检查路径和格式' TYPE 'E'.
    RETURN.
  ENDIF.

  " 步骤2:限量截取
  IF p_limit > 0 AND p_limit < LINES( gt_source ).
    DELETE gt_source FROM p_limit + 1.
  ENDIF.

  " 步骤3:循环处理
  DATA: lv_total TYPE i,
        lv_cur   TYPE i.
  lv_total = LINES( gt_source ).

  LOOP AT gt_source INTO DATA(ls_source).
    lv_cur = lv_cur + 1.
    gv_count = gv_count + 1.

    " 进度条
    IF lv_cur MOD 5 = 0.
      CALL FUNCTION 'SAPGUI_PROGRESS_INDICATOR'
        EXPORTING
          percentage = ( lv_cur * 100 / lv_total )
          text       = |处理中 { lv_cur } / { lv_total }|.
    ENDIF.

    " 调用物料创建子程序
    PERFORM create_material
      USING    ls_source
      CHANGING DATA(ls_log).

    APPEND ls_log TO gt_log.

    IF ls_log-status = 'S'.
      gv_success = gv_success + 1.
    ELSE.
      gv_error = gv_error + 1.
    ENDIF.

  ENDLOOP.

  " 步骤4:输出结果
  PERFORM output_result.
  PERFORM display_log.


"=======================================================================
" 子程序:读取CSV文件
"=======================================================================
FORM upload_file
  USING    iv_file TYPE rlgrap-filename
  CHANGING ct_data TYPE STANDARD TABLE.

  " 使用 GUI_UPLOAD 读取 CSV
  DATA: lt_raw TYPE TABLE OF string.

  CALL FUNCTION 'GUI_UPLOAD'
    EXPORTING
      filename = iv_file
      filetype = 'ASC'
    TABLES
      data_tab = lt_raw
    EXCEPTIONS
      OTHERS   = 1.

  IF sy-subrc <> 0.
    MESSAGE '文件读取失败' TYPE 'E'.
    RETURN.
  ENDIF.

  " 跳过首行标题,从第二行开始
  LOOP AT lt_raw INTO DATA(lv_line) FROM 2.
    SPLIT lv_line AT ',' INTO
      DATA(lv_matnr) DATA(lv_matkl) DATA(lv_mtart)
      DATA(lv_meins) DATA(lv_werks) DATA(lv_brgew)
      DATA(lv_biskey_de) DATA(lv_biskey_en) DATA(lv_old_matnr).

    APPEND VALUE #(
      matnr     = lv_matnr
      matkl     = lv_matkl
      mtart     = lv_mtart
      meins     = lv_meins
      werks     = COND #( WHEN lv_werks IS INITIAL THEN p_werks ELSE lv_werks )
      brgew     = lv_brgew
      biskey_de = lv_biskey_de
      biskey_en = lv_biskey_en
      old_matnr = lv_old_matnr
    ) TO ct_data.
  ENDLOOP.

ENDFORM.


"=======================================================================
" 子程序:创建物料(调用 BAPI_MATERIAL_SAVEDATA)
"=======================================================================
FORM create_material
  USING    iv_source TYPE ty_source
  CHANGING cv_log    TYPE ty_log.

  DATA: lv_matnr  TYPE matnr,
        lv_error  TYPE flag VALUE space,
        lt_return TYPE TABLE OF bapiret2.

  cv_log-guid      = cl_system_uuid=>create_uuid_c32_static( ).
  cv_log-lineno    = gv_count.
  cv_log-create_ts = zcl_helper=>get_timestamp( ).

  "-----------------------------------------------------------------------
  " 步骤1:确定物料号(自动生成或使用传入值)
  "-----------------------------------------------------------------------
  IF iv_source-matnr IS INITIAL.
    CALL FUNCTION 'Z_MATNR_GENERATE'
      EXPORTING
        iv_matkl   = iv_source-matkl
      IMPORTING
        ev_matnr   = lv_matnr
        ev_msgtype = DATA(lv_msgtype)
        ev_message = DATA(lv_msg).

    IF lv_msgtype <> 'S'.
      cv_log-status  = 'E'.
      cv_log-message = |编码生成失败:{ lv_msg }|.
      RETURN.
    ENDIF.
  ELSE.
    " 传入物料号需要转换为内部格式
    CALL FUNCTION 'CONVERSION_EXIT_MATN1_INPUT'
      EXPORTING input  = iv_source-matnr
      IMPORTING output = lv_matnr.
  ENDIF.

  cv_log-matnr = lv_matnr.

  "-----------------------------------------------------------------------
  " 步骤2:准备 BAPI 参数
  "-----------------------------------------------------------------------
  " HEADDATA(物料抬头)
  DATA: ls_head TYPE bapimathead.
  ls_head-material       = lv_matnr.
  ls_head-industrysector = 'M'.            " 行业:机械工程
  ls_head-materialtype   = iv_source-mtart.
  ls_head-basic_view     = 'X'.            " 创建基本视图

  " CLIENTDATA(客户端级数据)
  DATA: ls_clientdata  TYPE bapimatdoa,
        ls_clientdatax TYPE bapimatdob.
  ls_clientdata-matl_desc = iv_source-biskey_en.
  ls_clientdata-base_uom  = iv_source-meins.
  ls_clientdata-gross_wt  = iv_source-brgew.

  ls_clientdatax-matl_desc = 'X'.
  ls_clientdatax-base_uom  = 'X'.
  ls_clientdatax-gross_wt  = 'X'.

  " PLANTDATA(工厂级数据)
  DATA: ls_plantdata  TYPE bapimatdoa,
        ls_plantdatax TYPE bapimatdob.
  ls_plantdata-plant       = iv_source-werks.
  ls_plantdata-pur_group   = '001'.        " 采购组
  ls_plantdata-mrp_type    = 'PD'.         " MRP类型

  ls_plantdatax-plant      = 'X'.
  ls_plantdatax-pur_group  = 'X'.
  ls_plantdatax-mrp_type   = 'X'.

  " MATERIALDESCRIPTION(物料描述,多语言)
  DATA: lt_text TYPE TABLE OF bapi_makt.
  IF iv_source-biskey_en IS NOT INITIAL.
    APPEND VALUE #(
      material = lv_matnr
      langu    = 'EN'
      maktx    = iv_source-biskey_en
    ) TO lt_text.
  ENDIF.
  IF iv_source-biskey_de IS NOT INITIAL.
    APPEND VALUE #(
      material = lv_matnr
      langu    = 'DE'
      maktx    = iv_source-biskey_de
    ) TO lt_text.
  ENDIF.

  "-----------------------------------------------------------------------
  " 步骤3:调用 BAPI 创建物料
  "-----------------------------------------------------------------------
  CALL FUNCTION 'BAPI_MATERIAL_SAVEDATA'
    EXPORTING
      headdata         = ls_head
      clientdata       = ls_clientdata
      clientdatax      = ls_clientdatax
      plantdata        = ls_plantdata
      plantdatax       = ls_plantdatax
    TABLES
      materialdescription = lt_text
      return           = lt_return.

  "-----------------------------------------------------------------------
  " 步骤4:检查返回消息
  "-----------------------------------------------------------------------
  LOOP AT lt_return INTO DATA(ls_return) WHERE type CA 'EA'.
    lv_error = 'X'.
    cv_log-status  = 'E'.
    cv_log-message = ls_return-message.
    EXIT.
  ENDLOOP.

  IF lv_error = space.
    cv_log-status  = 'S'.
    cv_log-message = |物料 { lv_matnr } 创建成功|.
  ENDIF.

  "-----------------------------------------------------------------------
  " 步骤5:COMMIT / ROLLBACK
  "-----------------------------------------------------------------------
  IF lv_error = 'X' OR p_test = 'X'.
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    IF p_test = 'X' AND lv_error = space.
      cv_log-message = |【测试模式】{ cv_log-message }(已回滚)|.
    ENDIF.
  ELSE.
    CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
      EXPORTING wait = 'X'.
  ENDIF.

ENDFORM.


"=======================================================================
" 子程序:输出结果概览
"=======================================================================
FORM output_result.

  WRITE: / '=================================================='.
  WRITE: /  |物料批量导入完成|.
  WRITE: / '=================================================='.
  WRITE: /  |总处理:  { gv_count } 条|.
  WRITE: /  |成功:    { gv_success } 条|.
  WRITE: /  |错误:    { gv_error } 条|.
  WRITE: /  |测试模式:{ COND #( WHEN p_test = 'X' THEN '是' ELSE '否' ) }|.
  WRITE: / '=================================================='.

  IF p_test = 'X'.
    WRITE: / |注意:测试模式下数据已全部回滚,未保存到数据库|.
  ENDIF.

ENDFORM.


"=======================================================================
" 子程序:ALV 显示日志
"=======================================================================
FORM display_log.

  " 只显示有错误的记录
  IF gv_error = 0.
    RETURN.
  ENDIF.

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

      lo_alv->get_columns( )->set_optimize( 'X' ).
      lo_alv->get_columns( )->get_column( 'STATUS' )->set_medium_text( '状态' ).
      lo_alv->get_columns( )->get_column( 'MESSAGE' )->set_medium_text( '消息' ).
      lo_alv->display( ).

    CATCH cx_salv_msg INTO DATA(lx_msg).
      MESSAGE lx_msg TYPE 'E'.
  ENDTRY.

ENDFORM.

5.4 导入 CSV 文件格式说明

复制代码
CSV 文件格式(首行为标题,从第二行开始是数据):

  MATNR,MATKL,MTART,MEINS,WERKS,BRGEW,BISKEY_DE,BISKEY_EN,OLD_MATNR
  ,01-01,FERT,PC,1000,5.0,Schraube M10,Metal screw M10,OLD001
  ,01-02,FERT,PC,1000,3.2,Mutter M6,Nut M6,OLD002
  RM03000003,03-01,HALB,PC,2000,12.0,Welle,Shaft,OLD003
  ,02-01,VERP,PC,1000,1.5,Verpackung,Packaging,

  说明:
    - 第一列 MATNR 可为空 → 系统自动调用 Z_MATNR_GENERATE 生成编码
    - 工厂 WERKS 必填
    - 物料类型 MTART 必填(FERT=成品 / HALB=半成品 / ROH=原材料 / VERP=包装)

常见问题与排查

  • Q1:BADI_MATERIAL_CHECK 在 MM01 创建物料时不触发?

    A:排查方向:①SE19 检查 BAdI 实现是否激活;②实现的过滤器是否排除了当前物料类型/工厂;③MM01 操作后是否点击了保存 (CHECK_DATA 在保存时才触发);④实现类的方法名是否为 IF_EX_BADI_MATERIAL_CHECK~CHECK_DATA(接口只有一个方法);⑤在实现类开头加日志确认是否执行。

  • Q2:自定义批量导入程序中,BAPI_MATERIAL_SAVEDATA 调用成功了但物料没创建?

    A:检查是否正确调用了 BAPI_TRANSACTION_COMMIT。BAPI 调用成功只是返回,不会自动 COMMIT 。批量导入时每条处理完都要 COMMIT(带 WAIT = 'X'),否则事务回滚数据会丢失。另外检查测试模式标志是否误开。

  • Q3:CSV 批量导入时,中文描述显示乱码?

    A:CSV 文件编码问题。GUI_UPLOAD 默认读取 ASC 编码。建议:①CSV 文件保存为 UTF-8ANSI 编码;②如果 UTF-8 带 BOM,GUI_UPLOAD 可能会有乱码,建议另存为 ANSI;③使用 GUI_UPLOADcodepage 参数指定编码(如 '8400' 表示 UTF-8)。

  • Q4:BADI_MATERIAL_CHECK 的 is_mara / is_marc 结构里看不到自定义字段?

    A:能看到------CI_MARA/CI_MARC 追加结构中的字段会自动包含在 IM_MARA/IM_MARC 结构中。但编译期访问时,如果字段是新追加的,可能提示"字段不存在",需要通过 ASSIGN COMPONENT '字段名' OF STRUCTURE im_mara TO <fs> 动态访问。

  • Q5:批量导入失败了部分记录,怎么重试?

    A:日志表 gt_log 中记录了每条的状态和错误信息。重试流程:①筛选状态 'E' 的记录;②从原 CSV 中提取对应行;③单独重跑。建议扩展程序支持"失败重跑"模式,增加参数 P_RETRY_FILE 和相关处理逻辑。

  • Q6:物料编码自动生成函数和批量导入配合使用时,并发会重复吗?

    A:Z_MATNR_GENERATE 函数中用了 SELECT ... FOR UPDATE 锁行 + 事务提交,能保证并发安全------两个请求同时来,一个拿到锁,另一个等,第一个提交后第二个读+1。但必须在同一个 SAP 系统中使用------多应用服务器同时并发时,FOR UPDATE 的数据库级锁同样有效。

  • Q7:BADI_MATERIAL_CHECK 和用户出口 EXIT_SAPLMGMU_001 选哪个?

    A:选 BADI_MATERIAL_CHECK。用户出口是老式的过程式出口(只有一个 INCLUDE 可写代码,不支持多实现),BAdI 是面向对象的(支持多实现、过滤器、不同方法分职责)。新开发用 BAdI,老项目维护时可能需要同时关注两者。


总结

物料主数据增强开发全流程

步骤 操作 事务码 核心产出
1. 字段扩展 CI_MARA + CI_MARC 追加结构 SE11 基本数据/工厂数据自定义字段
2. BAdI 定位 搜索 BADI_MATERIAL_CHECK SE18/SE19 找到 BAdI 定义
3. 配置表 编码规则 ZT_MATNR_RULE + 黑名单 SE11 业务规则配置
4. BAdI 实现 四个校验维度(编码/描述/旧料号/批次) SE24 完整校验逻辑
5. 编码函数 Z_MATNR_GENERATE SE37 自动编码生成
6. 批量导入 自定义程序 ZMM_MATERIAL_IMPORT SE38 CSV → BAPI 批量创建
7. 测试 MM01 手动 + 批量导入测试 MM01/SE38 确认增强生效

关键代码避坑速查

错误做法 正确做法
BAdI 方法名 IF_EX_BADI_MATERIAL_CHECK~CHECK IF_EX_BADI_MATERIAL_CHECK~CHECK_DATA
活动类型值 IF im_activity = '01' IF im_activity = 'H'(H=创建/V=修改/A=显示)
BAPI 参数 HEADDATA 放在 TABLES HEADDATA 是 IMPORTING 结构
X 结构 只传 DATA 不传 X 每个 DATA 必须对应 X 结构标记更新字段
自定义字段访问 im_mara-zold_matnr ASSIGN COMPONENT 'ZOLD_MATNR' OF STRUCTURE im_mara
BAPI 提交 不调 COMMIT 每条处理后 BAPI_TRANSACTION_COMMIT + WAIT='X'

本篇与下一篇的衔接

本篇(MM 物料主数据) 下一篇(MM 采购申请)
物料主数据字段扩展+校验 采购申请自定义校验+自动转单
BADI_MATERIAL_CHECK BAdI ME_PROCESS_REQ_CUST BAdI
CSV 批量导入程序 采购申请与 SRM 接口集成

下一篇预告:《MM 模块采购申请与审批流开发:自定义校验、自动转单与需求集成实现》------物料主数据增强你会了,MM 模块的下一个高频场景是采购申请。下一篇讲解采购申请增强校验、自动转采购订单逻辑、采购申请与 SRM 系统接口对接、审批流增强配置的完整方案,结合"部门采购需求自动分配采购组、闲置库存不足自动触发采购申请"等典型场景给出实操代码。

作者 :爱喝水的鱼丶

版本记录:2026 年 9 月

💬 你做过物料主数据增强吗?批量导入时踩过哪些坑?欢迎分享你的物料主数据治理经验。

相关推荐
萧瑟余晖1 小时前
Hibernate 核心原理与架构详解
开发语言·架构·hibernate
geovindu1 小时前
rust:Builder Pattern
开发语言·设计模式·rust·建造者模式
白远山1 小时前
健身场馆无人自动化解决方案:从架构到落地实践
java·开发语言·数据库·数据挖掘·需求分析
Data_Journal2 小时前
如何将网页抓取用于机器学习
大数据·开发语言·数据库·python·scrapy
郝学胜-神的一滴2 小时前
Effective Python 条款 13:善用星号解包,告别下标切片拆分的坑
服务器·开发语言·数据结构·python·程序人生·pycharm
一水鉴天2 小时前
词、术语、概念:从特征集收束到统一拓扑变换的升格与降格模型 20260915(豆包)
开发语言·人工智能
weixin_307779133 小时前
C++代码实现MATLAB中的ode45函数功能
开发语言·c++·算法·matlab
是立不是利4 小时前
大文件导致请求被拒的连锁反应
开发语言·前端·javascript