【SAP】販売伝票のユーザEXIT 実装編(Userexit for SalesOrder document implementation tips)

こんにちは。
前回の[概要編](https://qiita.com/Go_Ohta/items/1d3bd888d792e201b983)に続き、今回は、SDの販売伝票におけるユーザEXIT(イグジット)の実装編となります。
前回では「販売伝票のユーザEXITではだいたいこういうことができる」ということを説明しましたが、今回は実装に関する情報となります。TIPS的な内容が主体となり、スクリーンショット等は少ないですが、参考になりましたら幸いです。
検索等でたどり着いた方で、「そもそもユーザEXITって何？」という方は概要編から確認をいただくと理解しやすいかと思います。

[概要編](https://qiita.com/Go_Ohta/items/1d3bd888d792e201b983 "【SAP】販売伝票のユーザEXIT 概要編")

＜注意＞
* ユーザEXIT内にコードを記述できる環境がなかったため、記載しているコードはサンプルとなります。
* ユーザEXITのサブルーチン名には「USEREXIT_」が先頭につきますが、省略して記述している場合があります。

## ユーザEXITの実装スタイル

販売伝票の場合は。MV45AFZZやMV45AFZBといったインクルードに、USEREXIT_CHECK_VBAPやUSEREXIT_MOVE_FIELD_TO_VBAPなどのサブルーチンが複数定義されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326392/9317db20-c5f8-cc6f-461b-09890bf12fa6.png)

ここに複数の設計書のコードを実装すると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326392/0d775a4b-c003-dbb3-56ca-19fea355bdeb.png)

ユーザ要件を実現するためには、ヘッダと明細データ両方の処理が必要となるなど、1設計書の機能で複数のユーザEXITでの実装となる場合があります。また、SAVE_DOCUMENT_PREPAREは伝票保存前のチェック処理になることから、実装が集中することが多いです。こういった前提を踏まえて、以下をご確認いただければと思います。

### ユーザEXITのサブルーチン内に直接コードを記述しない

USEREXITのコードはサブルーチン内に直接記述せず、サブルーチン内ではINCLUDEプログラムを呼び出すのみとして、実コードはINCLUDE側に記述することをお勧めします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326392/f2f6edfc-5569-4156-8b58-a1233a72ce63.png)

サブルーチン内に直接コードを記述するデメリットは以下となります。

* ユーザEXITの元プログラム（MV45AFZZ、同ZBなど）のコード量自体が増えることで可読性やメンテナンス性が落ちる
* 部分的な移送が行いづらくなる。たとえば、部品Aが実装完了したので移送を行いたい。ただし部品Bは未完了なので一時的にコメントアウトして保存したうえで移送を行う必要がある、など

また、実コードをインクルードに外出しするメリットは以下となります。

* ユーザEXIT側のコード量自体が多くならず、可読性が維持される
* 部分的な移送が行いやすくなる。必要なインクルードを移送するだけで対応できる
* ユーザEXIT側のインクルードの位置を変更するだけで、同じインクルードでの実行順序の変更および他ユーザEXITでの実行への変更などが容易になる

## ユーザEXITで使用できる特殊項目

以下に、ユーザEXITでの実装の際に使用できる特殊値をまとめました。

このページを参考にしました
https://answers.sap.com/questions/4939674/mv45afzz---usage-of-xvbakyvbakvbak-fields.html

#### 画面項目
* VBAK、VBAPなど：画面項目値の格納先ワーク領域（※VBAPやVBEPであっても常に構造型）
* *VBAK、*VBAPなど：ワーク値の古い値（値が設定されないなど挙動がうまくいかない場合もあるようです）
* その他、画面にて項目値が定義されている値

画面項目値をそのままユーザEXITのロジックで使用することができます。概要画面の出荷先(KUNWE-KUWEV)など、対応するテーブルおよび項目のIDとは異なる項目で画面が定義されている場合があります。

#### 構造/内部テーブル
*  **「X+DBテーブル名」** （XVBAK、XVBAPなど）：最新データ値 ※変更がなければ古い値のまま
* **「Y+DBテーブル名」** （YVBAK、YVBAPなど）：最新データの1つ前のデータが格納される ※変更が発生していなければ初期値状態

XVBAKは構造、それ以外（XVBAP,XVBEP,XVBKD,XVBPAなど）は**ヘッダ行付き**内部テーブルで定義されており、X始まりの構造・内部テーブル設定値が格納されます。内部テーブルで格納されているテーブルの場合、たとえば単にXVBAP-MATNRと記述するとヘッダ行（構造）の品目コード値を指定したことになりますので、対象となるデータはREAD TABLEや内部テーブルの条件指定読みなどを使用し、明細番号や納入日程行番号を明示的に指定して値を取得する必要があります。

