Files
sap-erp/services/SapDiConsole/Program.cs
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

301 lines
13 KiB
C#

using SAPbobsCOM;
using System;
using System.Collections.Generic;
using System.Web.Script.Serialization;
namespace SapDiConsole
{
class Program
{
static Company oCompany;
// Persistent request loop — one line of JSON in on stdin, one line of
// JSON out on stdout, per request, kept alive across MANY requests
// instead of the old one-shot "connect, do one thing, disconnect,
// exit" model. Company.Connect() alone was measured at ~30s (SAP's
// SLD/License round trip dominates it) — paying that cost on every
// single Purchase Request was the real cause of "creating a PR takes
// long". Node's services/diApiService.js now keeps ONE of these
// processes alive per SAP company and reuses it; EnsureConnected()
// below only reconnects if the session has actually dropped.
static int Main(string[] args)
{
var js = new JavaScriptSerializer();
string line;
while ((line = Console.In.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
Dictionary<string, object> p;
try { p = js.Deserialize<Dictionary<string, object>>(line); }
catch (Exception ex)
{
Console.WriteLine(js.Serialize(new { success = false, error = "Bad request JSON: " + ex.Message }));
continue;
}
string action = Get(p, "Action", "AddPurchaseRequest");
if (action == "Shutdown")
{
try { oCompany?.Disconnect(); } catch { }
return 0;
}
try
{
EnsureConnected(p);
object result;
switch (action)
{
case "Ping":
result = new { success = true, ping = true };
break;
case "RemoveProductionOrderLine":
result = RemoveProductionOrderLine(p);
break;
default:
result = AddPurchaseRequest(p);
break;
}
Console.WriteLine(js.Serialize(result));
}
catch (Exception ex)
{
// Non-fatal per request — stay alive for the next one.
// A connection-level failure will simply cause the next
// EnsureConnected() to notice oCompany.Connected is false
// and reconnect.
Console.WriteLine(js.Serialize(new {
success = false,
error = ex.Message
}));
}
}
// Node closed stdin (process shutting down, or replacing this
// pooled connection) — disconnect cleanly before exiting.
try { oCompany?.Disconnect(); } catch { }
return 0;
}
// Reuses the existing SAP session unless it's genuinely gone —
// avoids paying Connect()'s ~30s SLD/License round trip on every
// request, which is the whole point of this persistent process.
static void EnsureConnected(Dictionary<string, object> p)
{
if (oCompany != null && oCompany.Connected) return;
Connect(p);
}
// ── Remove a component line from a Production Order ──────────────
// Service Layer (OData PATCH) has no way to delete a ProductionOrderLines
// row — verified live, omitting a line from the array is silently
// ignored. DI API's ProductionOrders.Lines collection DOES support
// SetCurrentLine + Delete() even on a Released order (ProductionOrders
// has its OWN dedicated Lines interface — ProductionOrders_Lines, NOT
// the generic Document_Lines used by regular marketing documents).
// Used to actually delete a Deviation-superseded line (SAP-side; the
// app's own audit trail — the deviation record itself — is untouched
// either way).
static object RemoveProductionOrderLine(Dictionary<string, object> p)
{
int docEntry = int.Parse(Get(p, "DocEntry"));
int lineNum = int.Parse(Get(p, "LineNum"));
SAPbobsCOM.ProductionOrders po = (SAPbobsCOM.ProductionOrders)oCompany.GetBusinessObject(BoObjectTypes.oProductionOrders);
if (!po.GetByKey(docEntry))
throw new Exception($"Production Order {docEntry} not found");
int idx = -1;
for (int i = 0; i < po.Lines.Count; i++)
{
po.Lines.SetCurrentLine(i);
if (po.Lines.LineNumber == lineNum) { idx = i; break; }
}
if (idx < 0)
throw new Exception($"Line {lineNum} not found on Production Order {docEntry}");
po.Lines.SetCurrentLine(idx);
po.Lines.Delete();
int res = po.Update();
if (res != 0)
throw new Exception($"Update failed [{res}]: {oCompany.GetLastErrorDescription()}");
Console.Error.WriteLine($"[PROD-ORDER] Removed line {lineNum} from DocEntry {docEntry}");
return new { success = true, DocEntry = docEntry, RemovedLine = lineNum };
}
// ── Standalone DI API connection (no SAP client needed) ──────────
static void Connect(Dictionary<string, object> p)
{
oCompany = new Company();
// Order matters in SAP DI API: Server → DbServerType → CompanyDB → credentials
oCompany.Server = Get(p, "Server", "SAPSVR");
// DbServerType MUST match the SQL Server version or Connect() fails
// SBO-user auth with -132 (the DB itself connects fine via sa first).
// This install runs SQL Server 2022, but DI API 10.0 build 260 only
// knows up to dst_MSSQL2019 = 15 — use that (2022 is wire-compatible).
// Numeric cast (NOT the named enum) so values like 16 = dst_MSSQL2022
// compile even though this interop build only defines up to
// dst_MSSQL2019 = 15. Value comes from Node (env SAP_B1_DB_SERVER_TYPE).
// oCompany.DbServerType = (BoDataServerTypes)int.Parse(Get(p, "DbServerType", "15"));
oCompany.DbServerType = BoDataServerTypes.dst_MSSQL2022;
// No hardcoded fallback credentials — Node (services/diApiService.js)
// always supplies these from .env via the stdin JSON payload; an
// empty default here means a missing key fails loudly with a clear
// SAP login error instead of silently connecting with a real
// password that used to be baked into this binary.
oCompany.CompanyDB = Get(p, "CompanyDB");
oCompany.UserName = Get(p, "UserName");
oCompany.Password = Get(p, "Password");
oCompany.DbUserName = Get(p, "DbUserName");
oCompany.DbPassword = Get(p, "DbPassword");
oCompany.UseTrusted = false;
oCompany.language = 0;
// SAP B1 10.0 standalone DI API auth goes through the SLD/License
// service — without these, Connect() fails SBO auth with -132 even
// when the DB connection and password are valid. host:30010 = license
// service, host:40000 = SLD (System Landscape Directory).
string lic = Get(p, "LicenseServer", "");
if (!string.IsNullOrEmpty(lic)) oCompany.LicenseServer = lic;
string sld = Get(p, "SLDServer", "");
if (!string.IsNullOrEmpty(sld)) oCompany.SLDServer = sld;
int ret = oCompany.Connect();
if (ret != 0)
throw new Exception(
$"Login failed [{ret}]: {oCompany.GetLastErrorDescription()}");
Console.Error.WriteLine($"[DI] Connected as {Get(p,"UserName")} to {Get(p,"CompanyDB")}");
}
// ── Create Purchase Request ───────────────────────────────────────
static object AddPurchaseRequest(Dictionary<string, object> p)
{
Documents pr = (Documents)oCompany.GetBusinessObject(
BoObjectTypes.oPurchaseRequest);
// Header
string docDate = Get(p, "DocDate");
string dueDate = Get(p, "DocDueDate");
string reqDate = Get(p, "RequriedDate");
if (!string.IsNullOrEmpty(docDate)) pr.DocDate = DateTime.Parse(docDate);
if (!string.IsNullOrEmpty(dueDate)) pr.DocDueDate = DateTime.Parse(dueDate);
if (!string.IsNullOrEmpty(reqDate)) pr.RequriedDate = DateTime.Parse(reqDate);
string comments = Get(p, "Comments");
string cardCode = Get(p, "CardCode");
string numAtCard = Get(p, "NumAtCard");
string email = Get(p, "RequesterEmail");
if (!string.IsNullOrEmpty(comments)) pr.Comments = comments;
if (!string.IsNullOrEmpty(cardCode)) pr.CardCode = cardCode;
if (!string.IsNullOrEmpty(numAtCard)) pr.NumAtCard = numAtCard;
if (!string.IsNullOrEmpty(email)) pr.RequesterEmail = email;
// UDFs
SetUdf(pr, "U_ReqDqte", Get(p, "U_ReqDqte"));
SetUdf(pr, "U_Depart", Get(p, "U_Depart"));
SetUdf(pr, "U_Department", Get(p, "U_Department"));
SetUdf(pr, "U_STS_REQD", Get(p, "U_STS_REQD"));
// Lines
var lines = p.ContainsKey("DocumentLines")
? p["DocumentLines"] as System.Collections.ArrayList
: null;
bool first = true;
if (lines != null)
{
foreach (var lineObj in lines)
{
var line = lineObj as Dictionary<string, object>;
if (line == null) continue;
if (!first) pr.Lines.Add();
pr.Lines.ItemCode = GetL(line, "ItemCode");
string desc = GetL(line, "ItemDescription");
if (!string.IsNullOrEmpty(desc)) pr.Lines.ItemDescription = desc;
string qty = GetL(line, "Quantity");
if (!string.IsNullOrEmpty(qty)) pr.Lines.Quantity = double.Parse(qty);
string price = GetL(line, "UnitPrice");
if (!string.IsNullOrEmpty(price)) pr.Lines.UnitPrice = double.Parse(price);
string whs = GetL(line, "WarehouseCode");
if (!string.IsNullOrEmpty(whs)) pr.Lines.WarehouseCode = whs;
string lineDate = GetL(line, "RequiredDate");
if (!string.IsNullOrEmpty(lineDate)) pr.Lines.RequiredDate = DateTime.Parse(lineDate);
first = false;
}
}
Console.Error.WriteLine("[PR] Adding Purchase Request...");
int res = pr.Add();
if (res != 0)
throw new Exception(
$"Add PR failed [{res}]: {oCompany.GetLastErrorDescription()}");
string docEntry = oCompany.GetNewObjectKey();
Console.Error.WriteLine($"[PR] SUCCESS : {docEntry}");
// ── Check approval status ─────────────────────────────────────
bool approvalTriggered = false;
string wddCode = null;
string wddStatus = null;
try
{
Recordset rs = (Recordset)oCompany.GetBusinessObject(
BoObjectTypes.BoRecordset);
rs.DoQuery(
$"SELECT TOP 1 WddStatus, DraftKey FROM OPRQ WHERE DocEntry = {docEntry}");
if (!rs.EoF)
{
wddStatus = rs.Fields.Item("WddStatus").Value?.ToString();
wddCode = rs.Fields.Item("DraftKey").Value?.ToString();
approvalTriggered = wddStatus == "W" || wddStatus == "A";
Console.Error.WriteLine($"[APPROVAL] WddStatus={wddStatus} DraftKey={wddCode}");
}
}
catch { /* approval check is informational only */ }
return new {
success = true,
DocEntry = int.Parse(docEntry),
ApprovalTriggered = approvalTriggered,
WddStatus = wddStatus,
WddCode = wddCode
};
}
static void SetUdf(Documents doc, string field, string value)
{
try
{
if (!string.IsNullOrEmpty(value))
doc.UserFields.Fields.Item(field).Value = value;
}
catch { }
}
static string Get(Dictionary<string, object> d, string k, string def = "")
=> d.ContainsKey(k) && d[k] != null ? d[k].ToString() : def;
static string GetL(Dictionary<string, object> d, string k)
=> d.ContainsKey(k) && d[k] != null ? d[k].ToString() : "";
}
}