---
name: sap-mm-procurement
description: Directly call SAP S/4HANA MM OData services with curl for purchase requisitions, purchase orders, and goods receipts, with LINE-based approval for PR and PO creation.
requirements:
  commands:
    - bash
    - curl
  environment:
    - SAP_ODATA_BASE_URL
    - SAP_USER
    - SAP_PASSWORD
    - SAP_APPROVAL_MATRIX_FILE
---

# 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:

```bash
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:

```text
${SAP_ODATA_BASE_URL}/${SERVICE_NAME}
```

Entity URL pattern:

```text
${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:

```text
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:

```json
{
  "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:

1. Determine document type: `PR` or `PO`.
2. Determine estimated amount.
3. Pick the first rule whose `max_amount` is `null` or amount is less than or equal to `max_amount`.
4. Starting from the requester's `manager_line_user_id`, walk up the manager chain until finding an employee whose `job_level` is greater than or equal to `min_approver_job_level`.
5. If requester has `can_self_approve: true` and 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:

```text
${SAP_APPROVAL_STATE_DIR:-.sap-mm-approvals}
```

Use this layout:

```text
.sap-mm-approvals/
  pending/
  approved/
  rejected/
```

File name:

```text
<APPROVAL_ID>.json
```

Approval ID format:

```text
PR-YYYYMMDD-HHMMSS-xxxx
PO-YYYYMMDD-HHMMSS-xxxx
```

State file fields:

```json
{
  "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:

```text
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:

```text
已送出主管審核，尚未建立 SAP 單據。
審核編號：PR-20260612-203012-ab12
主管核准後才會送出 SAP。
```

### Handling Approver Replies

When a LINE message matches one of these formats:

```text
核准 <APPROVAL_ID>
approve <APPROVAL_ID>
駁回 <APPROVAL_ID> <reason>
reject <APPROVAL_ID> <reason>
```

Then:

1. Load `.sap-mm-approvals/pending/<APPROVAL_ID>.json`.
2. Verify the sender LINE user ID equals `approver_line_user_id`.
3. If sender is not authorized, reply "你不是此單據的指定審核主管" and stop.
4. For rejection, move the state file to `rejected/`, record reason and rejected time, notify requester.
5. For approval, POST the stored payload to SAP using the service and entity set in the state file.
6. On SAP success, move the state file to `approved/`, record SAP response summary, notify requester and approver.
7. 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:

```bash
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:

```bash
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:

```bash
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:

```bash
service_name="API_PURCHASEREQ_PROCESS_SRV"
entity_set="A_PurchaseRequisitionHeader"
```

Required user fields:

```json
{
  "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:

```json
{
  "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:

- `PurReqnDescription` and `PurchaseRequisitionItemText` should be 40 characters or less.
- `Supplier` is optional; omit it if the user did not provide a supplier and SAP source determination should decide.
- Set `PurReqnDoOnlyValidation` to `true` only when the user asks SAP validation-only behavior.

## Query Latest PR Items

Service/resource:

```bash
service_name="API_PURCHASEREQ_PROCESS_SRV"
resource="A_PurchaseRequisitionItem"
```

Curl:

```bash
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:

```bash
service_name="API_PURCHASEORDER_PROCESS_SRV"
entity_set="A_PurchaseOrder"
```

Required user fields:

```json
{
  "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:

```json
{
  "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:

```bash
service_name="API_PURCHASEORDER_PROCESS_SRV"
resource="A_PurchaseOrderItem(PurchaseOrder='4500004200',PurchaseOrderItem='00010')"
```

Curl:

```bash
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:

```bash
service_name="API_MATERIAL_DOCUMENT_SRV"
entity_set="A_MaterialDocumentHeader"
```

Required user fields:

```json
{
  "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:

```json
{
  "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:

- `GoodsMovementType` is `101` for PO goods receipt.
- `GoodsMovementRefDocType` must be `B` for purchase order reference.
- `GoodsMovementCode` is `01`.
- `PostingDate` must 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.