```ABAP:サンプル：明細レベルの値変更チェック
FORM USEREXIT_MOVE_FIELD_TO_VBAP.

DATA: WA_VBAP like line of XVBAP.
WA_XVBAP = VALUE #( XVBAP[ POSNR = VBAP-POSNR ] OPTIONAL ). "現在明細のデータ取得
IF VBAP-WERKS <> WA_XVBAP-WERKS. "画面上のプラント値が変更されたら(画面上値≠格納値だったら)
  VBAP-ZZXXXXX = ABAP_ON.
ENDIF.

ENDFORM.
```

#### 登録・変更・照会の判定

T180-TRTYPの値で判定ができます。実行トランザクション(SY-TCODE)での処理分岐と合わせて使うと便利です。

| T180-TRTYP | 意味 |
|:-:|:-:|
| H | 登録  |
| V  | 変更  |
| A  | 照会  |

```ABAP:登録時の処理分岐
FORM USEREXIT_MOVE_FIELD_TO_VBAK.

CASE T180-TRTYP.
    WHEN 'H'. "登録
      （登録時なら必ず行う処理）
      IF SY-TCODE = 'ZXXX'.
        （特定トランザクションコードの場合のみ行う処理）
      ENDIF.
    WHEN 'C'. "変更
    WHEN 'A'. "照会
  ENDCASE.

ENDFORM.
```

#### 伝票タイプと伝票カテゴリの使い分けについて

販売伝票タイプのパラメータ設定にて、その上位グループである販売伝票カテゴリの設定を行っています。そのため、常に、販売伝票カテゴリ＞販売伝票タイプという階層構造があります。これを使って「販売伝票カテゴリが「販売」に分類される伝票タイプには常にこの処理を行う/行わない、特定の伝票タイプの場合のみ追加で処理を行う/行わない」という記述ができます。なお、この'C'や'H'が何を意味するかは、VBAK-VBTYPのデータエレメント、VBTYPLに紐づくドメイン値を見ることで判断できるのですが、コード上でリテラル値で記述するよりも、インターフェース：IF_SD_DOC_CATEGORYの属性値を使ってこのように書いたほうが”CLEAN"でわかりやすいと思います。

https://free-croud.hatenablog.com/entry/2020/10/17/012103

```ABAP:販売伝票カテゴリと販売伝票タイプの使い分け
FORM USEREXIT_MOVE_FIELD_TO_VBAK.

CASE VBAK-VBTYP.
    WHEN IF_SD_DOC_CATEGORY=>ORDER. "受注
*    WHEN 'C'. "受注
      （受注に分類される伝票タイプすべてで行う処理）
      IF VBAK-AUART = 'ZXXX'.
        （伝票タイプが'ZXXX'の場合のみに追加で行う処理）
      ENDIF.
    WHEN IF_SD_DOC_CATEGORY=>ORDER. "返品
*   WHEN 'H'. "返品 
 ENDCASE.

ENDFORM.
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326392/6c03c8e0-39d9-49e8-a0c5-6fc52b647635.png)

## 各ユーザEXITにおけるTIPS/経験談

#### USEREXIT_CHECK_VBKD、USEREXIT_MOVE_FIELD_TO_VBKDについて

VBKDは、VBPAと同様に、ヘッダレベルと明細レベルの両方のデータを持ちますので、
ヘッダレベルの場合は、明細番号が初期値であることを判定条件とします。

```ABAP:VBKDヘッダレベル・明細レベルの判定
FORM USEREXIT_CHECK_VBKD.

IF VBKD-POSNR IS INITIAL.
  (VBKDヘッダレベルの値チェック処理)
ELSE.
  (VBKD明細レベルの値チェック処理)
ENDIF.

