sap-wm-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +161 -0
- package/lib/s4hClient.js +68 -0
- package/package.json +47 -0
- package/tools/binStatus.js +35 -0
- package/tools/binUtilization.js +48 -0
- package/tools/confirmTransferOrder.js +18 -0
- package/tools/confirmTransferOrderSU.js +23 -0
- package/tools/createTransferOrder.js +46 -0
- package/tools/emptyBins.js +30 -0
- package/tools/stockByMaterial.js +33 -0
package/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import 'dotenv/config';
|
|
6
|
+
|
|
7
|
+
import { getBinStatus } from './tools/binStatus.js';
|
|
8
|
+
import { getStockForMaterial } from './tools/stockByMaterial.js';
|
|
9
|
+
import { findEmptyBins } from './tools/emptyBins.js';
|
|
10
|
+
import { getBinUtilization } from './tools/binUtilization.js';
|
|
11
|
+
import { createTransferOrder } from './tools/createTransferOrder.js';
|
|
12
|
+
import { confirmTransferOrder } from './tools/confirmTransferOrder.js';
|
|
13
|
+
import { confirmTransferOrderSU } from './tools/confirmTransferOrderSU.js';
|
|
14
|
+
|
|
15
|
+
const server = new McpServer({ name: 'sap-wm-mcp', version: '0.1.0' });
|
|
16
|
+
|
|
17
|
+
// Tool 1 — get_bin_status
|
|
18
|
+
server.tool(
|
|
19
|
+
'get_bin_status',
|
|
20
|
+
'Query classic WM storage bins from S/4HANA by warehouse, storage type, or specific bin — returns empty/blocked status and capacity',
|
|
21
|
+
{
|
|
22
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
23
|
+
storageType: z.string().optional().describe('Storage type e.g. 001'),
|
|
24
|
+
bin: z.string().optional().describe('Specific bin number e.g. 01-01-01'),
|
|
25
|
+
top: z.number().optional().default(20).describe('Max records to return')
|
|
26
|
+
},
|
|
27
|
+
async (params) => {
|
|
28
|
+
try {
|
|
29
|
+
const result = await getBinStatus(params);
|
|
30
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Tool 2 — get_stock_for_material
|
|
38
|
+
server.tool(
|
|
39
|
+
'get_stock_for_material',
|
|
40
|
+
'Get physical warehouse stock for a material in classic WM — shows which bins hold the material and how much',
|
|
41
|
+
{
|
|
42
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
43
|
+
material: z.string().optional().describe('Material number e.g. TG0001'),
|
|
44
|
+
storageType: z.string().optional().describe('Filter by storage type'),
|
|
45
|
+
top: z.number().optional().default(20).describe('Max records to return')
|
|
46
|
+
},
|
|
47
|
+
async (params) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await getStockForMaterial(params);
|
|
50
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Tool 3 — find_empty_bins
|
|
58
|
+
server.tool(
|
|
59
|
+
'find_empty_bins',
|
|
60
|
+
'Find all empty storage bins in a classic WM warehouse, optionally filtered by storage type',
|
|
61
|
+
{
|
|
62
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
63
|
+
storageType: z.string().optional().describe('Storage type to filter e.g. 001'),
|
|
64
|
+
top: z.number().optional().default(50).describe('Max records to return')
|
|
65
|
+
},
|
|
66
|
+
async (params) => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await findEmptyBins(params);
|
|
69
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Tool 4 — get_bin_utilization
|
|
77
|
+
server.tool(
|
|
78
|
+
'get_bin_utilization',
|
|
79
|
+
'Get warehouse bin utilization stats for classic WM — occupied vs empty vs blocked, grouped by storage type',
|
|
80
|
+
{
|
|
81
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
82
|
+
storageType: z.string().optional().describe('Filter by storage type'),
|
|
83
|
+
top: z.number().optional().default(100).describe('Max bins to analyze')
|
|
84
|
+
},
|
|
85
|
+
async (params) => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await getBinUtilization(params);
|
|
88
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Tool 5 — create_transfer_order
|
|
96
|
+
server.tool(
|
|
97
|
+
'create_transfer_order',
|
|
98
|
+
'Create a classic WM Transfer Order in S/4HANA — moves stock from source bin to destination bin via L_TO_CREATE_SINGLE',
|
|
99
|
+
{
|
|
100
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
101
|
+
movementType: z.string().describe('WM movement type e.g. 999'),
|
|
102
|
+
material: z.string().describe('Material number e.g. TG0001'),
|
|
103
|
+
plant: z.string().describe('Plant e.g. 1710'),
|
|
104
|
+
quantity: z.number().describe('Quantity to move'),
|
|
105
|
+
unitOfMeasure: z.string().describe('Unit of measure e.g. ST, KG'),
|
|
106
|
+
sourceType: z.string().optional().default('').describe('Source storage type e.g. 001'),
|
|
107
|
+
sourceBin: z.string().optional().default('').describe('Source bin e.g. 01-02-01'),
|
|
108
|
+
sourceStorageUnit: z.string().optional().default('').describe('Source storage unit (LENUM) — required for SU-managed types e.g. 00000000001000000017'),
|
|
109
|
+
destType: z.string().describe('Destination storage type e.g. 001'),
|
|
110
|
+
destBin: z.string().describe('Destination bin e.g. 01-06-03'),
|
|
111
|
+
destStorageUnit: z.string().optional().default('').describe('Destination storage unit (LENUM) — for SU-managed types, same as source SU when moving full SU')
|
|
112
|
+
},
|
|
113
|
+
async (params) => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await createTransferOrder(params);
|
|
116
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Tool 6 — confirm_transfer_order
|
|
124
|
+
server.tool(
|
|
125
|
+
'confirm_transfer_order',
|
|
126
|
+
'Confirm a classic WM Transfer Order in S/4HANA — marks the TO as executed via L_TO_CONFIRM',
|
|
127
|
+
{
|
|
128
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
129
|
+
transferOrderNumber: z.string().describe('Transfer order number e.g. 0000000123')
|
|
130
|
+
},
|
|
131
|
+
async (params) => {
|
|
132
|
+
try {
|
|
133
|
+
const result = await confirmTransferOrder(params);
|
|
134
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Tool 7 — confirm_transfer_order_su
|
|
142
|
+
server.tool(
|
|
143
|
+
'confirm_transfer_order_su',
|
|
144
|
+
'Confirm all open transfer orders on a classic WM storage unit — uses L_TO_CONFIRM_SU to confirm by SU number instead of individual TO number',
|
|
145
|
+
{
|
|
146
|
+
warehouse: z.string().describe('Warehouse number e.g. 102'),
|
|
147
|
+
storageUnit: z.string().describe('Storage unit number (LENUM) e.g. 000000001234567890')
|
|
148
|
+
},
|
|
149
|
+
async (params) => {
|
|
150
|
+
try {
|
|
151
|
+
const result = await confirmTransferOrderSU(params);
|
|
152
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const transport = new StdioServerTransport();
|
|
160
|
+
await server.connect(transport);
|
|
161
|
+
console.error('SAP WM MCP Server running (stdio)...');
|
package/lib/s4hClient.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import 'dotenv/config';
|
|
4
|
+
|
|
5
|
+
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
6
|
+
const BASE_URL = process.env.SAP_URL;
|
|
7
|
+
const AUTH = Buffer.from(`${process.env.SAP_USER}:${process.env.SAP_PASSWORD}`).toString('base64');
|
|
8
|
+
const CLIENT = process.env.SAP_CLIENT;
|
|
9
|
+
|
|
10
|
+
export async function s4hGet(path) {
|
|
11
|
+
const url = `${BASE_URL}${path}`;
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
headers: {
|
|
14
|
+
'Authorization': `Basic ${AUTH}`,
|
|
15
|
+
'Accept': 'application/json',
|
|
16
|
+
'sap-client': CLIENT
|
|
17
|
+
},
|
|
18
|
+
agent
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`S4H OData error ${response.status}: ${await response.text()}`);
|
|
22
|
+
}
|
|
23
|
+
return response.json();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function s4hPost(path, body) {
|
|
27
|
+
const BASE_PATH = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001/`;
|
|
28
|
+
|
|
29
|
+
// Fetch CSRF token
|
|
30
|
+
const tokenRes = await fetch(`${BASE_URL}${BASE_PATH}`, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
'Authorization': `Basic ${AUTH}`,
|
|
34
|
+
'x-csrf-token': 'fetch',
|
|
35
|
+
'sap-client': CLIENT
|
|
36
|
+
},
|
|
37
|
+
agent
|
|
38
|
+
});
|
|
39
|
+
const csrfToken = tokenRes.headers.get('x-csrf-token');
|
|
40
|
+
if (!csrfToken) throw new Error('CSRF token fetch failed — no token in response headers');
|
|
41
|
+
|
|
42
|
+
// Forward session cookie so the token remains valid for this POST
|
|
43
|
+
const cookies = tokenRes.headers.raw?.()['set-cookie'] ?? tokenRes.headers.get('set-cookie');
|
|
44
|
+
const cookieHeader = Array.isArray(cookies)
|
|
45
|
+
? cookies.map(c => c.split(';')[0]).join('; ')
|
|
46
|
+
: (cookies ?? '').split(',').map(c => c.trim().split(';')[0]).join('; ');
|
|
47
|
+
|
|
48
|
+
const url = `${BASE_URL}${path}`;
|
|
49
|
+
const response = await fetch(url, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Authorization': `Basic ${AUTH}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'Accept': 'application/json',
|
|
55
|
+
'x-csrf-token': csrfToken,
|
|
56
|
+
'sap-client': CLIENT,
|
|
57
|
+
...(cookieHeader ? { 'Cookie': cookieHeader } : {})
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
agent
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`S4H OData POST error ${response.status}: ${await response.text()}`);
|
|
64
|
+
}
|
|
65
|
+
// 204 No Content is valid for some actions
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
return text ? JSON.parse(text) : {};
|
|
68
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sap-wm-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for SAP Classic Warehouse Management — connects AI agents to S/4HANA WM via a custom RAP OData V4 service. For systems where EWM is not active.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sap-wm-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"lib/",
|
|
13
|
+
"tools/"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node index.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"sap",
|
|
20
|
+
"wm",
|
|
21
|
+
"warehouse-management",
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"s4hana",
|
|
25
|
+
"warehouse",
|
|
26
|
+
"odata",
|
|
27
|
+
"rap",
|
|
28
|
+
"claude",
|
|
29
|
+
"ai-agents"
|
|
30
|
+
],
|
|
31
|
+
"author": "Noman Mohamed Hanif <noman@relacon.de> (https://github.com/CodeOfHANA)",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/CodeOfHANA/sap-wm-mcp.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/CodeOfHANA/sap-wm-mcp#readme",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
43
|
+
"dotenv": "^16.6.1",
|
|
44
|
+
"node-fetch": "^3.3.2",
|
|
45
|
+
"zod": "^3.25.76"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { s4hGet } from '../lib/s4hClient.js';
|
|
2
|
+
|
|
3
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001/WMStorageBin`;
|
|
4
|
+
|
|
5
|
+
export async function getBinStatus({ warehouse, storageType, bin, top = 20 }) {
|
|
6
|
+
const filters = [`WarehouseNumber eq '${warehouse}'`];
|
|
7
|
+
if (storageType) filters.push(`StorageType eq '${storageType}'`);
|
|
8
|
+
if (bin) filters.push(`StorageBin eq '${bin}'`);
|
|
9
|
+
|
|
10
|
+
const path = `${BASE}?$filter=${encodeURIComponent(filters.join(' and '))}&$top=${top}`;
|
|
11
|
+
const data = await s4hGet(path);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
count: data.value.length,
|
|
15
|
+
warehouse,
|
|
16
|
+
bins: data.value.map(b => ({
|
|
17
|
+
bin: b.StorageBin,
|
|
18
|
+
storageType: b.StorageType,
|
|
19
|
+
storageSection: b.StorageSection,
|
|
20
|
+
binType: b.StorageBinType,
|
|
21
|
+
empty: b.IsEmpty,
|
|
22
|
+
full: b.IsFull,
|
|
23
|
+
blockedPutaway: b.PutawayBlock,
|
|
24
|
+
blockedRemoval: b.RemovalBlock,
|
|
25
|
+
quants: b.NumberOfQuants,
|
|
26
|
+
maxWeight: b.MaximumWeight,
|
|
27
|
+
occupiedWeight: b.OccupiedWeight,
|
|
28
|
+
weightUnit: b.WeightUnit,
|
|
29
|
+
totalCapacity: b.TotalCapacity,
|
|
30
|
+
remainingCapacity: b.RemainingCapacity,
|
|
31
|
+
lastMovement: b.LastMovementDate,
|
|
32
|
+
dynamic: b.IsDynamicBin
|
|
33
|
+
}))
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { s4hGet } from '../lib/s4hClient.js';
|
|
2
|
+
|
|
3
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001/WMStorageBin`;
|
|
4
|
+
|
|
5
|
+
export async function getBinUtilization({ warehouse, storageType, top = 100 }) {
|
|
6
|
+
const filters = [`WarehouseNumber eq '${warehouse}'`];
|
|
7
|
+
if (storageType) filters.push(`StorageType eq '${storageType}'`);
|
|
8
|
+
|
|
9
|
+
const path = `${BASE}?$filter=${encodeURIComponent(filters.join(' and '))}&$top=${top}`;
|
|
10
|
+
const data = await s4hGet(path);
|
|
11
|
+
|
|
12
|
+
const bins = data.value;
|
|
13
|
+
const total = bins.length;
|
|
14
|
+
const empty = bins.filter(b => b.IsEmpty).length;
|
|
15
|
+
const full = bins.filter(b => b.IsFull).length;
|
|
16
|
+
const occupied = total - empty;
|
|
17
|
+
const blockedPutaway = bins.filter(b => b.PutawayBlock).length;
|
|
18
|
+
const blockedRemoval = bins.filter(b => b.RemovalBlock).length;
|
|
19
|
+
|
|
20
|
+
// Group by storage type
|
|
21
|
+
const byType = {};
|
|
22
|
+
for (const b of bins) {
|
|
23
|
+
const t = b.StorageType;
|
|
24
|
+
if (!byType[t]) byType[t] = { total: 0, empty: 0, occupied: 0 };
|
|
25
|
+
byType[t].total++;
|
|
26
|
+
if (b.IsEmpty) byType[t].empty++;
|
|
27
|
+
else byType[t].occupied++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
warehouse,
|
|
32
|
+
storageType: storageType ?? 'all',
|
|
33
|
+
summary: {
|
|
34
|
+
totalBins: total,
|
|
35
|
+
emptyBins: empty,
|
|
36
|
+
occupiedBins: occupied,
|
|
37
|
+
fullBins: full,
|
|
38
|
+
blockedForPutaway: blockedPutaway,
|
|
39
|
+
blockedForRemoval: blockedRemoval,
|
|
40
|
+
utilizationPct: total > 0 ? Math.round((occupied / total) * 100) : 0
|
|
41
|
+
},
|
|
42
|
+
byStorageType: Object.entries(byType).map(([type, stats]) => ({
|
|
43
|
+
storageType: type,
|
|
44
|
+
...stats,
|
|
45
|
+
utilizationPct: stats.total > 0 ? Math.round((stats.occupied / stats.total) * 100) : 0
|
|
46
|
+
}))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { s4hPost } from '../lib/s4hClient.js';
|
|
2
|
+
|
|
3
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001`;
|
|
4
|
+
const NS = `com.sap.gateway.srvd.zsd_wmmcpservice.v0001`;
|
|
5
|
+
|
|
6
|
+
export async function confirmTransferOrder({ warehouse, transferOrderNumber }) {
|
|
7
|
+
// Instance action — key goes in the URL path, not the body
|
|
8
|
+
const path = `${BASE}/WMTransferOrder(WarehouseNumber='${encodeURIComponent(warehouse)}',TransferOrderNumber='${encodeURIComponent(transferOrderNumber)}')/${NS}.ConfirmTransferOrder`;
|
|
9
|
+
|
|
10
|
+
const data = await s4hPost(path, {});
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
success: true,
|
|
14
|
+
warehouse,
|
|
15
|
+
transferOrderNumber,
|
|
16
|
+
raw: data
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { s4hPost } from '../lib/s4hClient.js';
|
|
2
|
+
|
|
3
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001`;
|
|
4
|
+
const NS = `com.sap.gateway.srvd.zsd_wmmcpservice.v0001`;
|
|
5
|
+
|
|
6
|
+
export async function confirmTransferOrderSU({ warehouse, storageUnit }) {
|
|
7
|
+
// Static action — called on the entity set (collection-bound)
|
|
8
|
+
const path = `${BASE}/WMTransferOrder/${NS}.ConfirmTransferOrderSU`;
|
|
9
|
+
|
|
10
|
+
const body = {
|
|
11
|
+
StorageUnit: storageUnit
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const data = await s4hPost(path, body);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
success: true,
|
|
18
|
+
warehouse,
|
|
19
|
+
storageUnit,
|
|
20
|
+
transferOrderNumber: data?.value?.[0]?.TransferOrderNumber ?? data?.TransferOrderNumber ?? null,
|
|
21
|
+
raw: data
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { s4hPost, s4hGet } from '../lib/s4hClient.js';
|
|
2
|
+
|
|
3
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001`;
|
|
4
|
+
const NS = `com.sap.gateway.srvd.zsd_wmmcpservice.v0001`;
|
|
5
|
+
|
|
6
|
+
export async function createTransferOrder({
|
|
7
|
+
warehouse, movementType, material, plant,
|
|
8
|
+
quantity, unitOfMeasure,
|
|
9
|
+
sourceType = '', sourceBin = '', sourceStorageUnit = '',
|
|
10
|
+
destType, destBin, destStorageUnit = ''
|
|
11
|
+
}) {
|
|
12
|
+
const path = `${BASE}/WMTransferOrder/${NS}.CreateTransferOrder`;
|
|
13
|
+
|
|
14
|
+
const body = {
|
|
15
|
+
WarehouseNumber: warehouse,
|
|
16
|
+
MovementType: movementType,
|
|
17
|
+
Material: material,
|
|
18
|
+
Plant: plant,
|
|
19
|
+
Quantity: quantity,
|
|
20
|
+
UnitOfMeasure: unitOfMeasure,
|
|
21
|
+
SourceStorageType: sourceType,
|
|
22
|
+
SourceBin: sourceBin,
|
|
23
|
+
SourceStorageUnit: sourceStorageUnit,
|
|
24
|
+
DestStorageType: destType,
|
|
25
|
+
DestBin: destBin,
|
|
26
|
+
DestStorageUnit: destStorageUnit
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const data = await s4hPost(path, body);
|
|
30
|
+
|
|
31
|
+
// RAP static action result does not carry entity fields back — query the latest TO
|
|
32
|
+
let transferOrderNumber = data?.value?.[0]?.TransferOrderNumber ?? data?.TransferOrderNumber ?? null;
|
|
33
|
+
if (!transferOrderNumber) {
|
|
34
|
+
const latest = await s4hGet(
|
|
35
|
+
`${BASE}/WMTransferOrder?$orderby=TransferOrderNumber%20desc&$top=1`
|
|
36
|
+
);
|
|
37
|
+
transferOrderNumber = latest?.value?.[0]?.TransferOrderNumber ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
warehouse,
|
|
43
|
+
transferOrderNumber,
|
|
44
|
+
raw: data
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { s4hGet } from '../lib/s4hClient.js';
|
|
2
|
+
|
|
3
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001/WMStorageBin`;
|
|
4
|
+
|
|
5
|
+
export async function findEmptyBins({ warehouse, storageType, top = 50 }) {
|
|
6
|
+
const filters = [
|
|
7
|
+
`WarehouseNumber eq '${warehouse}'`,
|
|
8
|
+
`IsEmpty eq true`
|
|
9
|
+
];
|
|
10
|
+
if (storageType) filters.push(`StorageType eq '${storageType}'`);
|
|
11
|
+
|
|
12
|
+
const path = `${BASE}?$filter=${encodeURIComponent(filters.join(' and '))}&$top=${top}`;
|
|
13
|
+
const data = await s4hGet(path);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
count: data.value.length,
|
|
17
|
+
warehouse,
|
|
18
|
+
storageType: storageType ?? 'all',
|
|
19
|
+
emptyBins: data.value.map(b => ({
|
|
20
|
+
bin: b.StorageBin,
|
|
21
|
+
storageType: b.StorageType,
|
|
22
|
+
storageSection: b.StorageSection,
|
|
23
|
+
binType: b.StorageBinType,
|
|
24
|
+
blockedPutaway: b.PutawayBlock,
|
|
25
|
+
blockedRemoval: b.RemovalBlock,
|
|
26
|
+
remainingCapacity: b.RemainingCapacity,
|
|
27
|
+
lastMovement: b.LastMovementDate
|
|
28
|
+
}))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { s4hGet } from '../lib/s4hClient.js';
|
|
2
|
+
|
|
3
|
+
const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001/WMWarehouseStock`;
|
|
4
|
+
|
|
5
|
+
export async function getStockForMaterial({ warehouse, material, storageType, top = 20 }) {
|
|
6
|
+
const filters = [`WarehouseNumber eq '${warehouse}'`];
|
|
7
|
+
if (material) filters.push(`Material eq '${material}'`);
|
|
8
|
+
if (storageType) filters.push(`StorageType eq '${storageType}'`);
|
|
9
|
+
|
|
10
|
+
const path = `${BASE}?$filter=${encodeURIComponent(filters.join(' and '))}&$top=${top}`;
|
|
11
|
+
const data = await s4hGet(path);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
count: data.value.length,
|
|
15
|
+
warehouse,
|
|
16
|
+
material: material ?? 'all',
|
|
17
|
+
stock: data.value.map(q => ({
|
|
18
|
+
bin: q.StorageBin,
|
|
19
|
+
storageType: q.StorageType,
|
|
20
|
+
quantNumber: q.QuantNumber,
|
|
21
|
+
material: q.Material,
|
|
22
|
+
plant: q.Plant,
|
|
23
|
+
batch: q.Batch,
|
|
24
|
+
totalStock: q.TotalStock,
|
|
25
|
+
availableStock: q.AvailableStock,
|
|
26
|
+
pickQuantity: q.PickQuantity,
|
|
27
|
+
transferQuantity: q.TransferQuantity,
|
|
28
|
+
uom: q.UnitOfMeasure,
|
|
29
|
+
stockCategory: q.StockCategory,
|
|
30
|
+
lastMovement: q.LastMovementDate
|
|
31
|
+
}))
|
|
32
|
+
};
|
|
33
|
+
}
|