REPORT zktest2. *----------------------------------------------------------------------- * 概要 *----------------------------------------------------------------------- * 本プログラムは、SAP GUI Dynpro + ALV で入力した購買発注データを、 * SAP S/4HANA の OData V4 API_PURCHASEORDER_2 にPOSTし、 * 購買発注を登録するサンプルである。 * * 重要なハマりポイント: * * 1. Dynpro入力項目名 * GS_HEAD-PURCHASINGGROUP のような長い構造項目名を画面項目に * 直接使うと、環境によって値転送が不安定になることがある。 * そのため、画面項目は GV_BSART / GV_EKGRP など短い変数にし、 * PAI時に screen_to_head( ) で内部構造へ転送する。 * * 2. CSRFトークン * POST前に X-CSRF-Token: Fetch でトークンを取得する必要がある。 * OData V4では、EntitySetではなくサービスルートでCSRFを取得した方が * 安定するため、本コードでは build_csrf_url( ) でサービスルートを使う。 * * 3. Cookie * CSRFトークンだけでは不十分で、CSRF取得時のCookieもPOSTへ渡す必要がある。 * set_header_field( name = 'Cookie' ... ) では実HTTPリクエストに * Cookieが出ない環境があったため、set_cookie( ) で1件ずつ登録する。 * * 4. ALV数量項目 * 数量が 21 → 0.021 になる問題を避けるため、数量型は MENGE_D、 * 単位型は MEINS とし、フィールドカタログでも QUAN/UNIT と * qfieldname を明示する。 * * 5. 単位 * MEINS内部値では PC が ST になることがある。 * ODataへは外部単位 PC を送る必要があるため、 * CONVERSION_EXIT_CUNIT_OUTPUT で外部単位へ変換する。 *----------------------------------------------------------------------- *----------------------------------------------------------------------- * Selection screen *----------------------------------------------------------------------- SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME. PARAMETERS: p_base TYPE string LOWER CASE OBLIGATORY, "例: https://host:44300 p_client TYPE mandt DEFAULT sy-mandt OBLIGATORY, p_user TYPE string LOWER CASE OBLIGATORY, p_pass TYPE string LOWER CASE OBLIGATORY. SELECTION-SCREEN END OF BLOCK b1. *----------------------------------------------------------------------- * Types *----------------------------------------------------------------------- TYPES: BEGIN OF ty_head, purchaseordertype TYPE c LENGTH 4, supplier TYPE c LENGTH 10, purchaseorderdate TYPE d, purchasingorg TYPE c LENGTH 4, purchasinggroup TYPE c LENGTH 3, companycode TYPE c LENGTH 4, documentcurrency TYPE c LENGTH 5, END OF ty_head. TYPES: BEGIN OF ty_item, purchaseorderitem TYPE c LENGTH 5, material TYPE c LENGTH 40, purchaseorderitemtext TYPE c LENGTH 80, "数量は MENGE_D を使用する。 "単純に TYPE p LENGTH ... DECIMALS ... にすると、 "編集可能ALVとの組み合わせで 21 → 0.021 のような "小数点ずれが発生することがある。 orderquantity TYPE menge_d, "単位は MEINS を使用する。 "ただし、MEINS内部値は ST などになることがあるため、 "JSON出力時には json_unit( ) で外部単位へ変換する。 purchaseorderquantityunit TYPE meins, plant TYPE c LENGTH 4, storagelocation TYPE c LENGTH 4, scheduleline TYPE c LENGTH 4, schedulelinedeliverydate TYPE d, "納入日程行数量も数量型として MENGE_D を使用する。 schedulelineorderquantity TYPE menge_d, createdpo TYPE c LENGTH 10, END OF ty_item. TYPES ty_t_item TYPE STANDARD TABLE OF ty_item WITH EMPTY KEY. *----------------------------------------------------------------------- * Global data *----------------------------------------------------------------------- DATA: gs_head TYPE ty_head, gt_item TYPE ty_t_item, gs_item TYPE ty_item. *----------------------------------------------------------------------- * Dynpro 0100 用の短い画面項目 *----------------------------------------------------------------------- * Dynpro画面項目名に GS_HEAD-PURCHASINGGROUP のような長い名前を使うと、 * 画面項目とABAP変数の自動転送が不安定になることがある。 * そのため、画面項目は以下の短いグローバル変数にする。 * * Dynpro 0100 のレイアウト上の入力項目名: * GV_BSART 購買伝票タイプ * GV_LIFNR 仕入先 * GV_BEDAT 購買伝票日付 * GV_EKORG 購買組織 * GV_EKGRP 購買グループ * GV_BUKRS 会社コード * GV_WAERS 通貨 *----------------------------------------------------------------------- DATA: gv_bsart TYPE c LENGTH 4, gv_lifnr TYPE c LENGTH 10, gv_bedat TYPE d, gv_ekorg TYPE c LENGTH 4, gv_ekgrp TYPE c LENGTH 3, gv_bukrs TYPE c LENGTH 4, gv_waers TYPE c LENGTH 5. DATA: go_container TYPE REF TO cl_gui_custom_container, go_grid TYPE REF TO cl_gui_alv_grid, gt_fcat TYPE lvc_t_fcat, gs_layout TYPE lvc_s_layo, gv_ok TYPE sy-ucomm. CONSTANTS: gc_container_name TYPE scrfname VALUE 'CC_ITEM_ALV', "OData V4 Purchase Order API のサービスパス。 "P_BASE と結合してURLを作る。 gc_service_path TYPE string VALUE '/sap/opu/odata4/iwbep/all/srvd_a2x/sap/api_purchaseorder_2/0001', gc_entity_set TYPE string VALUE 'PurchaseOrder'. *----------------------------------------------------------------------- * Local class *----------------------------------------------------------------------- CLASS lcl_app DEFINITION FINAL. PUBLIC SECTION. CLASS-METHODS: init_data, create_alv, refresh_alv, add_item, delete_selected_items, check_input, post_purchase_order. PRIVATE SECTION. CLASS-METHODS: build_fieldcatalog, add_fcat IMPORTING iv_fieldname TYPE lvc_fname iv_coltext TYPE lvc_txt iv_outputlen TYPE i iv_edit TYPE abap_bool, screen_to_head, head_to_screen, build_json RETURNING VALUE(rv_json) TYPE string, json_escape IMPORTING iv_value TYPE csequence RETURNING VALUE(rv_value) TYPE string, json_date IMPORTING iv_date TYPE d RETURNING VALUE(rv_date) TYPE string, json_number IMPORTING iv_value TYPE any RETURNING VALUE(rv_value) TYPE string, json_unit IMPORTING iv_unit TYPE meins RETURNING VALUE(rv_unit) TYPE string, alpha_item IMPORTING iv_value TYPE any RETURNING VALUE(rv_value) TYPE string, alpha_schedule_line IMPORTING iv_value TYPE any RETURNING VALUE(rv_value) TYPE string, build_csrf_url RETURNING VALUE(rv_url) TYPE string, build_post_uri RETURNING VALUE(rv_uri) TYPE string, create_http_client IMPORTING iv_url TYPE string RETURNING VALUE(ro_client) TYPE REF TO if_http_client, fetch_csrf_token IMPORTING io_client TYPE REF TO if_http_client EXPORTING ev_token TYPE string ev_cookie TYPE string, send_post IMPORTING io_client TYPE REF TO if_http_client iv_post_uri TYPE string iv_json TYPE string iv_token TYPE string iv_cookie TYPE string EXPORTING ev_status TYPE i ev_reason TYPE string ev_body TYPE string ev_po TYPE string, extract_cookie_header IMPORTING io_response TYPE REF TO if_http_response RETURNING VALUE(rv_cookie) TYPE string, add_cookies_to_request IMPORTING io_client TYPE REF TO if_http_client iv_cookie TYPE string, extract_json_string IMPORTING iv_json TYPE string iv_key TYPE string RETURNING VALUE(rv_value) TYPE string, extract_po_from_url IMPORTING iv_url TYPE string RETURNING VALUE(rv_po) TYPE string. ENDCLASS. *----------------------------------------------------------------------- * Selection screen PBO *----------------------------------------------------------------------- AT SELECTION-SCREEN OUTPUT. LOOP AT SCREEN. IF screen-name = 'P_PASS'. screen-invisible = 1. MODIFY SCREEN. ENDIF. ENDLOOP. CLASS lcl_app IMPLEMENTATION. METHOD init_data. CLEAR gs_head. CLEAR gt_item. gs_head-purchaseordertype = 'NB'. gs_head-purchaseorderdate = sy-datum. gs_head-documentcurrency = 'JPY'. "内部ヘッダ構造から画面用変数へ初期値を転送する。 head_to_screen( ). APPEND VALUE ty_item( purchaseorderitem = '00010' purchaseorderquantityunit = 'PC' scheduleline = '0001' schedulelinedeliverydate = sy-datum + 7 ) TO gt_item. ENDMETHOD. METHOD head_to_screen. gv_bsart = gs_head-purchaseordertype. gv_lifnr = gs_head-supplier. gv_bedat = gs_head-purchaseorderdate. gv_ekorg = gs_head-purchasingorg. gv_ekgrp = gs_head-purchasinggroup. gv_bukrs = gs_head-companycode. gv_waers = gs_head-documentcurrency. ENDMETHOD. METHOD screen_to_head. "Dynpro画面項目の値を内部ヘッダ構造へ転送する。 "POST前の入力チェックやJSON生成では gs_head を使うため、 "必ず check_input( ) の冒頭で呼ぶ。 gs_head-purchaseordertype = gv_bsart. gs_head-supplier = gv_lifnr. gs_head-purchaseorderdate = gv_bedat. gs_head-purchasingorg = gv_ekorg. gs_head-purchasinggroup = gv_ekgrp. gs_head-companycode = gv_bukrs. gs_head-documentcurrency = gv_waers. ENDMETHOD. METHOD create_alv. IF go_container IS INITIAL. CREATE OBJECT go_container EXPORTING container_name = gc_container_name. CREATE OBJECT go_grid EXPORTING i_parent = go_container. build_fieldcatalog( ). gs_layout-zebra = abap_true. gs_layout-cwidth_opt = abap_true. gs_layout-edit = abap_true. gs_layout-sel_mode = 'A'. go_grid->set_table_for_first_display( EXPORTING is_layout = gs_layout CHANGING it_outtab = gt_item it_fieldcatalog = gt_fcat ). ENDIF. ENDMETHOD. METHOD refresh_alv. IF go_grid IS BOUND. go_grid->refresh_table_display( ). ENDIF. ENDMETHOD. METHOD build_fieldcatalog. CLEAR gt_fcat. add_fcat( iv_fieldname = 'PURCHASEORDERITEM' iv_coltext = '購買伝票明細' iv_outputlen = 10 iv_edit = abap_true ). add_fcat( iv_fieldname = 'MATERIAL' iv_coltext = '品目' iv_outputlen = 18 iv_edit = abap_true ). add_fcat( iv_fieldname = 'PURCHASEORDERITEMTEXT' iv_coltext = '品目テキスト' iv_outputlen = 30 iv_edit = abap_true ). add_fcat( iv_fieldname = 'ORDERQUANTITY' iv_coltext = '購買発注量' iv_outputlen = 15 iv_edit = abap_true ). add_fcat( iv_fieldname = 'PURCHASEORDERQUANTITYUNIT' iv_coltext = '数量単位' iv_outputlen = 8 iv_edit = abap_true ). add_fcat( iv_fieldname = 'PLANT' iv_coltext = 'プラント' iv_outputlen = 8 iv_edit = abap_true ). add_fcat( iv_fieldname = 'STORAGELOCATION' iv_coltext = '保管場所' iv_outputlen = 8 iv_edit = abap_true ). add_fcat( iv_fieldname = 'SCHEDULELINE' iv_coltext = '納入日程行' iv_outputlen = 10 iv_edit = abap_true ). add_fcat( iv_fieldname = 'SCHEDULELINEDELIVERYDATE' iv_coltext = '納入日付' iv_outputlen = 10 iv_edit = abap_true ). add_fcat( iv_fieldname = 'SCHEDULELINEORDERQUANTITY' iv_coltext = '計画数量' iv_outputlen = 15 iv_edit = abap_true ). add_fcat( iv_fieldname = 'CREATEDPO' iv_coltext = '登録済購買伝票' iv_outputlen = 12 iv_edit = abap_false ). "数量項目の重要設定。 "これを設定しないと、編集可能ALVで 21 が 0.021 になるなど、 "小数点位置がずれることがある。 LOOP AT gt_fcat ASSIGNING FIELD-SYMBOL(). CASE -fieldname. WHEN 'ORDERQUANTITY'. -datatype = 'QUAN'. -inttype = 'P'. -ref_table = 'EKPO'. -ref_field = 'MENGE'. -qfieldname = 'PURCHASEORDERQUANTITYUNIT'. -just = 'R'. -edit = abap_true. WHEN 'SCHEDULELINEORDERQUANTITY'. -datatype = 'QUAN'. -inttype = 'P'. -ref_table = 'EKET'. -ref_field = 'MENGE'. -qfieldname = 'PURCHASEORDERQUANTITYUNIT'. -just = 'R'. -edit = abap_true. WHEN 'PURCHASEORDERQUANTITYUNIT'. -datatype = 'UNIT'. -ref_table = 'EKPO'. -ref_field = 'MEINS'. -edit = abap_true. ENDCASE. ENDLOOP. ENDMETHOD. METHOD add_fcat. DATA ls_fcat TYPE lvc_s_fcat. CLEAR ls_fcat. ls_fcat-fieldname = iv_fieldname. ls_fcat-coltext = iv_coltext. ls_fcat-outputlen = iv_outputlen. ls_fcat-edit = iv_edit. IF iv_fieldname CS 'QUANTITY'. ls_fcat-just = 'R'. ENDIF. APPEND ls_fcat TO gt_fcat. ENDMETHOD. METHOD add_item. DATA lv_next TYPE i. IF go_grid IS BOUND. go_grid->check_changed_data( ). ENDIF. lv_next = lines( gt_item ) + 1. APPEND VALUE ty_item( purchaseorderitem = alpha_item( lv_next * 10 ) purchaseorderquantityunit = 'PC' scheduleline = '0001' schedulelinedeliverydate = sy-datum + 7 ) TO gt_item. refresh_alv( ). ENDMETHOD. METHOD delete_selected_items. DATA lt_rows TYPE lvc_t_row. DATA ls_row TYPE lvc_s_row. IF go_grid IS INITIAL. RETURN. ENDIF. go_grid->check_changed_data( ). go_grid->get_selected_rows( IMPORTING et_index_rows = lt_rows ). SORT lt_rows BY index DESCENDING. LOOP AT lt_rows INTO ls_row. DELETE gt_item INDEX ls_row-index. ENDLOOP. refresh_alv( ). ENDMETHOD. METHOD check_input. screen_to_head( ). "編集可能ALVでは、セル入力値は即時に内部テーブルへ反映されない。 "POST前に必ず check_changed_data( ) を呼んで、GT_ITEMへ反映する。 IF go_grid IS BOUND. go_grid->check_changed_data( ). ENDIF. IF gs_head-purchaseordertype IS INITIAL. MESSAGE '購買伝票タイプを入力してください。' TYPE 'E'. ENDIF. IF gs_head-supplier IS INITIAL. MESSAGE '仕入先を入力してください。' TYPE 'E'. ENDIF. IF gs_head-purchaseorderdate IS INITIAL. MESSAGE '購買伝票日付を入力してください。' TYPE 'E'. ENDIF. IF gs_head-purchasingorg IS INITIAL. MESSAGE '購買組織を入力してください。' TYPE 'E'. ENDIF. IF gs_head-purchasinggroup IS INITIAL. MESSAGE '購買グループを入力してください。' TYPE 'E'. ENDIF. IF gs_head-companycode IS INITIAL. MESSAGE '会社コードを入力してください。' TYPE 'E'. ENDIF. IF gs_head-documentcurrency IS INITIAL. MESSAGE '通貨を入力してください。' TYPE 'E'. ENDIF. DELETE gt_item WHERE purchaseorderitem IS INITIAL AND material IS INITIAL. IF gt_item IS INITIAL. MESSAGE '登録対象の明細が存在しません。' TYPE 'E'. ENDIF. LOOP AT gt_item ASSIGNING FIELD-SYMBOL(). -purchaseorderitem = alpha_item( -purchaseorderitem ). -scheduleline = alpha_schedule_line( -scheduleline ). IF -purchaseorderitem IS INITIAL. MESSAGE |明細 { sy-tabix } の購買伝票明細番号が不正です。| TYPE 'E'. ENDIF. IF -material IS INITIAL. MESSAGE |明細 { -purchaseorderitem } の品目を入力してください。| TYPE 'E'. ENDIF. IF -orderquantity <= 0. MESSAGE |明細 { -purchaseorderitem } の購買発注量には0より大きい値を入力してください。| TYPE 'E'. ENDIF. IF -purchaseorderquantityunit IS INITIAL. MESSAGE |明細 { -purchaseorderitem } の数量単位を入力してください。| TYPE 'E'. ENDIF. IF -plant IS INITIAL. MESSAGE |明細 { -purchaseorderitem } のプラントを入力してください。| TYPE 'E'. ENDIF. IF -storagelocation IS INITIAL. MESSAGE |明細 { -purchaseorderitem } の保管場所を入力してください。| TYPE 'E'. ENDIF. IF -scheduleline IS INITIAL. MESSAGE |明細 { -purchaseorderitem } の納入日程行を入力してください。| TYPE 'E'. ENDIF. IF -schedulelinedeliverydate IS INITIAL. MESSAGE |明細 { -purchaseorderitem } の納入日付を入力してください。| TYPE 'E'. ENDIF. IF -schedulelineorderquantity <= 0. MESSAGE |明細 { -purchaseorderitem } の計画数量には0より大きい値を入力してください。| TYPE 'E'. ENDIF. ENDLOOP. ENDMETHOD. METHOD post_purchase_order. DATA: lv_csrf_url TYPE string, lv_post_uri TYPE string, lv_json TYPE string, lv_token TYPE string, lv_cookie TYPE string, lv_status TYPE i, lv_reason TYPE string, lv_body TYPE string, lv_po TYPE string, lo_client TYPE REF TO if_http_client. check_input( ). lv_csrf_url = build_csrf_url( ). lv_post_uri = build_post_uri( ). lv_json = build_json( ). "CSRFトークン取得はサービスルートURLで行う。 "EntitySetでFetchするより、OData V4ではサービスルートの方が安定する。 lo_client = create_http_client( lv_csrf_url ). fetch_csrf_token( EXPORTING io_client = lo_client IMPORTING ev_token = lv_token ev_cookie = lv_cookie ). IF lv_token IS INITIAL. lo_client->close( ). MESSAGE 'レスポンスヘッダに X-CSRF-Token が存在しません。' TYPE 'E'. ENDIF. send_post( EXPORTING io_client = lo_client iv_post_uri = lv_post_uri iv_json = lv_json iv_token = lv_token iv_cookie = lv_cookie IMPORTING ev_status = lv_status ev_reason = lv_reason ev_body = lv_body ev_po = lv_po ). lo_client->close( ). IF lv_status = 200 OR lv_status = 201 OR lv_status = 204. IF lv_po IS NOT INITIAL. LOOP AT gt_item ASSIGNING FIELD-SYMBOL(). -createdpo = lv_po. ENDLOOP. refresh_alv( ). MESSAGE |購買発注の登録が完了しました。購買伝票番号: { lv_po } 明細数: { lines( gt_item ) }| TYPE 'S'. ELSE. MESSAGE |登録は成功しましたが、購買伝票番号を取得できませんでした。HTTP { lv_status }| TYPE 'W'. ENDIF. ELSE. MESSAGE |登録に失敗しました。HTTP { lv_status } { lv_reason } { lv_body }| TYPE 'E'. ENDIF. ENDMETHOD. METHOD build_json. DATA lv_items TYPE string. DATA lv_sched TYPE string. DATA lv_item TYPE string. DATA lv_unit TYPE string. CLEAR rv_json. CLEAR lv_items. LOOP AT gt_item INTO DATA(ls_item). CLEAR: lv_item, lv_sched, lv_unit. "MEINS内部値をOData用の外部単位へ変換する。 "例: " 内部値 ST → 外部値 PC "これをしないと API側で " Unit ST is not created in language EN "のようなエラーになることがある。 lv_unit = json_unit( ls_item-purchaseorderquantityunit ). lv_sched = `{` && `"ScheduleLine":"` && json_escape( ls_item-scheduleline ) && `",` && `"ScheduleLineDeliveryDate":"` && json_date( ls_item-schedulelinedeliverydate ) && `",` && `"ScheduleLineOrderQuantity":` && json_number( ls_item-schedulelineorderquantity ) && `,` && `"PurchaseOrderQuantityUnit":"` && json_escape( lv_unit ) && `"` && `}`. lv_item = `{` && `"PurchaseOrderItem":"` && json_escape( ls_item-purchaseorderitem ) && `",` && `"Material":"` && json_escape( ls_item-material ) && `",` && `"PurchaseOrderItemText":"` && json_escape( ls_item-purchaseorderitemtext ) && `",` && `"OrderQuantity":` && json_number( ls_item-orderquantity ) && `,` && `"PurchaseOrderQuantityUnit":"` && json_escape( lv_unit ) && `",` && `"Plant":"` && json_escape( ls_item-plant ) && `",` && `"StorageLocation":"` && json_escape( ls_item-storagelocation ) && `",` && `"_PurchaseOrderScheduleLineTP":[` && lv_sched && `]` && `}`. IF lv_items IS INITIAL. lv_items = lv_item. ELSE. lv_items = lv_items && `,` && lv_item. ENDIF. ENDLOOP. rv_json = `{` && `"PurchaseOrderType":"` && json_escape( gs_head-purchaseordertype ) && `",` && `"Supplier":"` && json_escape( gs_head-supplier ) && `",` && `"PurchaseOrderDate":"` && json_date( gs_head-purchaseorderdate ) && `",` && `"PurchasingOrganization":"` && json_escape( gs_head-purchasingorg ) && `",` && `"PurchasingGroup":"` && json_escape( gs_head-purchasinggroup ) && `",` && `"CompanyCode":"` && json_escape( gs_head-companycode ) && `",` && `"DocumentCurrency":"` && json_escape( gs_head-documentcurrency ) && `",` && `"_PurchaseOrderItem":[` && lv_items && `]` && `}`. ENDMETHOD. METHOD json_escape. rv_value = CONV string( iv_value ). REPLACE ALL OCCURRENCES OF `\` IN rv_value WITH `\\`. REPLACE ALL OCCURRENCES OF `"` IN rv_value WITH `\"`. REPLACE ALL OCCURRENCES OF cl_abap_char_utilities=>cr_lf IN rv_value WITH `\n`. REPLACE ALL OCCURRENCES OF cl_abap_char_utilities=>newline IN rv_value WITH `\n`. ENDMETHOD. METHOD json_date. IF iv_date IS INITIAL. rv_date = ``. ELSE. rv_date = |{ iv_date DATE = ISO }|. ENDIF. ENDMETHOD. METHOD json_number. rv_value = |{ iv_value }|. CONDENSE rv_value NO-GAPS. REPLACE ALL OCCURRENCES OF ',' IN rv_value WITH '.'. IF rv_value IS INITIAL. MESSAGE '数値項目が空です。' TYPE 'E'. ENDIF. ENDMETHOD. METHOD json_unit. DATA lv_output TYPE c LENGTH 3. IF iv_unit IS INITIAL. rv_unit = ``. RETURN. ENDIF. CALL FUNCTION 'CONVERSION_EXIT_CUNIT_OUTPUT' EXPORTING input = iv_unit language = 'E' IMPORTING output = lv_output EXCEPTIONS unit_not_found = 1 OTHERS = 2. IF sy-subrc = 0 AND lv_output IS NOT INITIAL. rv_unit = lv_output. ELSE. rv_unit = CONV string( iv_unit ). ENDIF. CONDENSE rv_unit NO-GAPS. ENDMETHOD. METHOD alpha_item. DATA lv_num TYPE i. DATA lv_str TYPE string. lv_str = |{ iv_value }|. CONDENSE lv_str NO-GAPS. IF lv_str IS INITIAL. rv_value = ``. RETURN. ENDIF. IF lv_str CO '0123456789'. lv_num = lv_str. rv_value = |{ lv_num WIDTH = 5 PAD = '0' ALIGN = RIGHT }|. ELSE. rv_value = lv_str. ENDIF. ENDMETHOD. METHOD alpha_schedule_line. DATA lv_num TYPE i. DATA lv_str TYPE string. lv_str = |{ iv_value }|. CONDENSE lv_str NO-GAPS. IF lv_str IS INITIAL. rv_value = ``. RETURN. ENDIF. IF lv_str CO '0123456789'. lv_num = lv_str. rv_value = |{ lv_num WIDTH = 4 PAD = '0' ALIGN = RIGHT }|. ELSE. rv_value = lv_str. ENDIF. ENDMETHOD. METHOD build_csrf_url. DATA lv_base TYPE string. DATA lv_len TYPE i. lv_base = p_base. CONDENSE lv_base NO-GAPS. WHILE lv_base CP '*/'. lv_len = strlen( lv_base ) - 1. IF lv_len <= 0. EXIT. ENDIF. lv_base = lv_base+0(lv_len). ENDWHILE. "CSRF取得用URL。 "PurchaseOrder EntitySetではなく、サービスルートを使う。 rv_url = lv_base && gc_service_path && `/?sap-client=` && p_client. ENDMETHOD. METHOD build_post_uri. "POST時は、CSRF取得で生成したHTTPクライアントを使い回すため、 "~request_uri にセットする相対URIを作る。 rv_uri = gc_service_path && `/` && gc_entity_set && `?sap-client=` && p_client. ENDMETHOD. METHOD create_http_client. cl_http_client=>create_by_url( EXPORTING url = iv_url IMPORTING client = ro_client EXCEPTIONS argument_not_found = 1 plugin_not_active = 2 internal_error = 3 OTHERS = 4 ). IF sy-subrc <> 0 OR ro_client IS INITIAL. MESSAGE 'HTTPクライアントの生成に失敗しました。' TYPE 'E'. ENDIF. ro_client->propertytype_logon_popup = if_http_client=>co_disabled. "CookieをHTTPクライアントで受け取れるようにする。 "ただし、本コードでは後続でSet-Cookieから明示的にCookie文字列も抽出し、 "POST前に set_cookie( ) で1件ずつ登録している。 ro_client->propertytype_accept_cookie = if_http_client=>co_enabled. ro_client->authenticate( username = p_user password = p_pass ). ENDMETHOD. METHOD fetch_csrf_token. DATA lv_status TYPE i. DATA lv_reason TYPE string. DATA lv_body TYPE string. CLEAR: ev_token, ev_cookie. io_client->request->set_method( if_http_request=>co_request_method_get ). io_client->request->set_header_field( name = 'Accept' value = 'application/json' ). "変更系リクエスト前にCSRFトークンを取得する。 io_client->request->set_header_field( name = 'X-CSRF-Token' value = 'Fetch' ). io_client->send( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. MESSAGE 'CSRFトークン取得要求の送信に失敗しました。' TYPE 'E'. ENDIF. io_client->receive( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. MESSAGE 'CSRFトークン取得応答の受信に失敗しました。' TYPE 'E'. ENDIF. io_client->response->get_status( IMPORTING code = lv_status reason = lv_reason ). lv_body = io_client->response->get_cdata( ). IF lv_status <> 200. MESSAGE |CSRFトークン取得に失敗しました。HTTP { lv_status } { lv_reason } { lv_body }| TYPE 'E'. ENDIF. ev_token = io_client->response->get_header_field( 'X-CSRF-Token' ). "重要: "CSRFトークンだけではPOST時に403になる。 "GET応答のSet-Cookieも引き継ぐ必要がある。 ev_cookie = extract_cookie_header( io_client->response ). IF ev_token IS INITIAL. MESSAGE |CSRFトークン取得応答に X-CSRF-Token がありません。HTTP { lv_status } { lv_reason }| TYPE 'E'. ENDIF. ENDMETHOD. METHOD extract_cookie_header. DATA lt_fields TYPE tihttpnvp. DATA ls_field TYPE ihttpnvp. DATA lv_pair TYPE string. DATA lv_name TYPE string. io_response->get_header_fields( CHANGING fields = lt_fields ). LOOP AT lt_fields INTO ls_field. lv_name = to_lower( ls_field-name ). IF lv_name = 'set-cookie'. "Set-Cookieは " cookie=value; Path=...; Secure; HttpOnly "の形式で返るため、Cookieヘッダへ渡すのは先頭の cookie=value のみ。 lv_pair = ls_field-value. SPLIT lv_pair AT ';' INTO lv_pair DATA(lv_dummy). CONDENSE lv_pair. IF lv_pair IS NOT INITIAL. IF rv_cookie IS INITIAL. rv_cookie = lv_pair. ELSE. rv_cookie = rv_cookie && '; ' && lv_pair. ENDIF. ENDIF. ENDIF. ENDLOOP. ENDMETHOD. METHOD add_cookies_to_request. DATA: lt_parts TYPE STANDARD TABLE OF string WITH EMPTY KEY, lv_part TYPE string, lv_name TYPE string, lv_value TYPE string, lv_eqpos TYPE i, lv_vpos TYPE i. IF iv_cookie IS INITIAL. RETURN. ENDIF. SPLIT iv_cookie AT ';' INTO TABLE lt_parts. LOOP AT lt_parts INTO lv_part. CONDENSE lv_part. IF lv_part IS INITIAL. CONTINUE. ENDIF. CLEAR: lv_name, lv_value, lv_eqpos, lv_vpos. FIND FIRST OCCURRENCE OF '=' IN lv_part MATCH OFFSET lv_eqpos. IF sy-subrc <> 0. CONTINUE. ENDIF. lv_name = lv_part+0(lv_eqpos). lv_vpos = lv_eqpos + 1. lv_value = lv_part+lv_vpos. CONDENSE lv_name. CONDENSE lv_value. IF lv_name IS INITIAL OR lv_value IS INITIAL. CONTINUE. ENDIF. "今回の最大のハマりポイント: "set_header_field( name = 'Cookie' value = ... ) では、 "ABAPデバッガ上はCookieがあるように見えても、実HTTPリクエストに "Cookieヘッダが出ない環境があった。 " "そのため、Cookie文字列を分解し、set_cookie( ) で1件ずつ登録する。 "ICMトレースでPOSTリクエストに cookie: ... が出ることを確認済み。 io_client->request->set_cookie( name = lv_name value = lv_value ). ENDLOOP. ENDMETHOD. METHOD send_post. DATA lv_location TYPE string. DATA lv_entity_id TYPE string. "GET時の X-CSRF-Token: Fetch などをクリアする。 "その後、POST用に必要なヘッダを再設定する。 io_client->refresh_request( ). io_client->request->set_header_field( name = '~request_uri' value = iv_post_uri ). io_client->request->set_method( if_http_request=>co_request_method_post ). "refresh_request( ) 後は認証ヘッダが消えることがあるため再認証する。 io_client->authenticate( username = p_user password = p_pass ). io_client->request->set_header_field( name = 'Accept' value = 'application/json' ). io_client->request->set_header_field( name = 'Content-Type' value = 'application/json' ). "CSRFヘッダは1本だけセットする。 "Fetchではなく、取得済みトークン値を渡す。 io_client->request->set_header_field( name = 'X-CSRF-Token' value = iv_token ). "CSRF取得時のCookieをPOSTへ引き継ぐ。 add_cookies_to_request( io_client = io_client iv_cookie = iv_cookie ). io_client->request->set_cdata( iv_json ). io_client->send( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. MESSAGE '購買発注POST要求の送信に失敗しました。' TYPE 'E'. ENDIF. io_client->receive( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. MESSAGE '購買発注POST応答の受信に失敗しました。' TYPE 'E'. ENDIF. io_client->response->get_status( IMPORTING code = ev_status reason = ev_reason ). ev_body = io_client->response->get_cdata( ). "OData応答ボディから購買発注番号を抽出する。 ev_po = extract_json_string( iv_json = ev_body iv_key = 'PurchaseOrder' ). "応答ボディに番号が無い場合、Locationヘッダから抽出を試みる。 IF ev_po IS INITIAL. lv_location = io_client->response->get_header_field( 'Location' ). ev_po = extract_po_from_url( lv_location ). ENDIF. "環境によっては OData-EntityId に返ることがある。 IF ev_po IS INITIAL. lv_entity_id = io_client->response->get_header_field( 'OData-EntityId' ). ev_po = extract_po_from_url( lv_entity_id ). ENDIF. ENDMETHOD. METHOD extract_json_string. DATA lv_regex TYPE string. lv_regex = `"` && iv_key && `"\s*:\s*"([^"]*)"`. FIND PCRE lv_regex IN iv_json SUBMATCHES rv_value. ENDMETHOD. METHOD extract_po_from_url. FIND PCRE `PurchaseOrder\('([^']+)'\)` IN iv_url SUBMATCHES rv_po. ENDMETHOD. ENDCLASS. *----------------------------------------------------------------------- * Program flow *----------------------------------------------------------------------- START-OF-SELECTION. lcl_app=>init_data( ). CALL SCREEN 0100. *----------------------------------------------------------------------- * Screen 0100 PBO *----------------------------------------------------------------------- MODULE status_0100 OUTPUT. SET PF-STATUS 'STATUS_0100'. SET TITLEBAR 'TITLE_0100'. lcl_app=>create_alv( ). ENDMODULE. *----------------------------------------------------------------------- * Screen 0100 PAI *----------------------------------------------------------------------- MODULE user_command_0100 INPUT. gv_ok = sy-ucomm. CLEAR sy-ucomm. CASE gv_ok. WHEN 'POST'. lcl_app=>post_purchase_order( ). WHEN 'ADD'. lcl_app=>add_item( ). WHEN 'DEL'. lcl_app=>delete_selected_items( ). WHEN 'BACK' OR 'EXIT' OR 'CANC' OR 'RW' OR '%EX' OR 'ECAN'. LEAVE PROGRAM. WHEN OTHERS. "Enter押下時などは何もしない。 ENDCASE. ENDMODULE.