```

#### MOVE_FIELD_TO_VB* と CHECK_VB*の使い分け

前回の概要編で、
><dl>
>  <dt>販売伝票ヘッダ(VBAK)項目関連</dt>
  <dd>値設定 USEREXIT_MOVE_FIELD_TO_VBAK</dd>
  <dd>値チェック USEREXIT_CHECK_VBAK</dd>
</dl>

と書きましたが、この違いについてさらに説明します。
私も実は細かな違いを良く知らないまま設計書を書いていたのですが（えっ）、開発担当の方に教えていただき、以下だと理解しました。

* 値を設定する処理は、MOVE_FIELD_TO_VB*でもCHECK_VB*でも実装できる
* エラーメッセージを出す処理がある場合は**必ずCHECK_VB*で記述する**

MOVE_FIELD_TO_VB*でエラーメッセージを表示すると、画面項目がすべて不活性化（グレーアウト）して操作できなくなり、発生したエラーを解消できない（「画面ロック」などと言ってます）状態になります。そのためエラーメッセージを表示する場合は、必ずCHECK_VB*で処理を行う必要があります。
逆にチェック処理があっても、エラーメッセージを表示しない場合は、MOVE_FIELD_TO_VB*で記述することが可能です。

#### USEREXIT_SOURCE_DETERMINATIONの用途

結論から言いますと、プラントや明細カテゴリをSAP標準とは異なる独自ロジックで決定する場合は、ここに処理を記述する必要があります。

このユーザEXIT、パラメータ設定の説明では、
>「出荷に使用するプラントをどう決定するか使用する。標準システムでは出荷プラントは得意先マスタか得意先品目情報レコードからコピーされるが、独自ルールを適用したいならこのユーザEXITでそのルールを記述する必要がある」

とあります（英語版の文章を訳したため、日本語版の文章とは違いがあります）。

これを知らずにプラントの値を変更するロジックをUSEREXIT_CHECK_VBAPに実装したところ、まず、CHECK_VBAPで決定したプラント値が得意先の出荷プラントや品目の出荷プラントで上書きされました。これは、テストデータに本来使用しない各出荷プラントに値が設定されていたことが原因でした。
上記の出荷プラントを削除し、プラントの値は正しく設定されるようにしたところ、以下の現象が発生しました。

* 条件タブにて税の条件タイプが設定されずエラーとなる
  →出荷国が取得できず、価格決定表の税の条件タイプのところに設定されている必要条件を満たさなくなった
* 出荷ポイントが決定できなくなった
* CHECK_VBAPで設定したプラント値をクリアしてエンター押下、手入力し直してエンターを押下すると出荷ポイントおよび税の条件タイプが適切に設定される

色々調べた結果、以下のような処理を行っているようです。

1. SOURCE_DETERMINATIONに処理がある場合はそのロジックを使用し、そうでない場合はSAP標準ロジックを使用してプラントを決定する
1. 上記でプラントが決定できない場合、ユーザEXITでプラント値を設定しても無視する（えっ）
1. プラントが決定できない場合は、プラントを利用するほかの処理の値決定（今回の場合、プラントの国コードを出荷国と設定する）を行わなくなる
1. 以上により、画面上にはユーザEXITで設定したプラントが設定されているのに、プラントがないことが原因となる各種現象が発生する

なお、明細カテゴリの独自決定の場合もSOURCE_DETEMINATIONで行うようサブルーチンのコメントに記述がありますが、こちらの内容は確認していません（プラントの場合の出荷ポイントや出荷国のように、納入日程カテゴリの決定に影響が出ると思います）。

#### USEREXIT_SAVE_DOCUMENT_PREPAREについてのあれこれ

みんな大好き:kissing_heart: SAVE_DOCUMENT_PREPAREについてです。

* 保存ボタン押下のたびに1回だけ動く処理となる。そのため、明細レベルや納入日程行レベルのデータ処理を行う場合は、XVB*をLOOPする必要がある

* 保存前の最後のチェック処理となることや、CHECK_VB*やMOVE_FIELD_TO_VB*と比べて机上でのロジック設計がしやすい（個人的な感想）ため、実装が集中しやすい
→処理順序の入れ替えや、各処理の統合可否を検討する

* 保存処理を中止する際には、エラーメッセージを出すことで対応できるが、グローバル項目「GF_EXIT_SAVE_DOCUMENT_PREPARE」に'X'を設定することでも保存処理を中止することができる

3番目、これ実はパラメータ設定の文書のところにしれっと（？）載っている内容です。文書によると「'X'を設定しておくと、USEREXIT_SAVE_DOCUMENTを実行後即座に保存処理が中止される」とあります。エラーメッセージをTYPE S DISPLAY LIKE Eで表示すれば、**画面をグレーアウトせずにエラーメッセージを表示して保存をキャンセルする**ことが可能となります。メッセージタイプEの場合、エラー項目の画面にたどり着くのが手間になるほど項目が不活性されますので、GF_EXIT_SAVE_DOCUMENT_PREPARE＋メッセージタイプS DISPLAY LIKE Eでの処理をお勧めします。

ちなみに、「GF_EXIT_SAVE_DOCUMENT_PREPAREをCHECK_VB*などで設定しておけば、SAVE_DOCUMENT_PREPAREでのチェック処理を減らせるのでは？」と思って調べましたが、SAVE_DOCUMENT_PREPAREを呼び出す直前でGF_EXIT_SAVE_DOCUMENT_PREPAREがクリアされているため、この手段は使えませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326392/a5c7e293-205d-4069-1686-0e8d27254503.png)

**↓以下、2021/06/04追加内容**

#### 条件タブへの内容追加（XKOMVテーブルへのエントリ登録）について

EDIや一括登録での受注伝票データ処理用に、追加データBタブに追加した「割引率」の内容を、割引の条件タイプとして明細の条件タブに追加する、という仕様がありました。
ユーザEXIT上では条件タブの値を更新するようなルーチンは見当たらず、XKOMVのエントリを簡易作成できるような都合の良い汎用モジュールも見つかりませんでした。結局、SAP標準が作成するエントリ内容から設定内容を確認して、XKOMVのテーブルのエントリを作成するロジックをEXITに実装しました。その時はCHECK_VBAPで行いました。もっとスマートな方法をご存じの方、ぜひ情報をご提供いただければと思います。

#### 値変更判定用項目

ほかの方の実装内容から確認したのですが、INCLUDE VBAPDATAに、以下のような項目変更判定用項目が定義されています。このフラグをEXIT上で'X'に設定することで、実際にはその値を変更せずとも、各項目値が変更されたとSAP標準ロジックに判定させ、その後の各種標準処理や各種ユーザEXITのロジックを通らせることができるようです。
MOVE_FIELD_TO_VBAPで使用できるようですが、他ルーチンでの使用有無は確認できていません。

|項目名|内容（変数定義時のドイツ語コメントを英語訳）|
|:-:|:-:|
|MATNR_CHANGED|Help field for checking whether the material has been changed|
|WERKS_CHANGED|Help field for checking whether the work has been changed|
|ETDAT_CHANGED|Indicator: Requested date has been changed|
|VRKME_CHANGED|コメントなし：販売単位の変更判定フラグ？（未確認）|

#### 本来のテーブルに割り当てられているルーチンではないところで、値を設定するとどうなるか？

たとえば、明細レベルのルーチン（CHECK_VBAPやMOVE_FIELD_TO_VBAP）で、ビジネスデータ(VBKD)や納入日程行データ(VBEP)の項目値を設定する場合 など、本来のテーブルに割り当てられたルーチンではないルーチンで値を設定すると、様々な不具合が出る可能性があります。対処方法としては主に、本来のテーブル用ルーチンに「尻ぬぐい」的なロジックを実装することで対応しました。
文章だけではわかりづらいと思いますので以下、一例を挙げておきます。

**<現象>**
CHECK_VBAPで明細の支払条件キー(VBKD-ZTERM)の値を設定したところ、画面上では値は適切に設定された。しかし、不完全決定表で明細の支払条件キーが未入力と判定された。
**<対処方法>**
CHECK_VBKDにて、XVBKD（テーブル）がヘッダレベルの場合、VBKD-ZTERM（概要画面の支払条件キー）にダミー値を仕込んでおく、ことで解消した。これがベストの解消方法であるかは不明。

このような仕様は、基本（概要）および詳細設計書作成時には設計想定できない場合が多いです。設計して実装して、動かしてみて不具合を確認して、各種対応とするしかありません。ロジック上どうしても本来のルーチンでないところで値を設定することが避けられない場合は、開発やテストの工数を事前に積み上げておく以外の対処方法はないように思います。
※ただし、たとえば第1明細でヘッダレベルの項目値を設定する、というような場合は特に不具合は起きていないようでした。

## まとめ
販売伝票のユーザEXITについて以下のことを説明しました。

* ユーザEXITの実装方法について
* ユーザEXITで使用できる特殊な構造、
* 各ユーザEXITにおける方法、注意点、TIPSなど

ユーザEXIT関連で判明したことがあれば、この記事を更新していく予定です。
次回はユーザEXITと関連性が深い、追加データBタブについての予定です。
経験則で記載している内容ですので、理解不足、認識間違いなどあるかと思います。
お気づきの際はお手数をおかけいたしますが、コメントや質問等をいただけました幸いです。
今回の内容は以上となります。ありがとうございました。