Imported from wusheyi/SAP_MM_SKILLS (
skills/sap-mm-procurement/SKILL.md). Install upstream withnpx skills add wusheyi/SAP_MM_SKILLS --skill sap-mm-procurement. Copyright stays with the author.
SAP MM Procurement Skill
Use this skill when the user wants OpenClaw to call SAP S/4HANA MM OData APIs directly. Do not call a local API server. Do not run the demo Python files for normal skill operations.
When requests arrive from LINE, OpenClaw is responsible for LINE messaging. This skill defines the SAP and approval workflow; it does not start or manage a LINE webhook.
The user must configure the SAP S/4 OData base endpoint first:
export SAP_ODATA_BASE_URL="https://ncus4ap.mgt.ncu.edu.tw:44320/sap/opu/odata/sap"
export SAP_USER="..."
export SAP_PASSWORD="..."
export SAP_LANGUAGE="${SAP_LANGUAGE:-ZF}"
export SAP_VERIFY_SSL="${SAP_VERIFY_SSL:-false}"
export SAP_APPROVAL_MATRIX_FILE="${SAP_APPROVAL_MATRIX_FILE:-skills/sap-mm-procurement/approval_matrix.local.json}"
export SAP_APPROVAL_STATE_DIR="${SAP_APPROVAL_STATE_DIR:-.sap-mm-approvals}"
SAP_ODATA_BASE_URL is the common SAP service root without a service name. Build full service URLs by appending the service name.
Endpoint Map
| Business task | OData service | Entity set / resource | Method |
|---|---|---|---|
| Create purchase requisition (PR) | API_PURCHASEREQ_PROCESS_SRV |
A_PurchaseRequisitionHeader |
POST |
| Query latest purchase requisition items | API_PURCHASEREQ_PROCESS_SRV |
A_PurchaseRequisitionItem |
GET |
| Create purchase order (PO) | API_PURCHASEORDER_PROCESS_SRV |
A_PurchaseOrder |
POST |
| Query one purchase order item | API_PURCHASEORDER_PROCESS_SRV |
A_PurchaseOrderItem(PurchaseOrder='...',PurchaseOrderItem='00010') |
GET |
| Create goods receipt (GR) | API_MATERIAL_DOCUMENT_SRV |
A_MaterialDocumentHeader |
POST |
Service URL pattern:
${SAP_ODATA_BASE_URL}/${SERVICE_NAME}
Entity URL pattern:
${SAP_ODATA_BASE_URL}/${SERVICE_NAME}/${ENTITY_SET_OR_RESOURCE}
Safety Rule
Default to validation, payload construction, or GET queries. For PR and PO creation, never POST immediately from an operator request. Build the payload, create a pending approval request, send it to the required supervisor over LINE, and POST to SAP only after an authorized approver explicitly approves.
Goods receipt can follow the direct POST flow only when the user explicitly asks to post it. If the customer later requires GR approval, reuse the same approval workflow with document type GR.
Never print SAP_PASSWORD, API keys, cookies, or CSRF tokens in the user-facing answer.
LINE Approval Workflow
Use this workflow for:
- Creating purchase requisitions (
PR). - Creating purchase orders (
PO).
Do not use it for read-only GET queries.
Approval Matrix
Load the approval matrix from SAP_APPROVAL_MATRIX_FILE. If it is not set, look for:
skills/sap-mm-procurement/approval_matrix.local.json
Use skills/sap-mm-procurement/approval_matrix.example.json only as a template. Never use placeholder LINE IDs such as LINE_USER_ID_OPERATOR for live approval.
Matrix schema:
{
"employees": {
"LINE_USER_ID": {
"name": "採購操作員",
"job_level": 1,
"manager_line_user_id": "LINE_MANAGER_USER_ID",
"can_self_approve": false
}
},
"rules": {
"PR": [
{
"max_amount": 50000,
"min_approver_job_level": 2
}
],
"PO": [
{
"max_amount": 100000,
"min_approver_job_level": 2
}
]
}
}
Amount means:
- PR:
RequestedQuantity * PurchaseRequisitionPrice. - PO:
OrderQuantity * NetPriceAmount.
If price is missing, use 0 for routing and state in the approval message that the amount could not be fully calculated.
Requester Identity
When the request comes from LINE, use OpenClaw's LINE message metadata as the requester identity:
- Prefer LINE user ID.
- Also record display name when available.
If the requester LINE user ID is not present in the approval matrix, do not POST to SAP. Ask an admin to add that user to approval_matrix.local.json.
Approver Routing
To choose the approver:
- Determine document type:
PRorPO. - Determine estimated amount.
- Pick the first rule whose
max_amountisnullor amount is less than or equal tomax_amount. - Starting from the requester's
manager_line_user_id, walk up the manager chain until finding an employee whosejob_levelis greater than or equal tomin_approver_job_level. - If requester has
can_self_approve: trueand meets the required level, allow self-approval only when the matrix explicitly says so.
If no approver is found, do not POST. Reply with the missing routing problem.
Pending Approval State
Persist every pending request before messaging the approver.
Default state directory:
${SAP_APPROVAL_STATE_DIR:-.sap-mm-approvals}
Use this layout:
.sap-mm-approvals/
pending/
approved/
rejected/
File name:
<APPROVAL_ID>.json
Approval ID format:
PR-YYYYMMDD-HHMMSS-xxxx
PO-YYYYMMDD-HHMMSS-xxxx
State file fields:
{
"approval_id": "PR-20260612-203012-ab12",
"status": "pending",
"document_type": "PR",
"created_at": "2026-06-12T20:30:12+08:00",
"requester_line_user_id": "LINE_USER_ID_OPERATOR",
"requester_name": "採購操作員",
"approver_line_user_id": "LINE_USER_ID_SUPERVISOR",
"estimated_amount": "3000.00",
"currency": "TWD",
"summary": {
"material": "MAT_020",
"quantity": "3",
"unit": "EA",
"plant": "1710",
"supplier": "A100",
"delivery_or_posting_date": "2026-06-20"
},
"service_name": "API_PURCHASEREQ_PROCESS_SRV",
"entity_set": "A_PurchaseRequisitionHeader",
"payload": {}
}
Do not store SAP_PASSWORD, cookies, CSRF tokens, LINE access tokens, or API keys in approval state.
LINE Message To Approver
Send the approver a LINE message with:
- Approval ID.
- Requester name.
- Document type.
- Estimated amount and currency.
- Important fields: material, quantity, unit, plant, supplier, PR/PO number when applicable, date.
- Exact reply commands.
Approval message template:
SAP 採購審核
審核編號:PR-20260612-203012-ab12
申請人:採購操作員
類型:請購單 PR
物料:MAT_020
數量:3 EA
工廠:1710
供應商:A100
金額:約 3000.00 TWD
交貨日:2026-06-20
核准請回覆:
核准 PR-20260612-203012-ab12
駁回請回覆:
駁回 PR-20260612-203012-ab12 原因
Also reply to the requester:
已送出主管審核,尚未建立 SAP 單據。
審核編號:PR-20260612-203012-ab12
主管核准後才會送出 SAP。
Handling Approver Replies
When a LINE message matches one of these formats:
核准 <APPROVAL_ID>
approve <APPROVAL_ID>
駁回 <APPROVAL_ID> <reason>
reject <APPROVAL_ID> <reason>
Then:
- Load
.sap-mm-approvals/pending/<APPROVAL_ID>.json. - Verify the sender LINE user ID equals
approver_line_user_id. - If sender is not authorized, reply "你不是此單據的指定審核主管" and stop.
- For rejection, move the state file to
rejected/, record reason and rejected time, notify requester. - For approval, POST the stored payload to SAP using the service and entity set in the state file.
- On SAP success, move the state file to
approved/, record SAP response summary, notify requester and approver. - On SAP failure, keep the request pending or mark it failed in the state file, then notify the approver and requester with the SAP error summary.
After approval, use the stored payload exactly as approved. Do not silently change material, quantity, amount, supplier, or date. If changes are required, create a new approval request.
Common Curl Setup
Use this shell pattern for POST operations that require CSRF token and SAP cookies:
sap_curl_flags=(-sS)
if [[ "${SAP_VERIFY_SSL:-false}" != "true" ]]; then
sap_curl_flags+=(-k)
fi
cookie_jar="$(mktemp)"
header_file="$(mktemp)"
trap 'rm -f "$cookie_jar" "$header_file"' EXIT
service_url="${SAP_ODATA_BASE_URL}/${service_name}"
curl "${sap_curl_flags[@]}" \
-u "${SAP_USER}:${SAP_PASSWORD}" \
-c "$cookie_jar" \
-D "$header_file" \
-H "X-CSRF-Token: Fetch" \
-H "Accept: application/json" \
"${service_url}/?sap-language=${SAP_LANGUAGE:-ZF}" >/dev/null
csrf_token="$(
awk -F': ' 'tolower($1)=="x-csrf-token"{gsub(/\r/,"",$2); print $2; exit}' "$header_file"
)"
if [[ -z "$csrf_token" ]]; then
echo "Failed to fetch SAP CSRF token." >&2
exit 1
fi
Then POST JSON with:
curl "${sap_curl_flags[@]}" \
-u "${SAP_USER}:${SAP_PASSWORD}" \
-b "$cookie_jar" \
-c "$cookie_jar" \
-X POST "${service_url}/${entity_set}?sap-language=${SAP_LANGUAGE:-ZF}" \
-H "X-CSRF-Token: ${csrf_token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Accept-Language: zh-TW" \
-H "Content-Language: zh-TW" \
--data-binary @payload.json
Date Format
SAP OData V2 date fields in these payloads use /Date(milliseconds)/.
In WSL/Linux, convert YYYY-MM-DD to SAP date text:
delivery_ms="$(date -u -d "2026-06-20 00:00:00" +%s)000"
delivery_date="/Date(${delivery_ms})/"
Use the same format for PR DeliveryDate, GR DocumentDate, and GR PostingDate.
Create PR
Service:
service_name="API_PURCHASEREQ_PROCESS_SRV"
entity_set="A_PurchaseRequisitionHeader"
Required user fields:
{
"material": "MAT_020",
"quantity": "3",
"unit": "EA",
"plant": "1710",
"company_code": "1710",
"purchasing_organization": "1710",
"purchasing_group": "001",
"currency": "TWD",
"delivery_date": "2026-06-20"
}
Payload template:
{
"PurchaseRequisition": "",
"PurchaseRequisitionType": "NB",
"PurReqnDescription": "AI generated purchase requisition",
"SourceDetermination": false,
"PurReqnDoOnlyValidation": false,
"to_PurchaseReqnItem": {
"results": [
{
"PurchaseRequisition": "",
"PurchaseRequisitionItem": "10",
"PurchaseRequisitionItemText": "AI generated purchase requisition",
"AccountAssignmentCategory": "",
"Material": "MAT_020",
"RequestedQuantity": "3",
"BaseUnit": "EA",
"PurchaseRequisitionPrice": "1000.00",
"PurReqnPriceQuantity": "1",
"PurchasingOrganization": "1710",
"PurchasingGroup": "001",
"Plant": "1710",
"CompanyCode": "1710",
"PurReqnItemCurrency": "TWD",
"DeliveryDate": "/Date(1781913600000)/",
"to_PurchaseReqnAcctAssgmt": {
"results": []
},
"to_PurchaseReqnDeliveryAddress": {},
"to_PurchaseReqnItemText": {
"results": []
},
"Supplier": "A100"
}
]
}
}
Notes:
PurReqnDescriptionandPurchaseRequisitionItemTextshould be 40 characters or less.Supplieris optional; omit it if the user did not provide a supplier and SAP source determination should decide.- Set
PurReqnDoOnlyValidationtotrueonly when the user asks SAP validation-only behavior.
Query Latest PR Items
Service/resource:
service_name="API_PURCHASEREQ_PROCESS_SRV"
resource="A_PurchaseRequisitionItem"
Curl:
sap_curl_flags=(-sS)
if [[ "${SAP_VERIFY_SSL:-false}" != "true" ]]; then
sap_curl_flags+=(-k)
fi
curl "${sap_curl_flags[@]}" \
-u "${SAP_USER}:${SAP_PASSWORD}" \
--get "${SAP_ODATA_BASE_URL}/${service_name}/${resource}" \
--data-urlencode '$orderby=CreationDate desc, PurchaseRequisition desc, PurchaseRequisitionItem desc' \
--data-urlencode '$top=5' \
--data-urlencode '$format=json' \
--data-urlencode "sap-language=${SAP_LANGUAGE:-ZF}"
Create PO From PR
Service:
service_name="API_PURCHASEORDER_PROCESS_SRV"
entity_set="A_PurchaseOrder"
Required user fields:
{
"purchase_requisition": "10002140",
"purchase_requisition_item": "00010",
"material": "MAT_020",
"order_quantity": "3",
"plant": "1710",
"supplier": "A100",
"company_code": "1710",
"purchasing_organization": "1710",
"purchasing_group": "001",
"document_currency": "TWD",
"net_price_amount": "1000.00"
}
Payload template:
{
"CompanyCode": "1710",
"PurchaseOrderType": "NB",
"Supplier": "A100",
"PurchasingOrganization": "1710",
"PurchasingGroup": "001",
"to_PurchaseOrderItem": {
"results": [
{
"PurchaseOrderItem": "00010",
"Plant": "1710",
"Material": "MAT_020",
"OrderQuantity": "3",
"NetPriceAmount": "1000.00",
"DocumentCurrency": "TWD",
"PricingDateControl": "5",
"to_ScheduleLine": {
"results": [
{
"ScheduleLine": "0001",
"PurchaseRequisition": "10002140",
"PurchaseRequisitionItem": "00010",
"ScheduleLineOrderQuantity": "3"
}
]
}
}
]
}
}
Normalize item numbers before calling SAP:
- PO item
10->00010 - PR item
10->00010 - Schedule line
1->0001
Query PO Item
Service/resource:
service_name="API_PURCHASEORDER_PROCESS_SRV"
resource="A_PurchaseOrderItem(PurchaseOrder='4500004200',PurchaseOrderItem='00010')"
Curl:
sap_curl_flags=(-sS)
if [[ "${SAP_VERIFY_SSL:-false}" != "true" ]]; then
sap_curl_flags+=(-k)
fi
curl "${sap_curl_flags[@]}" \
-u "${SAP_USER}:${SAP_PASSWORD}" \
--get "${SAP_ODATA_BASE_URL}/${service_name}/${resource}" \
--data-urlencode '$format=json' \
--data-urlencode "sap-language=${SAP_LANGUAGE:-ZF}"
Create Goods Receipt
Service:
service_name="API_MATERIAL_DOCUMENT_SRV"
entity_set="A_MaterialDocumentHeader"
Required user fields:
{
"purchase_order": "4500004200",
"purchase_order_item": "00010",
"material": "MAT_020",
"plant": "1710",
"storage_location": "1710",
"quantity": "3",
"unit": "EA",
"posting_date": "2026-06-20",
"goods_recipient_name": "NKUSTAB00"
}
Payload template:
{
"DocumentDate": "/Date(1781913600000)/",
"PostingDate": "/Date(1781913600000)/",
"GoodsMovementCode": "01",
"to_MaterialDocumentItem": {
"results": [
{
"Material": "MAT_020",
"Plant": "1710",
"StorageLocation": "1710",
"GoodsMovementType": "101",
"GoodsMovementRefDocType": "B",
"EntryUnit": "EA",
"QuantityInEntryUnit": "3",
"PurchaseOrder": "4500004200",
"PurchaseOrderItem": "00010",
"GoodsRecipientName": "NKUSTAB00"
}
]
}
}
Important:
GoodsMovementTypeis101for PO goods receipt.GoodsMovementRefDocTypemust beBfor purchase order reference.GoodsMovementCodeis01.PostingDatemust be within an open SAP posting period.
Response Handling
For successful PR creation, report PurchaseRequisition and first item from to_PurchaseReqnItem.results.
For successful PO creation, report PurchaseOrder and first item from to_PurchaseOrderItem.results.
For successful GR creation, report MaterialDocument and MaterialDocumentYear.
If SAP returns an error, summarize the SAP error message, HTTP status, service name, and entity set. Do not expose credentials, cookies, or tokens.
