Integrating OptiPick into SAP EWM
Introduction
Below is a schematic overview of how the pick process is handled today in SAP (on the left) and how it would look like after integrating with our OptiPick API (on the right).
Sales orders are created in SAP SD (Sales & Distribution), these are called Outbound Deliveries. These are then handed over to the EWM (Extended Warehouse Management) system to create ODOs (Outbound Delivery Orders). Optionally the unfulfilled ODOs are grouped into waves based on business rules such as grouping them by carrier or shipping times. Then, WTs (Warehouse Tasks) are created, where the correct source locations in the warehouse for the ordered SKUs are allocated. Optionally, destination sources (such as a specific Handling Unit) can be allocated here as well. After creating these tasks, WOCR (Warehouse Order Creation Rules) are triggered to group orders together into picking routes, these are called WOs (Warehouse Orders). These WOs are finally placed in a queue and assigned to pickers, who then execute these tasks by following instructions on their handheld devices.
The OptiPick API is called after the Warehouse Task Creation, when the locations to pick from are allocated by the EWM. We also recommend to run the WOCR, but to not persist those results. Running these WOCRs provide two significant advantages: (i) they allow OptiPick to calculate the as-is distances, such that you can monitor how much distance is being saved; (ii) they can serve as a fallback mechanism in the unlikely event something goes wrong. By doing this, our API is completely non-intrusive, and operations can never get disturbed.
In the remainder of this guide, we demonstrate how we could generate a request (JSON format) that could be sent to our API. It is important to note that, in order to keep this guide as general as possible, many assumptions and simplifications were made. Your company might have some specific details that would require some modifications to the examples below.
Required API data
Below is a minimal JSON (with truncated information) for the /optimize/cluster endpoint which can do both batching of Warehouse Tasks into Warehouse Orders, as well as determining the optimal sequence of the Tasks within an Order.
1{ 2 "site_name": "Example", 3 "start_points": ["START"], 4 "end_points": ["END"], 5 "picks": [ 6 { 7 "pick_id": "pick_1", 8 "location_id": "BH-06", 9 "order_id": "order_0", 10 "wave_id": "wave_0", 11 "list_id": "list_0", 12 "asis_sequence": 0 13 }, 14 ... 15 ], 16 "parameters": { 17 "max_orders": 2, 18 ... 19 } 20}
Only the following two attributes are required at root-level of the request:
site_name: a reference to the floorplan in our OptiPick web application. This is is necessary to know the distances between all locations in the warehouse.picks: a list of Warehouse Tasks that need to be executed.
For each pick, the following attributes are required:
pick_id: a unique ID for each element. These will be reused in our response so you can easily link back to your input data.location_id: where to pick from in the warehouse. OptiPick supports multiple locations in case SKUs are spread in the warehouse.order_id: an identifier that groups picks for the same customer together. Picks with the same order_id are always picked within the same picking route (Warehouse Order)
Additionally, the following attributes can be specified for each pick:
wave_id: the results of the WOCR, picks with different wave identifiers are never grouped in the same picking route. Our algorithm applies grouping on each wave separately.list_id: an identifier grouping picks together according to the logic of SAP. As explained above, calculating & providing this is recommended. It serves as a fallback mechanism and allows to calculate the distance savings.asis_sequence: the sequence order, according to the SAP logic, in which the picks are executed within a pick route (typically a pick snake). OptiPick supports grouping optimally according to the pick snake by setting one of the parameters. To allow for this, the raw sequence values (so not the index after sorting) have to be provided.
Finally, different parameters can be provided that control the routing & clustering policy, stop conditions (under which conditions no more pick tasks can be added to a pick route), regexes to convert location IDs from the WMS to locations known to the OptiPick webapp, and more.
SAP Data tables
In the following subsections, we’ll have a look at each of the relevant data tables that correspond to the components of the schematic overview above which will allow us to construct a JSON that serves as a request for our API. We’ll look at the data both in the SAP GUI and by making a query in the ABAP (Advanced Business Application Programming) language.
Outbound Deliveries (LIKP, LIPS)
We can inspect our deliveries in the GUI by using the VL03N transaction (delivery per delivery), or by using the table viewer (SE16N) to see information of all deliveries. We’ll focus on the LIPS table here, as it contains more detailed information than the LIKP table.
In the VL03N window, we start with an empty search query to see high-level information of the deliveries:


After double clicking on a delivery and pressing ENTER, we can see its details:

We can also use the table viewer (/nSE16)


Each line in this table corresponds to a pick element for our API request. The table contains many columns, but we only need three pieces of data: (i) the Delivery corresponds to an order_id; (ii) the Item can be used to construct a unique pick_id after concatenating it to Delivery; (iii) the Material corresponds to the sku_id and can be used to join to other tables to get other information such as the location_id.
Let’s write a small query that retrieves these 3 attributes from the LIPS table and graphically displays the first 5 rows:
1REPORT query_LIPS. 2 3TYPES: BEGIN OF ty_lips_min, 4 vbeln TYPE vbeln_vl, " Delivery 5 posnr TYPE posnr_vl, " Item 6 matnr TYPE matnr, " Material 7 END OF ty_lips_min. 8TYPES tt_lips_min TYPE STANDARD TABLE OF ty_lips_min WITH EMPTY KEY. 9 10DATA lt_lips TYPE tt_lips_min. 11 12SELECT l~vbeln, l~posnr, l~matnr 13 FROM lips AS l 14 INNER JOIN likp AS h ON h~vbeln = l~vbeln 15 INTO TABLE @lt_lips 16 WHERE h~vstel = '1710' " SAP Warehouse ID 17 AND h~vbtyp = 'J' " Outbound delivery 18 AND h~wbstk <> 'C' " Not completely processed 19 AND l~lfimg > 0. " Quantity > 0 (optional) 20 21IF lt_lips IS INITIAL. 22 WRITE: / 'No delivery items (LIPS) found for the selection.'. 23 RETURN. 24ENDIF. 25 26"--- Show first 5 rows as a preview 27DATA lt_preview TYPE tt_lips_min. 28lt_preview = lt_lips. 29 30IF lines( lt_preview ) > 5. 31 DELETE lt_preview FROM 6 TO lines( lt_preview ). 32ENDIF. 33 34cl_demo_output=>display( lt_preview ).

