Use-case: Predictive Stock Transfer & Automatic Purchase Re-Order (Plant-to-Plant A2A scenario driven by real-time APIs)
A fast-moving material in Plant A is kept in stock by an end-to-end, automated flow that
checks forecast demand, looks for internal surplus in nearby plants, and automatically creates stock transfers or purchase requisitions as needed.
Prerequisities and needed
SAP S/4HANA Cloud (Inventory & MRP) – On-hand stock, stock in transit, MRP itemsSAP Integrated Business Planning (IBP) – Demand forecast for FG-100 at Plant ASAP Extended Warehouse Management (EWM) – Real-time on-hand stock incl. quarantineSAP Ariba – Supplier catalog and pricing for external optionsExternal supplier catalog (Ariba-like) – External pricing and lead timesSAP Integration Suite / SAP Event Mesh – Orchestrates the end-to-end flow and publishes events
Sequence – how the APIs interact end-to-end
Step 0 – Trigger
A nightly iFlow in SAP Integration Suite (or SAP Event Mesh) starts the orchestration.
Step 1 – Demand forecast
GET /api/ibp/v1/demandplanning/forecast?material=FG-100&plant=A&weeks=4
→ Returns 1,200 pcs forecasted demand.
Step 2 – Current & projected stock in Plant A
GET /sap/opu/odata/sap/API_MATERIAL_STOCK_SRV/MaterialStock?material=FG-100&plant=A
→ 180 pcs unrestricted, 40 pcs blocked.
GET /sap/opu/odata/sap/API_MRP_COCKPIT_SRV/MrpItems?material=FG-100&plant=A
Result: confirmed receipts 600 pcs
→ Confirmed receipts 600 pcs, so net shortage = 1,200 – 180 – 600 = 420 pcs.
Step 3 – Locate surplus stock in the network
Parallel calls (one per plant):
GET /sap/opu/odata/sap/API_MATERIAL_STOCK_SRV/MaterialStock?material=FG-100&plant=B
→ 300 pcs unrestricted.
GET /sap/opu/odata/sap/API_MATERIAL_STOCK_SRV/MaterialStock?material=FG-100&plant=C
→ 250 pcs unrestricted.
Step 4 – Check ATP (available-to-transfer) incl. transit time
POST /sap/opu/odata/sap/API_ATP_CHECK_SRV/CheckAvailability
Body:
{
“material”: “FG-100”,
“plant”: “B”,
“demandQty”: 300,
“requiredDate”: “2024-06-25”
}
→ Confirms 300 pcs can be delivered by 2024-06-23.
Same for Plant C → 120 pcs available by 2024-06-24.
Step 5 – Decide cheapest internal option
Cost service (custom REST on S/4):
POST /internal/transferCost
{
“fromPlants”: [“B”,”C”],
“toPlant”: “A”,
“qty”: [300,120]
}
→ Plant B has the lowest freight cost (€0.05/pc vs €0.07/pc).
Step 6 – Create stock transport order (STO)
POST /sap/opu/odata/sap/API_STOCK_TRANSPORT_ORDER_SRV/A_StockTransportOrder
Body:
{
“SupplyingPlant”: “B”,
“ReceivingPlant”: “A”,
“Material”: “FG-100”,
“OrderQuantity”: 300,
“DeliveryDate”: “2024-06-23”
}
→ STO 4500012345 created.
Step 7 – Remaining uncovered quantity
Shortage after internal transfer = 420 – 300 = 120 pcs.
Call Ariba to get best external price:
GET /v2/suppliers/catalog?material=FG-100&qty=120¤cy=EUR
→ Supplier S-987 offers €2.30/pc, lead time 7 days.
Step 8 – Create purchase requisition in S/4
POST /sap/opu/odata/sap/API_PURCHASEREQ_PROCESS_SRV/A_PurchaseRequisitionHeader
Body:
{
“Material”: “FG-100”,
“Plant”: “A”,
“Quantity”: 120,
“DeliveryDate”: “2024-06-27”,
“SupplierHint”: “S-987”
}
→ PR 1000123456 created.
Step 9 – Notify planners
Publish event to SAP Event Mesh topic /business/plantA/stockReplenished
Payload:
{
“material”: “FG-100”,
“sto”: “4500012345”,
“pr”: “1000123456”,
“status”: “covered”
}
Outcome
Plant A will receive 300 pcs from Plant B via an automatically created STO and 120 pcs via a purchase requisition with the cheapest external supplier—no manual intervention, no stock-out, and minimal freight cost.
complete code
#!/usr/bin/env python3
“””
End-to-end A2A flow:
1. Read demand forecast (IBP)
2. Check stock & MRP in Plant A
3. Search surplus stock in Plants B/C
4. Run ATP check
5. Create STO (cheapest internal)
6. Create PR for remaining qty (Ariba best price)
“””
import os, json, math
from datetime import datetime, timedelta
from dotenv import load_dotenv
from requests import Session
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import BackendApplicationClient
load_dotenv()
# ———- CONFIG ———-
MATERIAL = “FG-100”
PLANT_A = “A”
PLANTS_SURPLUS = [“B”, “C”]
DEMAND_WEEKS = 4
TRANSPORT_DAYS = 2
# —————————-
s = Session()
# ———- 0. OAUTH TOKEN for S/4 ———-
def s4_token():
url = f”{os.getenv(‘S4_HOST’)}/oauth/token”
r = s.post(url,
auth=(os.getenv(‘S4_USER’), os.getenv(‘S4_PASSWORD’)),
data={‘grant_type’:’client_credentials’})
r.raise_for_status()
return r.json()[‘access_token’]
s.headers.update({‘Authorization’: f”Bearer {s4_token()}”})
# ———- 1. IBP – demand forecast ———-
def ibp_forecast():
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_DEMAND_PLANNING_SRV/DemandForecast”)
params = {
“$filter”: f”Material eq ‘{MATERIAL}’ and Plant eq ‘{PLANT_A}'”,
“$select”: “DemandQuantity”,
“$format”: “json”
}
r = s.get(url, params=params)
r.raise_for_status()
total = sum(item[‘DemandQuantity’] for item in r.json()[‘d’][‘results’])
return total
demand_qty = ibp_forecast()
print(“Demand forecast:”, demand_qty)
# ———- 2. Stock & MRP ———-
def plant_stock(plant):
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_MATERIAL_STOCK_SRV/MaterialStock”)
params = {
“$filter”: f”Material eq ‘{MATERIAL}’ and Plant eq ‘{plant}’ and “
f”StorageLocation ne ””,
“$format”: “json”
}
r = s.get(url, params=params)
r.raise_for_status()
return sum(item[‘UnrestrictedStockQuantity’]
for item in r.json()[‘d’][‘results’])
def plant_receipts(plant):
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_MRP_COCKPIT_SRV/MrpItems”)
params = {
“$filter”: f”Material eq ‘{MATERIAL}’ and Plant eq ‘{plant}’ and “
f”MrpElementCategory eq ‘AR'”,
“$format”: “json”
}
r = s.get(url, params=params)
r.raise_for_status()
return sum(item[‘Quantity’] for item in r.json()[‘d’][‘results’])
stock_A = plant_stock(PLANT_A)
receipts_A = plant_receipts(PLANT_A)
shortage = max(0, demand_qty – stock_A – receipts_A)
print(“Shortage:”, shortage)
if shortage == 0:
print(“No action needed.”)
exit()
# ———- 3. Surplus stock ———-
surplus = {}
for p in PLANTS_SURPLUS:
surplus[p] = plant_stock(p)
print(“Surplus:”, surplus)
# ———- 4. ATP check ———-
def atp_ok(plant, qty, req_date):
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_ATP_CHECK_SRV/CheckAvailability”)
body = {
“Material”: MATERIAL,
“Plant”: plant,
“DemandQuantity”: str(qty),
“RequiredDate”: req_date.isoformat()
}
r = s.post(url, json=body)
r.raise_for_status()
return r.json()[‘d’][‘ConfirmedQuantity’] == str(qty)
best_internal = None
needed = shortage
for plant, qty in surplus.items():
take = min(qty, needed)
req = datetime.utcnow().date() + timedelta(days=TRANSPORT_DAYS)
if atp_ok(plant, take, req):
best_internal = (plant, take)
break
if best_internal:
plant, qty = best_internal
print(f”Best internal: {qty} from {plant}”)
# ———- 5. Create STO ———-
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_STOCK_TRANSPORT_ORDER_SRV/A_StockTransportOrder”)
body = {
“SupplyingPlant”: plant,
“ReceivingPlant”: PLANT_A,
“Material”: MATERIAL,
“OrderQuantity”: str(qty),
“DeliveryDate”: req.isoformat()
}
r = s.post(url, json=body)
r.raise_for_status()
sto = r.json()[‘d’][‘StockTransportOrder’]
print(“STO created:”, sto)
shortage -= qty
# ———- 6. Ariba – best external price ———-
if shortage > 0:
client = BackendApplicationClient(client_id=os.getenv(‘ARIBA_CLIENT_ID’))
oauth = OAuth2Session(client=client)
token = oauth.fetch_token(
token_url=os.getenv(‘ARIBA_TOKEN_URL’),
client_id=os.getenv(‘ARIBA_CLIENT_ID’),
client_secret=os.getenv(‘ARIBA_CLIENT_SECRET’)
)
url = “https://api.ariba.com/v2/suppliers/catalog”
params = {
“material”: MATERIAL,
“qty”: shortage,
“currency”: “EUR”
}
r = oauth.get(url, params=params)
r.raise_for_status()
best = min(r.json()[‘offers’], key=lambda x: float(x[‘price’]))
print(“Best supplier:”, best[‘supplier’], best[‘price’])
# ———- 7. Create PR ———-
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_PURCHASEREQ_PROCESS_SRV/A_PurchaseRequisitionHeader”)
body = {
“Material”: MATERIAL,
“Plant”: PLANT_A,
“Quantity”: str(shortage),
“DeliveryDate”: (datetime.utcnow().date()
+ timedelta(days=int(best[‘leadtime’]))).isoformat(),
“SupplierHint”: best[‘supplier’]
}
r = s.post(url, json=body)
r.raise_for_status()
pr = r.json()[‘d’][‘PurchaseRequisition’]
print(“PR created:”, pr)
print(“Flow finished.”)
Use-case: Predictive Stock Transfer & Automatic Purchase Re-Order (Plant-to-Plant A2A scenario driven by real-time APIs)A fast-moving material in Plant A is kept in stock by an end-to-end, automated flow that checks forecast demand, looks for internal surplus in nearby plants, and automatically creates stock transfers or purchase requisitions as needed. Prerequisities and neededSAP S/4HANA Cloud (Inventory & MRP) – On-hand stock, stock in transit, MRP itemsSAP Integrated Business Planning (IBP) – Demand forecast for FG-100 at Plant ASAP Extended Warehouse Management (EWM) – Real-time on-hand stock incl. quarantineSAP Ariba – Supplier catalog and pricing for external optionsExternal supplier catalog (Ariba-like) – External pricing and lead timesSAP Integration Suite / SAP Event Mesh – Orchestrates the end-to-end flow and publishes eventsSequence – how the APIs interact end-to-endStep 0 – TriggerA nightly iFlow in SAP Integration Suite (or SAP Event Mesh) starts the orchestration.Step 1 – Demand forecastGET /api/ibp/v1/demandplanning/forecast?material=FG-100&plant=A&weeks=4→ Returns 1,200 pcs forecasted demand.Step 2 – Current & projected stock in Plant AGET /sap/opu/odata/sap/API_MATERIAL_STOCK_SRV/MaterialStock?material=FG-100&plant=A→ 180 pcs unrestricted, 40 pcs blocked.GET /sap/opu/odata/sap/API_MRP_COCKPIT_SRV/MrpItems?material=FG-100&plant=AResult: confirmed receipts 600 pcs→ Confirmed receipts 600 pcs, so net shortage = 1,200 – 180 – 600 = 420 pcs.Step 3 – Locate surplus stock in the networkParallel calls (one per plant):GET /sap/opu/odata/sap/API_MATERIAL_STOCK_SRV/MaterialStock?material=FG-100&plant=B→ 300 pcs unrestricted.GET /sap/opu/odata/sap/API_MATERIAL_STOCK_SRV/MaterialStock?material=FG-100&plant=C→ 250 pcs unrestricted.Step 4 – Check ATP (available-to-transfer) incl. transit timePOST /sap/opu/odata/sap/API_ATP_CHECK_SRV/CheckAvailabilityBody:{
“material”: “FG-100”,
“plant”: “B”,
“demandQty”: 300,
“requiredDate”: “2024-06-25”
}→ Confirms 300 pcs can be delivered by 2024-06-23.Same for Plant C → 120 pcs available by 2024-06-24.Step 5 – Decide cheapest internal optionCost service (custom REST on S/4):POST /internal/transferCost{
“fromPlants”: [“B”,”C”],
“toPlant”: “A”,
“qty”: [300,120]
}→ Plant B has the lowest freight cost (€0.05/pc vs €0.07/pc).Step 6 – Create stock transport order (STO)POST /sap/opu/odata/sap/API_STOCK_TRANSPORT_ORDER_SRV/A_StockTransportOrderBody:{
“SupplyingPlant”: “B”,
“ReceivingPlant”: “A”,
“Material”: “FG-100”,
“OrderQuantity”: 300,
“DeliveryDate”: “2024-06-23”
}→ STO 4500012345 created.Step 7 – Remaining uncovered quantityShortage after internal transfer = 420 – 300 = 120 pcs.Call Ariba to get best external price:GET /v2/suppliers/catalog?material=FG-100&qty=120¤cy=EUR→ Supplier S-987 offers €2.30/pc, lead time 7 days.Step 8 – Create purchase requisition in S/4POST /sap/opu/odata/sap/API_PURCHASEREQ_PROCESS_SRV/A_PurchaseRequisitionHeaderBody:{
“Material”: “FG-100”,
“Plant”: “A”,
“Quantity”: 120,
“DeliveryDate”: “2024-06-27”,
“SupplierHint”: “S-987”
}→ PR 1000123456 created.Step 9 – Notify plannersPublish event to SAP Event Mesh topic /business/plantA/stockReplenishedPayload:{
“material”: “FG-100”,
“sto”: “4500012345”,
“pr”: “1000123456”,
“status”: “covered”
}OutcomePlant A will receive 300 pcs from Plant B via an automatically created STO and 120 pcs via a purchase requisition with the cheapest external supplier—no manual intervention, no stock-out, and minimal freight cost.complete code#!/usr/bin/env python3
“””
End-to-end A2A flow:
1. Read demand forecast (IBP)
2. Check stock & MRP in Plant A
3. Search surplus stock in Plants B/C
4. Run ATP check
5. Create STO (cheapest internal)
6. Create PR for remaining qty (Ariba best price)
“””
import os, json, math
from datetime import datetime, timedelta
from dotenv import load_dotenv
from requests import Session
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import BackendApplicationClient
load_dotenv()
# ———- CONFIG ———-
MATERIAL = “FG-100”
PLANT_A = “A”
PLANTS_SURPLUS = [“B”, “C”]
DEMAND_WEEKS = 4
TRANSPORT_DAYS = 2
# —————————-
s = Session()
# ———- 0. OAUTH TOKEN for S/4 ———-
def s4_token():
url = f”{os.getenv(‘S4_HOST’)}/oauth/token”
r = s.post(url,
auth=(os.getenv(‘S4_USER’), os.getenv(‘S4_PASSWORD’)),
data={‘grant_type’:’client_credentials’})
r.raise_for_status()
return r.json()[‘access_token’]
s.headers.update({‘Authorization’: f”Bearer {s4_token()}”})
# ———- 1. IBP – demand forecast ———-
def ibp_forecast():
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_DEMAND_PLANNING_SRV/DemandForecast”)
params = {
“$filter”: f”Material eq ‘{MATERIAL}’ and Plant eq ‘{PLANT_A}'”,
“$select”: “DemandQuantity”,
“$format”: “json”
}
r = s.get(url, params=params)
r.raise_for_status()
total = sum(item[‘DemandQuantity’] for item in r.json()[‘d’][‘results’])
return total
demand_qty = ibp_forecast()
print(“Demand forecast:”, demand_qty)
# ———- 2. Stock & MRP ———-
def plant_stock(plant):
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_MATERIAL_STOCK_SRV/MaterialStock”)
params = {
“$filter”: f”Material eq ‘{MATERIAL}’ and Plant eq ‘{plant}’ and “
f”StorageLocation ne ””,
“$format”: “json”
}
r = s.get(url, params=params)
r.raise_for_status()
return sum(item[‘UnrestrictedStockQuantity’]
for item in r.json()[‘d’][‘results’])
def plant_receipts(plant):
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_MRP_COCKPIT_SRV/MrpItems”)
params = {
“$filter”: f”Material eq ‘{MATERIAL}’ and Plant eq ‘{plant}’ and “
f”MrpElementCategory eq ‘AR'”,
“$format”: “json”
}
r = s.get(url, params=params)
r.raise_for_status()
return sum(item[‘Quantity’] for item in r.json()[‘d’][‘results’])
stock_A = plant_stock(PLANT_A)
receipts_A = plant_receipts(PLANT_A)
shortage = max(0, demand_qty – stock_A – receipts_A)
print(“Shortage:”, shortage)
if shortage == 0:
print(“No action needed.”)
exit()
# ———- 3. Surplus stock ———-
surplus = {}
for p in PLANTS_SURPLUS:
surplus[p] = plant_stock(p)
print(“Surplus:”, surplus)
# ———- 4. ATP check ———-
def atp_ok(plant, qty, req_date):
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_ATP_CHECK_SRV/CheckAvailability”)
body = {
“Material”: MATERIAL,
“Plant”: plant,
“DemandQuantity”: str(qty),
“RequiredDate”: req_date.isoformat()
}
r = s.post(url, json=body)
r.raise_for_status()
return r.json()[‘d’][‘ConfirmedQuantity’] == str(qty)
best_internal = None
needed = shortage
for plant, qty in surplus.items():
take = min(qty, needed)
req = datetime.utcnow().date() + timedelta(days=TRANSPORT_DAYS)
if atp_ok(plant, take, req):
best_internal = (plant, take)
break
if best_internal:
plant, qty = best_internal
print(f”Best internal: {qty} from {plant}”)
# ———- 5. Create STO ———-
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_STOCK_TRANSPORT_ORDER_SRV/A_StockTransportOrder”)
body = {
“SupplyingPlant”: plant,
“ReceivingPlant”: PLANT_A,
“Material”: MATERIAL,
“OrderQuantity”: str(qty),
“DeliveryDate”: req.isoformat()
}
r = s.post(url, json=body)
r.raise_for_status()
sto = r.json()[‘d’][‘StockTransportOrder’]
print(“STO created:”, sto)
shortage -= qty
# ———- 6. Ariba – best external price ———-
if shortage > 0:
client = BackendApplicationClient(client_id=os.getenv(‘ARIBA_CLIENT_ID’))
oauth = OAuth2Session(client=client)
token = oauth.fetch_token(
token_url=os.getenv(‘ARIBA_TOKEN_URL’),
client_id=os.getenv(‘ARIBA_CLIENT_ID’),
client_secret=os.getenv(‘ARIBA_CLIENT_SECRET’)
)
url = “https://api.ariba.com/v2/suppliers/catalog”
params = {
“material”: MATERIAL,
“qty”: shortage,
“currency”: “EUR”
}
r = oauth.get(url, params=params)
r.raise_for_status()
best = min(r.json()[‘offers’], key=lambda x: float(x[‘price’]))
print(“Best supplier:”, best[‘supplier’], best[‘price’])
# ———- 7. Create PR ———-
url = (f”{os.getenv(‘S4_HOST’)}/sap/opu/odata/sap/”
f”API_PURCHASEREQ_PROCESS_SRV/A_PurchaseRequisitionHeader”)
body = {
“Material”: MATERIAL,
“Plant”: PLANT_A,
“Quantity”: str(shortage),
“DeliveryDate”: (datetime.utcnow().date()
+ timedelta(days=int(best[‘leadtime’]))).isoformat(),
“SupplierHint”: best[‘supplier’]
}
r = s.post(url, json=body)
r.raise_for_status()
pr = r.json()[‘d’][‘PurchaseRequisition’]
print(“PR created:”, pr)
print(“Flow finished.”) Read More Technology Blog Posts by SAP articles
#SAP
#SAPTechnologyblog