301 lines
13 KiB
C#
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() : "";
|
|
}
|
|
}
|