ERP Integration Guide¶
This guide shows how to build an ERP connector that enriches components with live data, uploads BOMs, syncs project variables, and provides a parts search dialog. The example uses SQL Server, but the same pattern applies to SAP, REST APIs, or any other backend.
Which capabilities to implement¶
| Capability | Method | Purpose |
|---|---|---|
getComponentParameters |
GetComponentParameters |
Enrich components with ERP data (status, price, stock) |
uploadBom |
UploadBom |
Push the schematic BOM to the ERP when the user closes the project |
getProjectVariables |
GetProjectVariables |
Pull title-block data from the ERP when the file opens |
setProjectVariables |
SetProjectVariables |
Push title-block changes back to the ERP when the file closes |
getPartFamilyMembers |
GetPartFamilyMembers |
List variants in a part family |
Start with getComponentParameters. Add others as needed.
Connect to the ERP¶
private SqlConnection _db = null!;
public override void Initialize(InitializeRequest request)
{
var config = IniFile.Load(request.ConfigPath);
string connStr = config["Database", "ConnectionString"];
_db = new SqlConnection(connStr);
_db.Open(); // verify connection at startup
_log = new FileLogger(
Path.Combine(Path.GetDirectoryName(request.ConfigPath)!, "acme-erp.log"));
}
GetComponentParameters¶
public override GetComponentParametersResponse GetComponentParameters(
GetComponentParametersRequest request)
{
const string sql = """
SELECT ArticleCode, StatusCode, Description, PriceEUR,
Manufacturer, ManufacturerPartNo, LeadTimeDays, StockQty
FROM Articles
WHERE ArticleCode = @code
""";
using var cmd = new SqlCommand(sql, _db);
cmd.Parameters.AddWithValue("@code", request.ArticleCode);
using var reader = cmd.ExecuteReader();
if (!reader.Read())
throw new PluginException("NOT_FOUND",
$"Article '{request.ArticleCode}' not found in ERP.");
return new GetComponentParametersResponse
{
ArticleCode = reader.GetString(0),
Status = MapStatus(reader.GetString(1)),
Description = reader.GetString(2),
Price = reader.GetDouble(3),
Currency = "EUR",
Manufacturer = reader.GetString(4),
ManufacturerPartNo = reader.GetString(5),
LeadTimeDays = reader.GetInt32(6),
Stock = reader.GetInt32(7),
};
}
private static string MapStatus(string erpCode) => erpCode switch
{
"REL" => "released",
"DRF" => "draft",
"OBS" => "obsolete",
"BLK" => "blocked",
_ => "draft",
};
Caching
If your ERP query is slow, cache results by article code for the duration of the session. Use ConcurrentDictionary<string, GetComponentParametersResponse> and a reasonable TTL (e.g., 5 minutes).
GetProjectVariables¶
Called when a file opens. Return the title-block fields for this project:
public override GetProjectVariablesResponse GetProjectVariables(
GetProjectVariablesRequest request)
{
if (request.ProjectId is null)
throw new PluginException("VALIDATION_ERROR",
"No project ID available. Cannot fetch project variables.");
const string sql = """
SELECT CustomerName, Revision, DrawingNumber, Description, ProjectManager
FROM Projects
WHERE ProjectId = @id
""";
using var cmd = new SqlCommand(sql, _db);
cmd.Parameters.AddWithValue("@id", request.ProjectId);
using var reader = cmd.ExecuteReader();
if (!reader.Read())
throw new PluginException("NOT_FOUND",
$"Project '{request.ProjectId}' not found in ERP.");
return new GetProjectVariablesResponse
{
Variables = new()
{
["CustomerName"] = reader.GetString(0),
["Revision"] = reader.GetString(1),
["DrawingNumber"] = reader.GetString(2),
["Description"] = reader.GetString(3),
["ProjectManager"] = reader.GetString(4),
}
};
}
SetProjectVariables¶
Called when a file closes. Push any changed title-block data back to the ERP:
public override void SetProjectVariables(SetProjectVariablesRequest request)
{
if (request.ProjectId is null) return;
const string sql = """
UPDATE Projects
SET Revision = @rev, Description = @desc
WHERE ProjectId = @id
""";
using var cmd = new SqlCommand(sql, _db);
cmd.Parameters.AddWithValue("@id", request.ProjectId);
cmd.Parameters.AddWithValue("@rev", request.Variables.GetValueOrDefault("Revision", ""));
cmd.Parameters.AddWithValue("@desc",request.Variables.GetValueOrDefault("Description", ""));
cmd.ExecuteNonQuery();
}
UploadBom¶
Called when the user closes the project (if auto-upload is enabled). The BOM is a flat or hierarchical list of BomItem objects:
public override UploadBomResponse UploadBom(UploadBomRequest request)
{
if (request.Format == "file")
{
// HydroSym has already generated a BOM file — upload it as-is
var file = request.ExportedFiles?.FirstOrDefault(f => f.Format == "xml")
?? throw new PluginException("VALIDATION_ERROR", "Expected an XML export file.");
var bomId = _erp.UploadBomFile(request.ProjectId!, file.FilePath);
return new UploadBomResponse { BomId = bomId };
}
// Structured BOM — insert line by line
string bomId = _erp.BeginBom(request.ProjectId!);
foreach (var item in request.Items ?? [])
{
_erp.AddBomItem(bomId, new ErpBomLine
{
Position = item.Position,
ArticleCode = item.ArticleCode,
Quantity = item.Quantity,
Unit = item.Unit ?? "pcs",
});
}
_erp.CommitBom(bomId);
return new UploadBomResponse
{
BomId = bomId,
Message = $"BOM uploaded with {request.Items?.Count ?? 0} items.",
};
}
Adding a search menu item¶
Declare the item in GetInfo with ResultType = "component":
new MenuItem
{
Id = "erp-search",
Label = "&Search ERP Parts...",
Location = "plugin",
Icon = "search",
ResultType = "component",
}
Implement OnMenuAction:
public override OnMenuActionResponse OnMenuAction(OnMenuActionRequest request)
{
if (request.MenuItemId != "erp-search")
throw new PluginException("NOT_FOUND", $"Unknown menu item: {request.MenuItemId}");
var dialog = new ErpSearchDialog(_db)
{
Owner = NativeWindow.FromHandle(new IntPtr(request.ParentWindowHandle))
};
if (dialog.ShowDialog() != DialogResult.OK || dialog.SelectedArticle is null)
return new OnMenuActionResponse(); // user cancelled
var art = dialog.SelectedArticle;
return new OnMenuActionResponse
{
ArticleCode = art.ArticleCode,
Description = art.Description,
Manufacturer = art.Manufacturer,
ManufacturerPartNo = art.ManufacturerPartNo,
};
}
See Custom Search Dialog for the dialog implementation.
Complete example¶
A complete ERP connector sample is in the SDK repository at samples/AcmeErpConnector/. It includes:
- SQL Server queries against a sample schema
- A WinForms search dialog
- An xUnit test suite with an in-memory SQLite backend
- Integration test scripts for HydroSymPluginTester