Outbound Delivery Orders (/SCWM/PRDO, /SCDL/*)
The information we need to retrieve from these tables is very similar to the LIPS table, but it has the advantage that they have already been transferred from SAP SD to SAP EWM. In the GUI, we can view detailed information for each ODO in /n/SCWM/PRDO. Ideally, the information sent to our API should come from this table, and not the LIPS table. However, in the sandbox environment of SAP, this table does not contain many rows which is why we opted to use the LIPS table instead.

Let’s query /SCDL/PROCI_O (I suffix in PROCI stands for Item-Level, _O suffix stands for Outbound).
1REPORT zquery_proci_o_min. 2 3TYPES: BEGIN OF ty_proci_min, 4 docno TYPE /SCDL/DL_DOCNO_INT, " Sales Document Number 5 itemno TYPE /SCDL/DL_ITEMNO, " Order Line / Item Number 6 productno TYPE /SCDL/DL_PRODUCTNO, " Product Number 7 END OF ty_proci_min. 8TYPES tt_proci_min TYPE STANDARD TABLE OF ty_proci_min WITH EMPTY KEY. 9 10DATA lt_proci TYPE tt_proci_min. 11 12SELECT docno, itemno, productno 13 FROM /scdl/db_proci_o 14 INTO TABLE @lt_proci 15 WHERE /scwm/whno = '1710' " hardcoded WH 16 AND doccat = 'PDO'. 17 18IF lt_proci IS INITIAL. 19 WRITE: / 'No /SCDL/DB_PROCI_O items for LGNUM 1710.'. 20 RETURN. 21ENDIF. 22 23" show first 5 rows only 24DATA lt_preview TYPE tt_proci_min. 25lt_preview = lt_proci. 26IF lines( lt_preview ) > 5. 27 DELETE lt_preview FROM 6 TO lines( lt_preview ). 28ENDIF. 29 30cl_demo_output=>display( lt_preview ).

Locations (SCWM/BINMAT, /SCWM/AQUA, /SAPAPO/MATKEY, …)
Depending on whether your warehouse using fixed location allocation or dynamic slotting, the information of where each product is stored in the warehouse might be stored in different tables.
In the example below, we’ll assume dynamic slotting and join the AQUA and MATKEY tables together on their MATID, after converting one of them to the corresponding format. To resolve conflicts (locations with more than 1 SKU in the table), we’ll take the most recent record.
In the case you would use fixed bin allocation, you could use the SCWM/BINMAT table instead.
Let’s first extract the necessary data from the MATKEY table. We’ll need a matnr and matid. matnr corresponds to the productno from the /SCDL/PROCI_O table or the matnr from the LIPS table. We will need matid to join it with the AQUA table to find locations.
1REPORT z_step1_matkey_min. 2 3TYPES: BEGIN OF ty_matkey_min, 4 matnr TYPE matnr, 5 matid TYPE /sapapo/matkey-matid, 6 END OF ty_matkey_min. 7TYPES tt_matkey_min TYPE STANDARD TABLE OF ty_matkey_min WITH EMPTY KEY. 8 9DATA lt_matkey TYPE tt_matkey_min. 10DATA lt_preview TYPE tt_matkey_min. 11 12SELECT matnr, matid 13 FROM /sapapo/matkey 14 INTO TABLE @lt_matkey 15 WHERE matid IS NOT NULL AND matid <> ''. 16 17IF lt_matkey IS INITIAL. 18 WRITE: / '/SAPAPO/MATKEY: no rows with MATID.'. 19 LEAVE PROGRAM. 20ENDIF. 21 22lt_preview = lt_matkey. 23IF lines( lt_preview ) > 10. 24 DELETE lt_preview FROM 11 TO lines( lt_preview ). 25ENDIF. 26cl_demo_output=>display( lt_preview ).

Now we extract data from the /SCWM/AQUA table which contains warehouse location information for each SKU. We will extract lgpla and matid. lgpla is the location id, and matid to join it with the table we retrieved in the previous step.
1REPORT z_step2_aqua_min. 2 3CONSTANTS c_lgnum TYPE /scwm/lgnum VALUE '1710'. 4 5TYPES: BEGIN OF ty_aqua_sel, 6 lgpla TYPE /scwm/lgpla, 7 matid TYPE /scwm/aqua-matid, 8 END OF ty_aqua_sel. 9TYPES tt_aqua_sel TYPE STANDARD TABLE OF ty_aqua_sel WITH EMPTY KEY. 10 11DATA lt_aqua TYPE tt_aqua_sel. 12DATA lt_preview TYPE tt_aqua_sel. 13 14SELECT lgpla, matid 15 FROM /scwm/aqua 16 INTO TABLE @lt_aqua 17 WHERE lgnum = @c_lgnum 18 AND lgpla <> ''. 19 20IF lt_aqua IS INITIAL. 21 WRITE: / '/SCWM/AQUA: no rows for LGNUM ', c_lgnum, '.'. 22 LEAVE PROGRAM. 23ENDIF. 24 25lt_preview = lt_aqua. 26IF lines( lt_preview ) > 10. 27 DELETE lt_preview FROM 11 TO lines( lt_preview ). 28ENDIF. 29cl_demo_output=>display( lt_preview ).

We notice an additional challenge. The matid from /SAPAPO/MATKEY seems to be in a different format (C22) than matid from /SCWM/AQUA (X16). To solve this, we apply conversion on the MATKEY values.
1REPORT z_step3_convert_uuid_min. 2 3TYPES: BEGIN OF ty_src, 4 matnr TYPE matnr, 5 matid_c22 TYPE /sapapo/matkey-matid, 6 END OF ty_src. 7DATA lt_src TYPE STANDARD TABLE OF ty_src. 8 9TYPES: BEGIN OF ty_out, 10 matnr TYPE matnr, 11 matid_c22 TYPE /sapapo/matkey-matid, 12 matid_x16 TYPE /scwm/aqua-matid, "RAW16 13 END OF ty_out. 14DATA lt_out TYPE STANDARD TABLE OF ty_out. 15DATA ls_out TYPE ty_out. 16 17DATA lv_x16 TYPE /scwm/aqua-matid. 18 19SELECT matnr, matid 20 FROM /sapapo/matkey 21 INTO TABLE @lt_src 22 WHERE matid IS NOT NULL AND matid <> ''. 23 24LOOP AT lt_src INTO DATA(ls_src). 25 CLEAR: lv_x16, ls_out. 26 TRY. 27 cl_system_uuid=>convert_uuid_c22_static( 28 EXPORTING uuid = ls_src-matid_c22 29 IMPORTING uuid_x16 = lv_x16 ). 30 CATCH cx_uuid_error. 31 CONTINUE. 32 ENDTRY. 33 34 ls_out-matnr = ls_src-matnr. 35 ls_out-matid_c22 = ls_src-matid_c22. 36 ls_out-matid_x16 = lv_x16. 37 APPEND ls_out TO lt_out. 38ENDLOOP. 39 40cl_demo_output=>display( lt_out ).

Finally, we throw it all together, and resolve conflicts by taking the most recent rows from AQUA in case we have multiple SKUs on the same location, or multiple locations for the same SKU (the latter could actually be resolved by just passing every location for an SKU to our API).
1REPORT z_build_lt_dict_min. 2 3CONSTANTS c_lgnum TYPE /scwm/lgnum VALUE '1710'. 4 5TYPES: BEGIN OF ty_dict, 6 matnr TYPE matnr, 7 lgpla TYPE /scwm/lgpla, 8 END OF ty_dict. 9TYPES tt_dict TYPE STANDARD TABLE OF ty_dict WITH EMPTY KEY. 10DATA lt_dict TYPE tt_dict. 11 12TYPES: BEGIN OF ty_matkey_min, 13 matnr TYPE matnr, 14 matid TYPE /sapapo/matkey-matid, 15 END OF ty_matkey_min. 16DATA lt_matkey TYPE STANDARD TABLE OF ty_matkey_min. 17 18TYPES: BEGIN OF ty_map_x, 19 matnr TYPE matnr, 20 matid_x16 TYPE /scwm/aqua-matid, 21 END OF ty_map_x. 22DATA lt_map_x TYPE STANDARD TABLE OF ty_map_x. 23 24TYPES: BEGIN OF ty_aqua_sel, 25 lgpla TYPE /scwm/lgpla, 26 matid TYPE /scwm/aqua-matid, 27 END OF ty_aqua_sel. 28DATA lt_aqua TYPE STANDARD TABLE OF ty_aqua_sel. 29 30FIELD-SYMBOLS: <mk> LIKE LINE OF lt_matkey, 31 <mx_fix> LIKE LINE OF lt_map_x, 32 <aq> LIKE LINE OF lt_aqua, 33 <mx> LIKE LINE OF lt_map_x. 34 35"=========================================================================== 36" 1) Read MATKEY and convert MATID (C22/…)-> X16 so it matches AQUA-MATID 37"=========================================================================== 38SELECT matnr, matid 39 FROM /sapapo/matkey 40 INTO TABLE @lt_matkey 41 WHERE matid IS NOT NULL AND matid <> ''. 42 43IF lt_matkey IS INITIAL. 44 WRITE: / '/SAPAPO/MATKEY has no materials with MATID.'. 45 LEAVE PROGRAM. 46ENDIF. 47 48LOOP AT lt_matkey ASSIGNING <mk>. 49 IF <mk>-matid IS INITIAL. 50 CONTINUE. 51 ENDIF. 52 53 DATA lv_x16 TYPE /scwm/aqua-matid. 54 TRY. 55 cl_system_uuid=>convert_uuid_c22_static( 56 EXPORTING uuid = <mk>-matid 57 IMPORTING uuid_x16 = lv_x16 ). 58 CATCH cx_uuid_error. 59 CONTINUE. "skip malformed UUIDs 60 ENDTRY. 61 62 APPEND VALUE ty_map_x( matnr = <mk>-matnr matid_x16 = lv_x16 ) TO lt_map_x. 63ENDLOOP. 64 65" normalize MATNR (leading zeros) and drop duplicates 66LOOP AT lt_map_x ASSIGNING <mx_fix>. 67 DATA lv_m_fix TYPE matnr. 68 lv_m_fix = <mx_fix>-matnr. 69 CALL FUNCTION 'CONVERSION_EXIT_ALPHA_INPUT' 70 EXPORTING input = lv_m_fix 71 IMPORTING output = lv_m_fix. 72 <mx_fix>-matnr = lv_m_fix. 73ENDLOOP. 74DELETE ADJACENT DUPLICATES FROM lt_map_x COMPARING matnr matid_x16. 75 76IF lt_map_x IS INITIAL. 77 WRITE: / 'No valid MATNR <-> MATID_X16 pairs after conversion.'. 78 LEAVE PROGRAM. 79ENDIF. 80 81"=========================================================================== 82" 2) Read AQUA (bins) for this LGNUM and keep minimal fields 83"=========================================================================== 84SELECT lgpla, matid 85 FROM /scwm/aqua 86 INTO TABLE @lt_aqua 87 WHERE lgnum = @c_lgnum 88 AND lgpla <> ''. 89 90IF lt_aqua IS INITIAL. 91 WRITE: / 'AQUA returned no stock rows for LGNUM ', c_lgnum, '.'. 92 LEAVE PROGRAM. 93ENDIF. 94 95"=========================================================================== 96" 3) Build lt_dict: latest AQUA row wins per MATNR 97" (iterate AQUA backwards; linear search on lt_map_x by MATID_X16) 98"=========================================================================== 99CLEAR lt_dict. 100SORT lt_map_x BY matid_x16. 101 102LOOP AT lt_aqua ASSIGNING <aq> FROM lines( lt_aqua ) TO 1 STEP -1. 103 UNASSIGN <mx>. 104 LOOP AT lt_map_x ASSIGNING <mx> WHERE matid_x16 = <aq>-matid. 105 EXIT. 106 ENDLOOP. 107 108 IF <mx> IS ASSIGNED AND <mx>-matnr IS NOT INITIAL 109 AND NOT line_exists( lt_dict[ matnr = <mx>-matnr ] ). 110 APPEND VALUE ty_dict( matnr = <mx>-matnr lgpla = <aq>-lgpla ) TO lt_dict. 111 ENDIF. 112ENDLOOP. 113 114IF lt_dict IS INITIAL. 115 WRITE: / 'Could not resolve any MATNR -> LGPLA from AQUA.'. 116 LEAVE PROGRAM. 117ENDIF. 118 119"--- (Optional) quick preview: first 10 mappings 120DATA lt_preview TYPE tt_dict. 121lt_preview = lt_dict. 122IF lines( lt_preview ) > 10. 123 DELETE lt_preview FROM 11 TO lines( lt_preview ). 124ENDIF. 125cl_demo_output=>display( lt_preview ).

Warehouse Tasks & Orders (/SCWM/ORDIM_O, /SCWM/ORDIM_C)
These are the tables where the results will be persisted too. Below is a filtered view of the /SCWM/ORDIM_O table.

Setting up a connection with OptiPick
We need to allow our SAP system to make a connection the OptiPick API. This can be configured in /nSTRUST and /nSM59. For this, we need to download the Certificate of our OptiPick API to upload it here. The certificate can be downloaded from the browser (https://optipick.api.optioryx.com/). Below are screenshots to do it in Google Chrome:



Most export formats will work, so choose your favourite. Afterwards, go to /nSTRUST in SAP and upload the certificate here. You should see it added in the Certificate List (middle panel).

After adding the certificate, we add an extra connection using /nSM59. In here, create a new “HTTP Connection to External Server”.

Then fill in the following information in the “Technical Settings” and “Logon & Security” tabs.


When configured, you can run a “Connection Test” from here:

We get Error 405 from our API, but this makes sense as the Connection Test just sends a simple GET request (while our API expects a POST). This confirms our connection works! Within ABAP we can use this with the http_client as follows:
1DATA: lo_http TYPE REF TO if_http_client. 2 3cl_http_client=>create_by_destination( 4 EXPORTING destination = 'OptiPick' 5 IMPORTING client = lo_http ).
Creating the warehouse in OptiPick
For our algorithm to know the distances between every pair of locations in your warehouse, a floorplan needs to be drawn (or uploaded in Excel format) to our webapplication at https://optipick.optioryx.com/.
We created one with the name “SAP”. This is the site_name we will use in our API request.



The location regex provided as one of the parameters to our API is used to convert the names of the locations in your WMS to the names of the locations in our webapp. In the example below, the trailing letter from the location names in our WMS is stripped.
ABAP script to generate request
We’re now ready to throw everything we’ve discussed so far together into one big script that generates a JSON that serves as a request for our API, send this JSON through the HTTP client to our API and then print out the response. We will leave out the parsing/processing of our response and persisting those results in the EWM from the script here. We will just group two orders per pick route, and do random grouping to simulate the as-is grouping. The asis_sequences are retrieved from the correct tables.
1REPORT zopti_group_odos2json. 2 3"---------------- Parameters ---------------- 4PARAMETERS: p_lgnum TYPE /scwm/lgnum OBLIGATORY DEFAULT '1710', 5 p_vstel TYPE vstel DEFAULT '1710', " shipping point filter (LIKP) 6 p_max TYPE i DEFAULT 500, " max items to send 7 p_site TYPE string DEFAULT 'SAP', 8 p_dest TYPE rfcdest OBLIGATORY DEFAULT 'OptiPick', " SM59 HTTP dest 9 p_apkey TYPE string LOWER CASE OBLIGATORY 10 DEFAULT '<CENSORED>'. 11 12"---------------- Types ---------------- 13TYPES: BEGIN OF ty_pick, 14 pick_id TYPE string, 15 location_id TYPE string, 16 order_id TYPE vbeln_vl, 17 wave_id TYPE string, 18 list_id TYPE string, 19 asis_sequence TYPE i, " <-- NEW 20 END OF ty_pick. 21TYPES: tt_pick TYPE STANDARD TABLE OF ty_pick WITH EMPTY KEY. 22 23TYPES: ty_regex_pair TYPE string_table. 24TYPES: tt_location_regex TYPE STANDARD TABLE OF ty_regex_pair WITH EMPTY KEY. 25 26" And make sure ty_params includes it: 27TYPES: BEGIN OF ty_params, 28 max_orders TYPE i, 29 location_regex TYPE tt_location_regex, 30 routing_policy TYPE string, 31 END OF ty_params. 32 33TYPES: BEGIN OF ty_payload, 34 site_name TYPE string, 35 picks TYPE tt_pick, 36 parameters TYPE ty_params, 37 END OF ty_payload. 38 39TYPES: BEGIN OF ty_lips_min, 40 vbeln TYPE vbeln_vl, 41 posnr TYPE posnr_vl, 42 matnr TYPE matnr, 43 END OF ty_lips_min. 44TYPES: tt_lips_min TYPE STANDARD TABLE OF ty_lips_min WITH EMPTY KEY. 45 46"---------------- Data ---------------- 47DATA: lt_picks TYPE tt_pick, 48 ls_pick TYPE ty_pick, 49 ls_payload TYPE ty_payload, 50 lv_json_req TYPE string, 51 lv_json_res TYPE string, 52 lv_status TYPE i, 53 lv_reason TYPE string. 54 55DATA: lt_lips TYPE tt_lips_min, 56 lo_http TYPE REF TO if_http_client. 57 58"---------------- JSON helper ---------------- 59CLASS lcl_json DEFINITION FINAL. 60 PUBLIC SECTION. 61 CLASS-METHODS ser IMPORTING data TYPE any RETURNING VALUE(json) TYPE string. 62ENDCLASS. 63CLASS lcl_json IMPLEMENTATION. 64 METHOD ser. 65 json = /ui2/cl_json=>serialize( 66 data = data 67 pretty_name = /ui2/cl_json=>pretty_mode-low_case ). 68 ENDMETHOD. 69ENDCLASS. 70 71START-OF-SELECTION. 72 73"============================================================== 74" 1) Pull delivery items from ERP (LIPS) joined with LIKP for VSTEL 75"============================================================== 76CLEAR lt_lips. 77 78SELECT l~vbeln, l~posnr, l~matnr 79 FROM lips AS l 80 INNER JOIN likp AS h ON h~vbeln = l~vbeln 81 INTO TABLE @lt_lips 82 WHERE h~vstel = @p_vstel 83 AND h~vbtyp = 'J' " Outbound 84 AND h~wbstk <> 'C' " Not yet completed 85 AND l~lfimg > 0. " quantity > 0 (optional) 86 87IF lt_lips IS INITIAL. 88 WRITE: / 'No delivery items (LIPS) found for the selection.'. 89 LEAVE PROGRAM. 90ENDIF. 91 92" Limit to p_max items 93IF lines( lt_lips ) > p_max. 94 DELETE lt_lips FROM p_max + 1 TO lines( lt_lips ). 95ENDIF. 96 97"============================================================== 98" 2) Resolve bins via MATKEY→AQUA (lt_dict) and build picks 99" (this replaces the old BINMAT logic) 100"============================================================== 101 102" -- types & temps needed for this step 103TYPES: BEGIN OF ty_matnr_key, matnr TYPE matnr, END OF ty_matnr_key. 104TYPES tt_matnrs TYPE SORTED TABLE OF ty_matnr_key WITH UNIQUE KEY matnr. 105 106TYPES: BEGIN OF ty_matkey_min, 107 matnr TYPE matnr, 108 matid TYPE /sapapo/matkey-matid, 109 END OF ty_matkey_min. 110TYPES tt_matkey_min TYPE STANDARD TABLE OF ty_matkey_min WITH EMPTY KEY. 111 112TYPES: BEGIN OF ty_aqua_sel, 113 lgpla TYPE /scwm/lgpla, 114 matid TYPE /scwm/aqua-matid, 115 END OF ty_aqua_sel. 116TYPES tt_aqua_sel TYPE STANDARD TABLE OF ty_aqua_sel WITH EMPTY KEY. 117 118TYPES: BEGIN OF ty_dict, 119 matnr TYPE matnr, 120 lgpla TYPE /scwm/lgpla, 121 END OF ty_dict. 122TYPES tt_dict TYPE STANDARD TABLE OF ty_dict WITH EMPTY KEY. 123 124DATA: lt_matnrs TYPE tt_matnrs, 125 lt_aqua TYPE tt_aqua_sel, 126 lt_dict TYPE tt_dict. 127 128FIELD-SYMBOLS: <li> LIKE LINE OF lt_lips. 129 130"--- 2.1 Collect distinct MATNRs (internal format) from LIPS 131LOOP AT lt_lips ASSIGNING <li>. 132 DATA lv_m TYPE matnr. 133 lv_m = <li>-matnr. 134 CALL FUNCTION 'CONVERSION_EXIT_ALPHA_INPUT' 135 EXPORTING input = lv_m 136 IMPORTING output = lv_m. 137 INSERT VALUE ty_matnr_key( matnr = lv_m ) INTO TABLE lt_matnrs. 138ENDLOOP. 139IF lt_matnrs IS INITIAL. 140 WRITE: / 'No LIPS materials.'. 141 RETURN. 142ENDIF. 143 144"--- 2.2 Map MATNR -> MATID (C22) and convert to X16 145DATA lt_matkey TYPE STANDARD TABLE OF ty_matkey_min. 146SELECT matnr, matid 147 FROM /sapapo/matkey 148 INTO TABLE @lt_matkey 149 FOR ALL ENTRIES IN @lt_matnrs 150 WHERE matnr = @lt_matnrs-matnr. 151IF lt_matkey IS INITIAL. 152 WRITE: / '/SAPAPO/MATKEY empty for these MATNRs.'. 153 RETURN. 154ENDIF. 155 156TYPES: BEGIN OF ty_map_x, 157 matnr TYPE matnr, 158 matid_x16 TYPE /scwm/aqua-matid, 159 END OF ty_map_x. 160DATA lt_map_x TYPE STANDARD TABLE OF ty_map_x. 161FIELD-SYMBOLS: <mk> LIKE LINE OF lt_matkey. 162 163LOOP AT lt_matkey ASSIGNING <mk>. 164 IF <mk>-matid IS INITIAL. CONTINUE. ENDIF. 165 DATA lv_x16 TYPE /scwm/aqua-matid. 166 TRY. 167 cl_system_uuid=>convert_uuid_c22_static( 168 EXPORTING uuid = <mk>-matid 169 IMPORTING uuid_x16 = lv_x16 ). 170 CATCH cx_uuid_error. 171 CONTINUE. 172 ENDTRY. 173 APPEND VALUE ty_map_x( matnr = <mk>-matnr matid_x16 = lv_x16 ) TO lt_map_x. 174ENDLOOP. 175 176" normalize MATNRs (leading zeros) and drop dups 177LOOP AT lt_map_x ASSIGNING FIELD-SYMBOL(<mx_fix>). 178 DATA lv_m_fix TYPE matnr. 179 lv_m_fix = <mx_fix>-matnr. 180 CALL FUNCTION 'CONVERSION_EXIT_ALPHA_INPUT' 181 EXPORTING input = lv_m_fix 182 IMPORTING output = lv_m_fix. 183 <mx_fix>-matnr = lv_m_fix. 184ENDLOOP. 185DELETE ADJACENT DUPLICATES FROM lt_map_x COMPARING matnr matid_x16. 186IF lt_map_x IS INITIAL. 187 WRITE: / 'No MATIDs after conversion.'. 188 RETURN. 189ENDIF. 190 191"--- 2.3 Read AQUA (LGNUM scope, minimal fields) 192SELECT lgpla, matid 193 FROM /scwm/aqua 194 INTO TABLE @lt_aqua 195 WHERE lgnum = @p_lgnum 196 AND lgpla <> ''. 197IF lt_aqua IS INITIAL. 198 WRITE: / 'AQUA has no bins in this LGNUM.'. 199 RETURN. 200ENDIF. 201 202"--- 2.4 Build lt_dict (MATNR -> most recent LGPLA) 203FIELD-SYMBOLS: <aq> LIKE LINE OF lt_aqua, 204 <mx> LIKE LINE OF lt_map_x, 205 <d> LIKE LINE OF lt_dict. 206 207CLEAR lt_dict. 208SORT lt_map_x BY matid_x16. "for stable read below if you prefer BINARY SEARCH 209 210" iterate AQUA backwards so latest row wins; linear search in lt_map_x 211LOOP AT lt_aqua ASSIGNING <aq> FROM lines( lt_aqua ) TO 1 STEP -1. 212 UNASSIGN <mx>. 213 LOOP AT lt_map_x ASSIGNING <mx> WHERE matid_x16 = <aq>-matid. 214 EXIT. 215 ENDLOOP. 216 IF <mx> IS ASSIGNED AND <mx>-matnr IS NOT INITIAL 217 AND NOT line_exists( lt_dict[ matnr = <mx>-matnr ] ). 218 APPEND VALUE ty_dict( matnr = <mx>-matnr lgpla = <aq>-lgpla ) TO lt_dict. 219 ENDIF. 220ENDLOOP. 221 222IF lt_dict IS INITIAL. 223 WRITE: / 'Could not resolve any MATNR -> LGPLA from AQUA.'. 224 RETURN. 225ENDIF. 226 227"--- 2.5 Create picks from LIPS using lt_dict 228CLEAR lt_picks. 229LOOP AT lt_lips ASSIGNING <li>. 230 DATA lv_m_for_key TYPE matnr. 231 lv_m_for_key = <li>-matnr. 232 CALL FUNCTION 'CONVERSION_EXIT_ALPHA_INPUT' 233 EXPORTING input = lv_m_for_key 234 IMPORTING output = lv_m_for_key. 235 236 READ TABLE lt_dict WITH KEY matnr = lv_m_for_key ASSIGNING <d>. 237 IF sy-subrc <> 0. CONTINUE. ENDIF. "no bin -> skip 238 239 DATA lv_pos TYPE posnr_vl. 240 lv_pos = <li>-posnr. 241 CALL FUNCTION 'CONVERSION_EXIT_ALPHA_OUTPUT' 242 EXPORTING input = lv_pos 243 IMPORTING output = lv_pos. 244 245 CLEAR ls_pick. 246 ls_pick-pick_id = |{ <li>-vbeln }-{ lv_pos }|. 247 ls_pick-order_id = <li>-vbeln. 248 ls_pick-location_id = <d>-lgpla. " from lt_dict 249 ls_pick-wave_id = 'wave_0'. 250 ls_pick-list_id = 'list_0'. 251 APPEND ls_pick TO lt_picks. 252ENDLOOP. 253 254IF lt_picks IS INITIAL. 255 WRITE: / 'No picks derived (no AQUA bins matched the LIPS items).'. 256 RETURN. 257ENDIF. 258 259"============================================================== 260" Randomly group orders into lists: 2 orders per list_id 261"============================================================== 262CONSTANTS c_orders_per_list TYPE i VALUE 2. 263 264" 1) Collect distinct orders from the picks 265TYPES: BEGIN OF ty_order_key, order_id TYPE vbeln_vl, END OF ty_order_key. 266DATA lt_orders TYPE SORTED TABLE OF ty_order_key WITH UNIQUE KEY order_id. 267 268FIELD-SYMBOLS: <p> LIKE LINE OF lt_picks. 269LOOP AT lt_picks ASSIGNING <p>. 270 INSERT VALUE ty_order_key( order_id = <p>-order_id ) INTO TABLE lt_orders. 271ENDLOOP. 272IF lt_orders IS INITIAL. 273 RETURN. 274ENDIF. 275 276" 2) Attach a random number to each order (seed from current time) 277TYPES: BEGIN OF ty_order_rnd, order_id TYPE vbeln_vl, rnd TYPE i, END OF ty_order_rnd. 278DATA lt_order_rnd TYPE STANDARD TABLE OF ty_order_rnd. 279 280DATA: lv_h TYPE i, lv_s TYPE i, lv_seed TYPE i. 281lv_h = sy-uzeit+0(2). 282lv_m = sy-uzeit+2(2). 283lv_s = sy-uzeit+4(2). 284lv_seed = lv_h * 3600 + lv_m * 60 + lv_s. 285 286DATA lo_rng TYPE REF TO cl_abap_random_int. 287lo_rng = cl_abap_random_int=>create( seed = lv_seed min = 1 max = 2147483647 ). 288 289DATA ls_ok TYPE ty_order_key. 290DATA ls_rnd TYPE ty_order_rnd. 291LOOP AT lt_orders INTO ls_ok. 292 CLEAR ls_rnd. 293 ls_rnd-order_id = ls_ok-order_id. 294 ls_rnd-rnd = lo_rng->get_next( ). 295 APPEND ls_rnd TO lt_order_rnd. 296ENDLOOP. 297SORT lt_order_rnd BY rnd. 298 299" 3) Map each order to a list_id in chunks of c_orders_per_list 300TYPES: BEGIN OF ty_o2l, order_id TYPE vbeln_vl, list_id TYPE string, END OF ty_o2l. 301DATA lt_o2l TYPE HASHED TABLE OF ty_o2l WITH UNIQUE KEY order_id. 302 303DATA: lv_group_ix TYPE i VALUE 1, 304 lv_in_group TYPE i VALUE 0. 305LOOP AT lt_order_rnd INTO ls_rnd. 306 INSERT VALUE ty_o2l( 307 order_id = ls_rnd-order_id 308 list_id = |list_{ lv_group_ix }| ) 309 INTO TABLE lt_o2l. 310 311 lv_in_group = lv_in_group + 1. 312 IF lv_in_group >= c_orders_per_list. 313 lv_in_group = 0. 314 lv_group_ix = lv_group_ix + 1. 315 ENDIF. 316ENDLOOP. 317 318" 4) Stamp list_id back into each pick line 319FIELD-SYMBOLS: <m> LIKE LINE OF lt_o2l. 320LOOP AT lt_picks ASSIGNING <p>. 321 READ TABLE lt_o2l ASSIGNING <m> WITH TABLE KEY order_id = <p>-order_id. 322 IF sy-subrc = 0. 323 <p>-list_id = <m>-list_id. 324 ENDIF. 325ENDLOOP. 326 327"============================================================== 328" Stamp AS-IS sequence per bin from /SCWM/LAGP (Storage Bin Sorting) 329"============================================================== 330 331" 1) Collect distinct bins from the picks 332TYPES: BEGIN OF ty_bin_key, 333 lgpla TYPE /scwm/lgpla, 334 END OF ty_bin_key. 335DATA lt_bins TYPE SORTED TABLE OF ty_bin_key WITH UNIQUE KEY lgpla. 336 337LOOP AT lt_picks ASSIGNING <p>. 338 IF <p>-location_id IS NOT INITIAL. 339 INSERT VALUE ty_bin_key( lgpla = <p>-location_id ) INTO TABLE lt_bins. 340 ENDIF. 341ENDLOOP. 342IF lt_bins IS INITIAL. 343 EXIT. 344ENDIF. 345 346" 2) Read bin master for these bins (LGNUM scope) 347DATA lt_lagp TYPE STANDARD TABLE OF /scwm/lagp. 348SELECT * 349 FROM /scwm/lagp 350 INTO TABLE lt_lagp 351 FOR ALL ENTRIES IN lt_bins 352 WHERE lgnum = p_lgnum 353 AND lgpla = lt_bins-lgpla. 354 355" 3) Build map: bin -> sort sequence (best effort) 356TYPES: BEGIN OF ty_b2s, 357 lgpla TYPE /scwm/lgpla, 358 seq TYPE i, 359 END OF ty_b2s. 360DATA lt_b2s TYPE HASHED TABLE OF ty_b2s WITH UNIQUE KEY lgpla. 361 362FIELD-SYMBOLS: <lg> TYPE any. 363FIELD-SYMBOLS: <c> TYPE any. 364 365DATA lv_seq_i TYPE i. 366DATA lv_ok TYPE abap_bool. 367 368LOOP AT lt_lagp ASSIGNING <lg>. 369 lv_ok = abap_false. 370 CLEAR lv_seq_i. 371 372 " Try common field names for the configured sort sequence 373 ASSIGN COMPONENT 'SRT_POS' OF STRUCTURE <lg> TO <c>. IF sy-subrc = 0. lv_seq_i = <c>. lv_ok = abap_true. ENDIF. 374 IF lv_ok = abap_false. 375 ASSIGN COMPONENT 'SORT_SEQ' OF STRUCTURE <lg> TO <c>. IF sy-subrc = 0. lv_seq_i = <c>. lv_ok = abap_true. ENDIF. 376 ENDIF. 377 IF lv_ok = abap_false. 378 ASSIGN COMPONENT 'SRTSEQ' OF STRUCTURE <lg> TO <c>. IF sy-subrc = 0. lv_seq_i = <c>. lv_ok = abap_true. ENDIF. 379 ENDIF. 380 IF lv_ok = abap_false. 381 ASSIGN COMPONENT 'SORTNO' OF STRUCTURE <lg> TO <c>. IF sy-subrc = 0. lv_seq_i = <c>. lv_ok = abap_true. ENDIF. 382 ENDIF. 383 IF lv_ok = abap_false. 384 ASSIGN COMPONENT 'SORT' OF STRUCTURE <lg> TO <c>. IF sy-subrc = 0. lv_seq_i = <c>. lv_ok = abap_true. ENDIF. 385 ENDIF. 386 387 ASSIGN COMPONENT 'LGPLA' OF STRUCTURE <lg> TO <c>. 388 IF sy-subrc = 0. 389 INSERT VALUE ty_b2s( lgpla = <c> seq = lv_seq_i ) INTO TABLE lt_b2s. 390 ENDIF. 391ENDLOOP. 392 393" 4) Fallback for bins without a configured sequence: stable alphabetical order 394DATA lt_missing TYPE STANDARD TABLE OF ty_bin_key. 395DATA ls_bk TYPE ty_bin_key. 396LOOP AT lt_bins INTO ls_bk. 397 READ TABLE lt_b2s WITH TABLE KEY lgpla = ls_bk-lgpla TRANSPORTING NO FIELDS. 398 IF sy-subrc <> 0. 399 APPEND ls_bk TO lt_missing. 400 ELSE. 401 " treat zero/initial as 'missing' 402 READ TABLE lt_b2s ASSIGNING FIELD-SYMBOL(<brow>) WITH TABLE KEY lgpla = ls_bk-lgpla. 403 IF <brow>-seq IS INITIAL. 404 APPEND ls_bk TO lt_missing. 405 DELETE TABLE lt_b2s FROM <brow>. " <-- note TABLE keyword (hashed table) 406 ENDIF. 407 ENDIF. 408ENDLOOP. 409 410IF lt_missing IS NOT INITIAL. 411 SORT lt_missing BY lgpla. 412 DATA lv_next TYPE i VALUE 1. 413 LOOP AT lt_missing INTO ls_bk. 414 INSERT VALUE ty_b2s( lgpla = ls_bk-lgpla seq = lv_next ) INTO TABLE lt_b2s. 415 lv_next = lv_next + 1. 416 ENDLOOP. 417ENDIF. 418 419" 5) Stamp asis_sequence into picks (unique per bin) 420FIELD-SYMBOLS: <b2s> LIKE LINE OF lt_b2s. 421LOOP AT lt_picks ASSIGNING <p>. 422 READ TABLE lt_b2s ASSIGNING <b2s> WITH TABLE KEY lgpla = <p>-location_id. 423 IF sy-subrc = 0. 424 <p>-asis_sequence = <b2s>-seq. 425 ENDIF. 426ENDLOOP. 427 428" 429"============================================================== 430" 3) Build JSON and POST to your API (print status + req/resp) 431"============================================================== 432DATA lt_pair TYPE string_table. 433APPEND '(.*)[A-z0-9]$' TO lt_pair. 434APPEND '\1' TO lt_pair. 435 436CLEAR ls_payload. 437ls_payload-site_name = p_site. 438ls_payload-picks = lt_picks. 439ls_payload-parameters-max_orders = 2. 440ls_payload-parameters-routing_policy = 'OPTIMIZED'. 441ls_payload-parameters-location_regex = VALUE tt_location_regex( ( lt_pair ) ). 442 443lv_json_req = lcl_json=>ser( ls_payload ). 444 445cl_http_client=>create_by_destination( 446 EXPORTING destination = p_dest 447 IMPORTING client = lo_http ). 448 449lo_http->request->set_method( 'POST' ). 450lo_http->request->set_header_field( name = 'Content-Type' value = 'application/json' ). 451lo_http->request->set_header_field( name = 'accept' value = 'application/json' ). 452lo_http->request->set_header_field( name = 'x-api-key' value = p_apkey ). 453lo_http->request->set_cdata( lv_json_req ). 454 455lo_http->send( ). 456lo_http->receive( ). 457 458lo_http->response->get_status( IMPORTING code = lv_status reason = lv_reason ). 459lv_json_res = lo_http->response->get_cdata( ). 460 461" Show REQ 462CALL TRANSFORMATION sjson2html 463 SOURCE XML lv_json_req 464 RESULT XML DATA(html_req). 465cl_demo_output=>display_html( cl_abap_codepage=>convert_from( html_req ) ). 466 467" Show RESP 468CALL TRANSFORMATION sjson2html 469 SOURCE XML lv_json_res 470 RESULT XML DATA(html_res). 471cl_demo_output=>display_html( cl_abap_codepage=>convert_from( html_res ) ). 472 473lo_http->close( ).
The results of executing this script:



Field mapping (recap)
| OptiPick JSON | SAP Source |
|---|---|
picks[].pick_id | VBELN-POSNR (ERP) or DOCNO-ITEMNO (EWM), stringified |
picks[].order_id | VBELN (ERP) or DOCNO (EWM) |
picks[].location_id | From AQUA→LGPLA (dynamic) or BINMAT→LGPLA (fixed) |
picks[].wave_id | Wave id from your process (if present) |
picks[].list_id | Group label (fallback / as-is), e.g., WOCR list |
picks[].asis_sequence | From /SCWM/LAGP bin sort key |
site_name | OptiPick floorplan name |
parameters.location_regex | Regex pairs to normalize location_id to floorplan nodes |
Where to call OptiPick in SAP EWM?
Pick one of these patterns (teams usually start with (A) then graduate to (B)):
- A. Scheduled job (most common to start):
- Job in SM36 running
ZOPTI_GROUP_ODOS2JSONevery few minutes. - Reads open deliveries (not completed) and sends picks to OptiPick.
- Job in SM36 running
- B. Event-driven:
- Call from a PPF action on ODO save in
/SCWM/PRDO, or from a user exit/BAdI after ODO creation but before WO creation. - Goal: let OptiPick return an optimized grouping/sequence in time for WO creation (you’ll add the write-back later).
- Call from a PPF action on ODO save in
Fallback strategy: keep WOCR result uncommitted until OptiPick returns. On error/time-out, persist WOCR as is.
This program mimics current grouping & route order so OptiPick can compare “as-is” with optimized. It intentionally does not persist anything in EWM.
Persisting the results
- Persist grouping: create WOs and assign WTs to the optimized routes.
- Persist sequence: set the execution order within each WO to follow OptiPick’s route.
- Queue assignment: assign WOs to queues/users as per your rules.
- Traceability: store request/response _run_id or timestamp and the as-is/optimized distances for KPI dashboards.
Setup checklist
- Authorizations: read /SCDL/*, /SCWM/AQUA, /SCWM/BINMAT, /SCWM/LAGP, run STRUST/SM59.
- Warehouse filters: /SCWM/WHNO, DOCCAT aligned with scope.
- Feature toggle: TVARVC param Z_OPTIPICK_ENABLED per warehouse.
- Connectivity: outbound proxy/egress allow-listing to optipick.api.optioryx.com; SSL trust complete.
- Floorplan: start/end nodes, regex pairs tested, multilevel aisles modeled.
Additional Tips & Tricks
Reliability & monitoring
- Retries/backoff for HTTP 429/5xx.
- Timeouts and circuit breaker (skip optimization if service unstable).
- Log to Application Log (SLG1): selection set, request hash, HTTP code, scenario/run id, decision (optimized vs fallback).
Idempotency & pagination
- If you call repeatedly during a status window, ensure idempotent updates (only write WOs once).
- For very large waves, paginate picks and preserve order boundaries.
Performance
- Push filters to DB; use FOR ALL ENTRIES and keep MATNR ALPHA normalized once.
- Consider CDS/AMDP to pre-join /SCDL/DB_PROCI_O with location lookup if volumes are big.
Floorplan fidelity
- Ensure every location_id maps to a node; keep regex pairs in sync with bin code changes.
- For multi-bin items, include all candidate bins (OptiPick can choose among them).
Security
- Store API keys in SSFS or secured Z-table; don’t hardcode.
- Restrict who can view/maintain destinations and keys.