sap-wm-mcp 0.2.9 → 0.3.2

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/README.md CHANGED
@@ -13,7 +13,7 @@ Connect AI agents — Claude, Copilot, or any MCP-compatible client — directly
13
13
  >
14
14
  > This project ships a custom RAP OData V4 service that exposes classic WM operations as a proper API — and wraps it in an MCP server so AI agents can drive it. Large portions of the SAP install base are still on classic WM. This fills the gap.
15
15
 
16
- The **npm package** ships 22 tools covering core operations, analytics, shift management, anomaly detection, audit history, proactive replenishment, and interim zone reconciliation. This repository contains the 9 open-sourced tool implementations — the ABAP RAP service source and additional tool source are available separately (see [ABAP Service Installation](#abap-service-installation)).
16
+ The **npm package** ships 23 tools covering core operations, analytics, shift management, anomaly detection, audit history, proactive replenishment, interim zone reconciliation, and cycle count management. This repository contains the 9 open-sourced tool implementations — the ABAP RAP service source and additional tool source are available separately (see [ABAP Service Installation](#abap-service-installation)).
17
17
 
18
18
  ---
19
19
 
@@ -374,7 +374,7 @@ For write operations:
374
374
 
375
375
  ## Tools Reference
376
376
 
377
- The npm package ships **22 tools** across five capability areas. The 9 tools below are open-sourced in this repository. Analytics, shift management, anomaly detection, and operations tools are available in the published package.
377
+ The npm package ships **23 tools** across five capability areas. The 9 tools below are open-sourced in this repository. Analytics, shift management, anomaly detection, cycle count management, and operations tools are available in the published package.
378
378
 
379
379
  ---
380
380
 
@@ -478,6 +478,9 @@ Internally calls `L_TO_CREATE_SINGLE` via an RFC-enabled wrapper, isolated from
478
478
  | `destType` | string | ✅ | Destination storage type |
479
479
  | `destBin` | string | ✅ | Destination bin |
480
480
  | `destStorageUnit` | string | | Destination storage unit (LENUM) — for SU-managed destination types |
481
+ | `autoConfirm` | boolean | | When `true`, the TO is created and immediately confirmed in a single call — no separate `confirm_transfer_order` step needed. Maps to `I_SQUIT = 'X'` in `L_TO_CREATE_SINGLE`. Default: `false`. |
482
+
483
+ > **`autoConfirm`:** Use for internal replenishment or relocation moves where no physical scan or operator acknowledgement is required. Do not use for inbound putaway from GR zone or outbound picks where confirmation must happen on the warehouse floor.
481
484
 
482
485
  > **Note on SU-managed storage types:** Storage types with `LPTYP` set in `LAGP` (e.g. type `001`) require a `destStorageUnit` (LENUM) when used as the TO destination. Storage types with `LPTYP` blank (e.g. type `003`) do not.
483
486
 
@@ -538,6 +541,8 @@ The following tools are available in the published npm package and fully functio
538
541
  | `get_transfer_order_history` | Full TO history — creator, executor (resolved from LTAP.QNAME), and item detail; filterable by date range, status, movement type, material, `createdBy`, or `executedBy` |
539
542
  | `get_replenishment_needs` | Find forward-pick bins at or below a stock threshold — `defaultReplenishQty` param (default 50) used as fallback when no bin max qty is configured; flags bins with an open replenishment TO to avoid duplicate moves |
540
543
  | `get_interim_zone_anomalies` | Detect positive stock stranded in interim/staging zones (types 999, 998, 902) — surfaces same-day, overnight, and multi-day strandings with likely cause per zone; use `minDaysStranded` to filter noise during active shifts |
544
+ | `get_goods_issue_monitor` | Open outbound deliveries — delivery qty vs picked qty, GI status, overdue flag, and stock still in GI zone (type 999). Tracks picking progress and stalled shipments. |
545
+ | `create_cycle_count_document` | Create a WM inventory document (LI01N equivalent) for a specific bin. Activates the bin immediately by default (`LAGP.KZINV = 'IL'`). Use after `get_cycle_count_candidates` to trigger a count. |
541
546
 
542
547
  ---
543
548
 
@@ -563,11 +568,17 @@ The ABAP source is not included in this public repository. To obtain the ABAP pa
563
568
  | `ZR_WMCYCLECOUNTBIN` | CDS View | View over LAGP (cycle count indicators) |
564
569
  | `ZR_WMTRANSFERORDER` | BDEF | Behavior definition — defines actions |
565
570
  | `ZBP_R_WMTRANSFERORDER` | Class | RAP behavior implementation |
566
- | `ZWM_MFG` | Function Group | Contains RFC wrapper FM |
571
+ | `ZR_WMGOODSISSUEDELIVERY` | CDS View | View over LIKP + LIPS (outbound deliveries for GI monitor) |
572
+ | `ZR_WMTRANSFERORDER` | BDEF | Behavior definition — defines actions |
573
+ | `ZBP_R_WMTRANSFERORDER` | Class | RAP behavior implementation |
574
+ | `ZWM_MFG` | Function Group | Contains all RFC wrapper FMs |
567
575
  | `ZWM_TO_CREATE` | Function Module | RFC-enabled wrapper for `L_TO_CREATE_SINGLE` |
568
- | `ZA_WMCREATETOPARAM` | Structure | Parameter type for CreateTransferOrder action |
569
- | `ZA_WMCONFIRMTOSU` | Structure | Parameter type for ConfirmTransferOrderSU action |
570
- | `ZSD_WMMCPSERVICE` | Service Def | OData V4 service definition (7 entity sets) |
576
+ | `ZWM_TO_CANCEL` | Function Module | RFC-enabled wrapper for `L_TO_CANCEL` |
577
+ | `ZWM_INV_CREATE` | Function Module | RFC-enabled wrapper writes directly to LINK/LINP/LINV and locks bin in LAGP |
578
+ | `ZA_WMCREATETOPARAM` | Abstract Entity | Parameter type for CreateTransferOrder action |
579
+ | `ZA_WMCONFIRMTOSUPARAM` | Abstract Entity | Parameter type for ConfirmTransferOrderSU action |
580
+ | `ZA_WMCREATEINVDOCPARAM` | Abstract Entity | Parameter type for CreateInventoryDocument action |
581
+ | `ZSD_WMMCPSERVICE` | Service Def | OData V4 service definition (8 entity sets) |
571
582
  | `ZSB_WMMCPSERVICE_ODATA4_UI` | Service Binding | OData V4 UI binding |
572
583
 
573
584
  ### abapGit compatibility
@@ -613,7 +624,8 @@ Classic WM has no standard OData APIs. This project builds one using **ABAP REST
613
624
  | `CreateTransferOrder` | `L_TO_CREATE_SINGLE` | Called via RFC wrapper `ZWM_TO_CREATE` with `DESTINATION 'NONE'` |
614
625
  | `ConfirmTransferOrder` | `L_TO_CONFIRM` | Called directly — no COMMIT needed |
615
626
  | `ConfirmTransferOrderSU` | `L_TO_CONFIRM_SU` | Called directly — no COMMIT needed |
616
- | `CancelTransferOrder` | `L_TO_CANCEL` | Called directly no COMMIT needed |
627
+ | `CancelTransferOrder` | `L_TO_CANCEL` | Called via RFC wrapper `ZWM_TO_CANCEL` with `DESTINATION 'NONE'` |
628
+ | `CreateInventoryDocument` | Direct writes to LINK/LINP/LINV | Called via RFC wrapper `ZWM_INV_CREATE` with `DESTINATION 'NONE'` |
617
629
 
618
630
  ### Why the RFC wrapper?
619
631
 
@@ -680,7 +692,9 @@ sap-wm-mcp/
680
692
  │ ├── cancelTransferOrder.js ← cancel_transfer_order
681
693
  │ ├── transferOrderHistory.js ← get_transfer_order_history
682
694
  │ ├── replenishmentNeeds.js ← get_replenishment_needs
683
- └── interimZoneAnomalies.js ← get_interim_zone_anomalies
695
+ ├── interimZoneAnomalies.js ← get_interim_zone_anomalies
696
+ │ ├── goodsIssueMonitor.js ← get_goods_issue_monitor
697
+ │ └── createCycleCountDoc.js ← create_cycle_count_document
684
698
  ├── .env.example
685
699
  └── package.json
686
700
  ```
@@ -705,7 +719,7 @@ Running both side-by-side shows the contrast directly: same tools, same question
705
719
  | Phase | Status | Description |
706
720
  |---|---|---|
707
721
  | Phase 0 — RAP Service | ✅ Complete | Custom OData V4 service with 7 entity sets over classic WM tables |
708
- | Phase 1 — Local MCP | ✅ Complete | 21 tools working, security hardened, published to npm |
722
+ | Phase 1 — Local MCP | ✅ Complete | 23 tools working, security hardened, published to npm |
709
723
  | Phase 2 — BTP CF | 🔜 Planned | Deploy to SAP BTP Cloud Foundry with SSE transport + XSUAA + Cloud Connector |
710
724
  | Phase 3 — Joule Agent | 💡 Future | Native Joule Studio agent using the same RAP service |
711
725
 
package/index.js CHANGED
@@ -29,8 +29,10 @@ import { getTransferOrderHistory } from './tools/transferOrderHistory.js';
29
29
  import { cancelTransferOrder } from './tools/cancelTransferOrder.js';
30
30
  import { getReplenishmentNeeds } from './tools/replenishmentNeeds.js';
31
31
  import { getInterimZoneAnomalies } from './tools/interimZoneAnomalies.js';
32
+ import { getGoodsIssueMonitor } from './tools/goodsIssueMonitor.js';
33
+ import { createCycleCountDoc } from './tools/createCycleCountDoc.js';
32
34
 
33
- const server = new McpServer({ name: 'sap-wm-mcp', version: '0.2.9' });
35
+ const server = new McpServer({ name: 'sap-wm-mcp', version: '0.3.2' });
34
36
 
35
37
  // Tool 1 — get_bin_status
36
38
  server.tool(
@@ -40,7 +42,7 @@ server.tool(
40
42
  warehouse: z.string().describe('Warehouse number e.g. 102'),
41
43
  storageType: z.string().optional().describe('Storage type e.g. 001'),
42
44
  bin: z.string().optional().describe('Specific bin number e.g. 01-01-01'),
43
- top: z.number().optional().default(20).describe('Max records to return')
45
+ top: z.coerce.number().optional().default(20).describe('Max records to return')
44
46
  },
45
47
  async (params) => {
46
48
  try {
@@ -60,7 +62,7 @@ server.tool(
60
62
  warehouse: z.string().describe('Warehouse number e.g. 102'),
61
63
  material: z.string().optional().describe('Material number e.g. TG0001'),
62
64
  storageType: z.string().optional().describe('Filter by storage type'),
63
- top: z.number().optional().default(20).describe('Max records to return')
65
+ top: z.coerce.number().optional().default(20).describe('Max records to return')
64
66
  },
65
67
  async (params) => {
66
68
  try {
@@ -80,7 +82,7 @@ server.tool(
80
82
  warehouse: z.string().describe('Warehouse number e.g. 102'),
81
83
  storageType: z.string().optional().describe('Storage type to filter e.g. 001'),
82
84
  binType: z.string().optional().describe('Bin type to filter e.g. E1, E2 — use when destination bin must match a specific storage unit type'),
83
- top: z.number().optional().default(50).describe('Max records to return')
85
+ top: z.coerce.number().optional().default(50).describe('Max records to return')
84
86
  },
85
87
  async (params) => {
86
88
  try {
@@ -99,7 +101,7 @@ server.tool(
99
101
  {
100
102
  warehouse: z.string().describe('Warehouse number e.g. 102'),
101
103
  storageType: z.string().optional().describe('Filter by storage type'),
102
- top: z.number().optional().default(100).describe('Max bins to analyze')
104
+ top: z.coerce.number().optional().default(100).describe('Max bins to analyze')
103
105
  },
104
106
  async (params) => {
105
107
  try {
@@ -120,14 +122,15 @@ server.tool(
120
122
  movementType: z.string().describe('WM movement type e.g. 999'),
121
123
  material: z.string().describe('Material number e.g. TG0001'),
122
124
  plant: z.string().describe('Plant e.g. 1710'),
123
- quantity: z.number().describe('Quantity to move'),
125
+ quantity: z.coerce.number().describe('Quantity to move'),
124
126
  unitOfMeasure: z.string().optional().default('').describe('Unit of measure — leave empty to use material base UOM (recommended)'),
125
127
  sourceType: z.string().optional().default('').describe('Source storage type e.g. 001'),
126
128
  sourceBin: z.string().optional().default('').describe('Source bin e.g. 01-02-01'),
127
129
  sourceStorageUnit: z.string().optional().default('').describe('Source storage unit (LENUM) — required for SU-managed types e.g. 00000000001000000017'),
128
130
  destType: z.string().describe('Destination storage type e.g. 001'),
129
131
  destBin: z.string().describe('Destination bin e.g. 01-06-03'),
130
- destStorageUnit: z.string().optional().default('').describe('Destination storage unit (LENUM) — for SU-managed types, same as source SU when moving full SU')
132
+ destStorageUnit: z.string().optional().default('').describe('Destination storage unit (LENUM) — for SU-managed types, same as source SU when moving full SU'),
133
+ autoConfirm: z.boolean().optional().default(false).describe('Immediately confirm the TO after creation (sets I_SQUIT=X in L_TO_CREATE_SINGLE) — use for internal replenishment moves where no physical confirmation step is needed')
131
134
  },
132
135
  async (params) => {
133
136
  try {
@@ -184,7 +187,7 @@ server.tool(
184
187
  storageType: z.string().optional().describe('Filter by source or destination storage type e.g. 999'),
185
188
  bin: z.string().optional().describe('Filter by source or destination bin e.g. AUFNAHME'),
186
189
  material: z.string().optional().describe('Filter by material number e.g. TG0001'),
187
- top: z.number().optional().default(50).describe('Max TO headers to return')
190
+ top: z.coerce.number().optional().default(50).describe('Max TO headers to return')
188
191
  },
189
192
  async (params) => {
190
193
  try {
@@ -204,7 +207,7 @@ server.tool(
204
207
  warehouse: z.string().describe('Warehouse number e.g. 102'),
205
208
  storageType: z.string().optional().describe('Storage type e.g. 001. Omit to see all types.'),
206
209
  bin: z.string().optional().describe('Narrow to a specific bin within the type'),
207
- top: z.number().optional().default(100).describe('Max records to return')
210
+ top: z.coerce.number().optional().default(100).describe('Max records to return')
208
211
  },
209
212
  async (params) => {
210
213
  try {
@@ -225,7 +228,7 @@ server.tool(
225
228
  status: z.enum(['open', 'partial', 'to-created', 'completed']).optional().describe('TR status filter. Defaults to open + partial + to-created. Use to-created to see TRs where a TO exists but is not yet confirmed.'),
226
229
  material: z.string().optional().describe('Filter by material number e.g. TG0001'),
227
230
  storageType: z.string().optional().describe('Filter by source or destination storage type'),
228
- top: z.number().optional().default(50).describe('Max records to return')
231
+ top: z.coerce.number().optional().default(50).describe('Max records to return')
229
232
  },
230
233
  async (params) => {
231
234
  try {
@@ -246,7 +249,7 @@ server.tool(
246
249
  plant: z.string().describe('Plant e.g. 1010'),
247
250
  storageLocation: z.string().optional().describe('Storage location (LGORT) linked to this warehouse e.g. 0002 — required for accurate results, otherwise MARD returns all plant stock'),
248
251
  material: z.string().optional().describe('Narrow to a specific material'),
249
- threshold: z.number().optional().default(0).describe('Ignore variances smaller than this quantity')
252
+ threshold: z.coerce.number().optional().default(0).describe('Ignore variances smaller than this quantity')
250
253
  },
251
254
  async (params) => {
252
255
  try {
@@ -265,8 +268,8 @@ server.tool(
265
268
  {
266
269
  warehouse: z.string().describe('Warehouse number e.g. 102'),
267
270
  storageType: z.string().optional().describe('Limit to a specific storage type e.g. 001'),
268
- daysSinceLastCount: z.number().optional().default(180).describe('Flag bins not counted within this many days (default 180)'),
269
- top: z.number().optional().default(100).describe('Max bins to return')
271
+ daysSinceLastCount: z.coerce.number().optional().default(180).describe('Flag bins not counted within this many days (default 180)'),
272
+ top: z.coerce.number().optional().default(100).describe('Max bins to return')
270
273
  },
271
274
  async (params) => {
272
275
  try {
@@ -286,8 +289,8 @@ server.tool(
286
289
  warehouse: z.string().describe('Warehouse number e.g. 102'),
287
290
  storageType: z.string().optional().describe('Narrow to a specific storage type'),
288
291
  material: z.string().optional().describe('Narrow to a specific material'),
289
- daysSinceLastMove: z.number().optional().default(90).describe('Flag stock not moved in this many days (default 90)'),
290
- top: z.number().optional().default(100).describe('Max quants to scan')
292
+ daysSinceLastMove: z.coerce.number().optional().default(90).describe('Flag stock not moved in this many days (default 90)'),
293
+ top: z.coerce.number().optional().default(100).describe('Max quants to scan')
291
294
  },
292
295
  async (params) => {
293
296
  try {
@@ -342,8 +345,8 @@ server.tool(
342
345
  {
343
346
  warehouse: z.string().describe('Warehouse number e.g. 102'),
344
347
  storageType: z.string().optional().describe('Limit to a specific storage type e.g. 003'),
345
- threshold: z.number().optional().default(3).describe('Flag bin+material combos with this many quants or more (default 3)'),
346
- top: z.number().optional().default(200).describe('Max stock records to scan')
348
+ threshold: z.coerce.number().optional().default(3).describe('Flag bin+material combos with this many quants or more (default 3)'),
349
+ top: z.coerce.number().optional().default(200).describe('Max stock records to scan')
347
350
  },
348
351
  async (params) => {
349
352
  try {
@@ -362,8 +365,8 @@ server.tool(
362
365
  {
363
366
  warehouse: z.string().describe('Warehouse number e.g. 102'),
364
367
  storageType: z.string().optional().describe('Specific zone type to check e.g. 999. Defaults to 999 + 998.'),
365
- minAgeDays: z.number().optional().default(7).describe('Only show negatives older than this many days (default 7). Set to 0 to see all.'),
366
- top: z.number().optional().default(500).describe('Max stock records to scan')
368
+ minAgeDays: z.coerce.number().optional().default(7).describe('Only show negatives older than this many days (default 7). Set to 0 to see all.'),
369
+ top: z.coerce.number().optional().default(500).describe('Max stock records to scan')
367
370
  },
368
371
  async (params) => {
369
372
  try {
@@ -406,7 +409,7 @@ server.tool(
406
409
  createdBy: z.string().optional().describe('Filter by the SAP user who created the TO e.g. NOMANH'),
407
410
  executedBy: z.string().optional().describe('Filter by the SAP user who executed/confirmed the TO'),
408
411
  material: z.string().optional().describe('Filter by material number e.g. TG0001'),
409
- top: z.number().optional().default(50).describe('Max number of TOs to return (default 50)')
412
+ top: z.coerce.number().optional().default(50).describe('Max number of TOs to return (default 50)')
410
413
  },
411
414
  async (params) => {
412
415
  try {
@@ -444,10 +447,10 @@ server.tool(
444
447
  warehouse: z.string().describe('Warehouse number e.g. 102'),
445
448
  storageType: z.string().optional().default('P01').describe('Forward pick storage type to check (default P01)'),
446
449
  material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
447
- minimumQuantity: z.number().optional().default(0).describe('Flag bins at or below this level. Default 0 = empty/negative only. Overridden per bin when LAGP min qty (MNMNG) is configured.'),
448
- targetQuantity: z.number().optional().describe('Fill-to target — overrides bin master MaximumQuantity when set.'),
449
- defaultReplenishQty: z.number().optional().default(50).describe('Fallback replenishment qty when bin master max qty (MXMNG) is 0 and no targetQuantity given. Default 50.'),
450
- top: z.number().optional().default(50).describe('Max records to return')
450
+ minimumQuantity: z.coerce.number().optional().default(0).describe('Flag bins at or below this level. Default 0 = empty/negative only. Overridden per bin when LAGP min qty (MNMNG) is configured.'),
451
+ targetQuantity: z.coerce.number().optional().describe('Fill-to target — overrides bin master MaximumQuantity when set.'),
452
+ defaultReplenishQty: z.coerce.number().optional().default(50).describe('Fallback replenishment qty when bin master max qty (MXMNG) is 0 and no targetQuantity given. Default 50.'),
453
+ top: z.coerce.number().optional().default(50).describe('Max records to return')
451
454
  },
452
455
  async (params) => {
453
456
  try {
@@ -466,9 +469,9 @@ server.tool(
466
469
  {
467
470
  warehouse: z.string().describe('Warehouse number e.g. 102'),
468
471
  interimTypes: z.array(z.string()).optional().default(['999', '998', '902']).describe('Interim storage types to check — default [999, 998, 902]'),
469
- minDaysStranded: z.number().optional().default(0).describe('Only return stock stranded for at least this many days. Default 0 = include same-day strandings.'),
472
+ minDaysStranded: z.coerce.number().optional().default(0).describe('Only return stock stranded for at least this many days. Default 0 = include same-day strandings.'),
470
473
  material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
471
- top: z.number().optional().default(100).describe('Max quants to scan (scans top * 3 internally to account for client-side filtering)')
474
+ top: z.coerce.number().optional().default(100).describe('Max quants to scan (scans top * 3 internally to account for client-side filtering)')
472
475
  },
473
476
  async (params) => {
474
477
  try {
@@ -480,6 +483,47 @@ server.tool(
480
483
  }
481
484
  );
482
485
 
486
+ // Tool 23 — get_goods_issue_monitor
487
+ server.tool(
488
+ 'get_goods_issue_monitor',
489
+ 'Show open outbound deliveries in a classic WM warehouse — delivery qty vs picked qty, GI status, overdue flag, and which items still have stock in the GI zone (type 999). Use to track picking progress, identify stalled shipments, and understand the outbound delivery context behind TOs sourced from the GI zone.',
490
+ {
491
+ warehouse: z.string().describe('Warehouse number e.g. 102'),
492
+ shippingPoint: z.string().optional().describe('Filter by shipping point e.g. 1000'),
493
+ includeCompleted: z.boolean().optional().default(false).describe('Include deliveries where GI has already been posted (default false — open only)'),
494
+ material: z.string().optional().describe('Narrow to a specific material e.g. TG0001'),
495
+ top: z.coerce.number().optional().default(50).describe('Max delivery items to fetch (default 50)')
496
+ },
497
+ async (params) => {
498
+ try {
499
+ const result = await getGoodsIssueMonitor(params);
500
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
501
+ } catch (err) {
502
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
503
+ }
504
+ }
505
+ );
506
+
507
+ // Tool 24 — create_cycle_count_document
508
+ server.tool(
509
+ 'create_cycle_count_document',
510
+ 'Create a classic WM inventory document (cycle count) for a specific bin — equivalent to LI01N. Activates the bin for counting immediately by default. Use after get_cycle_count_candidates to trigger a count on flagged bins.',
511
+ {
512
+ warehouse: z.string().describe('Warehouse number e.g. 102'),
513
+ storageType: z.string().describe('Storage type of the bin to count e.g. 001'),
514
+ bin: z.string().describe('Storage bin to count e.g. 01-02-03'),
515
+ activateNow: z.boolean().optional().default(true).describe('Activate the inventory document immediately so the bin is ready for counting (default true)')
516
+ },
517
+ async (params) => {
518
+ try {
519
+ const result = await createCycleCountDoc(params);
520
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
521
+ } catch (err) {
522
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
523
+ }
524
+ }
525
+ );
526
+
483
527
  const transport = new StdioServerTransport();
484
528
  await server.connect(transport);
485
529
  console.error('SAP WM MCP Server running (stdio)...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-wm-mcp",
3
- "version": "0.2.9",
3
+ "version": "0.3.2",
4
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
5
  "type": "module",
6
6
  "main": "index.js",
@@ -0,0 +1,54 @@
1
+ import { s4hPost, s4hGet } from '../lib/s4hClient.js';
2
+ import { esc } from '../lib/sanitize.js';
3
+
4
+ const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001`;
5
+ const NS = `com.sap.gateway.srvd.zsd_wmmcpservice.v0001`;
6
+
7
+ export async function createCycleCountDoc({
8
+ warehouse,
9
+ storageType,
10
+ bin,
11
+ activateNow = true
12
+ }) {
13
+ const path = `${BASE}/WMTransferOrder/${NS}.CreateInventoryDocument`;
14
+
15
+ const body = {
16
+ WarehouseNumber: esc(warehouse),
17
+ StorageType: esc(storageType),
18
+ StorageBin: esc(bin),
19
+ ActivateNow: activateNow ? 'X' : ' '
20
+ };
21
+
22
+ await s4hPost(path, body);
23
+
24
+ // RAP static action returns $self (WMTransferOrder) — field assignment does not
25
+ // propagate back through the OData layer. Instead, read the inventory document
26
+ // number from the bin master record (LAGP.IVNUM) via a follow-up GET on
27
+ // WMStorageBin. When activateNow=true the bin is locked and IVNUM is set.
28
+ const binFilter = `WarehouseNumber eq '${esc(warehouse)}' and StorageType eq '${esc(storageType)}' and StorageBin eq '${esc(bin)}'`;
29
+ const binData = await s4hGet(`${BASE}/WMStorageBin?$filter=${encodeURIComponent(binFilter)}&$select=InventoryDocNumber,InventoryStatus`);
30
+ const binRow = binData?.value?.[0];
31
+ const invDocNumber = binRow?.InventoryDocNumber?.trim() || null;
32
+
33
+ if (!invDocNumber) {
34
+ return {
35
+ success: true,
36
+ warning: 'Inventory document created but number unavailable via OData — verify in SAP via LI04.',
37
+ warehouse,
38
+ storageType,
39
+ bin,
40
+ activated: activateNow
41
+ };
42
+ }
43
+
44
+ return {
45
+ success: true,
46
+ warehouse,
47
+ storageType,
48
+ bin,
49
+ inventoryDocNumber: invDocNumber,
50
+ inventoryStatus: binRow?.InventoryStatus ?? '',
51
+ activated: activateNow,
52
+ message: `Inventory document ${invDocNumber} created${activateNow ? ' and activated' : ''} for bin ${storageType}/${bin}`
53
+ };
54
+ }
@@ -7,7 +7,8 @@ export async function createTransferOrder({
7
7
  warehouse, movementType, material, plant,
8
8
  quantity, unitOfMeasure = '',
9
9
  sourceType = '', sourceBin = '', sourceStorageUnit = '',
10
- destType, destBin, destStorageUnit = ''
10
+ destType, destBin, destStorageUnit = '',
11
+ autoConfirm = false
11
12
  }) {
12
13
  const path = `${BASE}/WMTransferOrder/${NS}.CreateTransferOrder`;
13
14
 
@@ -23,7 +24,8 @@ export async function createTransferOrder({
23
24
  SourceStorageUnit: sourceStorageUnit,
24
25
  DestStorageType: destType,
25
26
  DestBin: destBin,
26
- DestStorageUnit: destStorageUnit
27
+ DestStorageUnit: destStorageUnit,
28
+ AutoConfirm: autoConfirm ? 'X' : ' '
27
29
  };
28
30
 
29
31
  // Snapshot the latest TO number BEFORE the call to avoid the race condition
@@ -0,0 +1,92 @@
1
+ import { s4hGet } from '../lib/s4hClient.js';
2
+ import { esc } from '../lib/sanitize.js';
3
+
4
+ const BASE = `/sap/opu/odata4/iwbep/all/srvd/sap/zsd_wmmcpservice/0001/WMGoodsIssueDelivery`;
5
+
6
+ const GI_STATUS = {
7
+ 'A': 'Not started',
8
+ 'B': 'Partially picked',
9
+ 'C': 'Goods issue posted',
10
+ ' ': 'Not started'
11
+ };
12
+
13
+ export async function getGoodsIssueMonitor({
14
+ warehouse,
15
+ shippingPoint,
16
+ includeCompleted = false,
17
+ material,
18
+ top = 50
19
+ }) {
20
+ const filters = [
21
+ `WarehouseNumber eq '${esc(warehouse)}'`
22
+ ];
23
+ if (!includeCompleted) filters.push(`GIStatus ne 'C'`);
24
+ if (shippingPoint) filters.push(`ShippingPoint eq '${esc(shippingPoint)}'`);
25
+ if (material) filters.push(`Material eq '${esc(material)}'`);
26
+
27
+ const path = `${BASE}?$filter=${encodeURIComponent(filters.join(' and '))}&$top=${top}&$orderby=PlannedGIDate asc`;
28
+ const data = await s4hGet(path);
29
+ const rows = data.value ?? [];
30
+
31
+ const today = new Date();
32
+ const todayStr = today.toISOString().slice(0, 10);
33
+
34
+ // Group items by delivery number
35
+ const deliveryMap = {};
36
+ for (const r of rows) {
37
+ const del = r.DeliveryNumber;
38
+ if (!deliveryMap[del]) {
39
+ deliveryMap[del] = {
40
+ deliveryNumber: del,
41
+ shippingPoint: r.ShippingPoint,
42
+ customer: r.Customer?.trimStart?.() ?? r.Customer,
43
+ plannedGIDate: r.PlannedGIDate,
44
+ actualGIDate: r.ActualGIDate || null,
45
+ deliveryDate: r.DeliveryDate,
46
+ giStatus: r.GIStatus,
47
+ giStatusText: GI_STATUS[r.GIStatus] ?? r.GIStatus,
48
+ isOverdue: r.PlannedGIDate && r.PlannedGIDate < todayStr && r.GIStatus !== 'C',
49
+ items: []
50
+ };
51
+ }
52
+ const deliveryQty = parseFloat(r.DeliveryQuantity ?? 0);
53
+ const pickedQty = parseFloat(r.PickedQuantity ?? 0);
54
+ deliveryMap[del].items.push({
55
+ item: r.DeliveryItem,
56
+ material: r.Material?.trimStart?.() ?? r.Material,
57
+ plant: r.Plant,
58
+ storageType: r.StorageType,
59
+ bin: r.StorageBin,
60
+ wmMovementType: r.WMMovementType,
61
+ wmTORequired: r.WMTORequired,
62
+ deliveryQty,
63
+ pickedQty,
64
+ remainingQty: Math.max(0, deliveryQty - pickedQty),
65
+ uom: r.DeliveryUOM,
66
+ pickComplete: deliveryQty > 0 && pickedQty >= deliveryQty
67
+ });
68
+ }
69
+
70
+ const deliveries = Object.values(deliveryMap);
71
+ const overdue = deliveries.filter(d => d.isOverdue);
72
+
73
+ return {
74
+ warehouse,
75
+ filters: {
76
+ shippingPoint: shippingPoint ?? 'all',
77
+ includeCompleted,
78
+ material: material ?? 'all'
79
+ },
80
+ summary: {
81
+ totalDeliveries: deliveries.length,
82
+ overdueDeliveries: overdue.length,
83
+ byStatus: {
84
+ notStarted: deliveries.filter(d => d.giStatus === 'A' || d.giStatus === ' ').length,
85
+ partial: deliveries.filter(d => d.giStatus === 'B').length,
86
+ complete: deliveries.filter(d => d.giStatus === 'C').length
87
+ }
88
+ },
89
+ truncated: rows.length === top,
90
+ deliveries
91
+ };
92
+ }