應用情境 - ERP 採購單自動觸發 BPM 流程
「外部起單」功能用於讓外部系統的資料直接起單,建立 UOF X 的表單。在與 ERP、CRM 等外部系統進行整合時,若需要透過這些系統輸入資料來觸發 UOF X 的流程,就需要使用「外部起單」功能,以下會展示一個應用情境:「ERP 採購單自動觸發 BPM 流程」,並提供具體的實作步驟與方法作為參考。
應用情境¶
採購人員原採用於 ERP 系統輸入採購單後,列印紙本單據進行內部簽核,審核過後再於 ERP 系統將正式採購單向供應商進行訂購。
然而,現有作業中的紙本簽核和流程追蹤較為繁瑣,容易出現延誤或錯誤。採購單簽核中如需修改採購單資訊,更是容易發生資訊未更新至 ERP 的狀況。 透過 UOF X 外部起單功能,資訊部門開發將 ERP 系統輸入採購單後,可自動將採購單拋轉至 UOF X 系統進行電子簽核,並在起單成功時將表單編號寫入 ERP 資料庫,再於簽核流程完成後,將狀態依據單據結果更新回 ERP,進行後續採購作業。
外部起單流程圖
flowchart LR
A(ERP系統新增採購單) --> B(執行外部起單) --> C(UOF X簽核) --> D(ERP系統更新採購單狀態)
UOF X 與 ERP 整合優勢¶
- 自動化審核流程:減少人為錯誤,提高工作效率。
- 流程透明:管理層可以於 UOF X 隨時查看採購單審查進度,方便控管。
- 數據一致性:通過 ERP 與 UOF X 系統的整合,確保採購單資訊的準確性和一致性。
- 流程設定彈性:於 UOF X 可根據不同的採購條件,自動選擇不同的審核流程。
實作¶
在繼續往下說明之前,請確認您下列事項皆已經準備完成:
- 已取得 UOF X
站台網址
- 已取得
金鑰
- 將範例表單匯入 UOF X 站台,參考新增表單
- 匯出一份
API 起單檔案
,參考取得 API 起單檔案
匯入範例表單下載:formScript.edez
範例程式碼:UOFX-SDK-Training
以下為此情境中外部起單的實作步驟:
- 建立模擬 ERP 資料庫資料
- 設定金鑰、站台網址: 可參考 SDK 參數設定
- 設定申請者資料: 可參考 使用說明
- 設定 CallBackUrl、CustomData: 可參考 使設定 Callback
- 透過 FormSchema 填寫表單欄位資料: 欄位資料格式可參考支援欄位
- 透過 ByFormSchema 取得起單內容,呼叫站台進行起單: 可參考呼叫 X 站台進行起單
- 起單成功後將表單編號寫入 ERP 資料庫
- 將單據結果寫回 ERP 資料庫: 可參考外部程式站點
建立模擬 ERP 資料庫資料¶
若想要依照範例程式碼進行實作,可先依照以下步驟建立資料庫資料
A. 掛載北風資料庫¶
下載 instnwnd.sql 資料庫檔案,並將其載入 SQL Server 中
B. 建立採購單資料表¶
-- 建立 Purchases 表
CREATE TABLE Purchases (
PurchaseID INT IDENTITY(1,1) PRIMARY KEY, -- 採購單號
SupplierID INT, -- 供應商編號
Status NVARCHAR(50), -- 狀態(待審核/已核准/已作廢/已拒絕)
PurchaseDate DATETIME, -- 採購日期
PurchaseType NVARCHAR(50), -- 採購類型(一般採購/緊急採購)
BpmID NVARCHAR(50), -- BPM 簽核單號
FOREIGN KEY (SupplierID) REFERENCES Suppliers(SupplierID) -- 跟 Suppliers 表的 SupplierID 關聯
);
-- 建立 Purchase Details 表
CREATE TABLE [Purchase Details] (
PurchaseDetailID INT IDENTITY(1,1) PRIMARY KEY, -- 採購明細編號
PurchaseID INT, -- 採購單號
ProductID INT, -- 產品ID
Quantity INT, -- 數量
UnitPrice DECIMAL(18, 2), -- 單價
Subtotal AS (Quantity * UnitPrice) PERSISTED, -- 小計 (計算欄位)
FOREIGN KEY (PurchaseID) REFERENCES Purchases(PurchaseID), -- 跟 Purchases 表的 PurchaseID 關聯
FOREIGN KEY (ProductID) REFERENCES Products(ProductID) -- 跟 Products 表的 ProductID 關聯
);
C. 匯入起單資料¶
我們模擬在 ERP 系統新增採購單後,將採購單資料寫入 ERP 資料庫的動作,並將狀態設定為待審核,BPM 單號設定為 Null
DECLARE @PurchaseID INT;
-- 插入 Purchases 資料
INSERT INTO Purchases (SupplierID, Status, PurchaseDate, PurchaseType)
VALUES (3, '待審核', '2025-03-14', '一般採購');
-- 取得自動生成的 PurchaseID
SET @PurchaseID = SCOPE_IDENTITY();
-- 插入 Purchase Details 明細資料
INSERT INTO [Purchase Details] (PurchaseID, ProductID, Quantity, UnitPrice)
VALUES
(@PurchaseID, 1, 10, 130),
(@PurchaseID, 2, 25, 110),
(@PurchaseID, 4, 13, 95);
新增採購單 API 範例程式碼
/**
* 新增一筆採購單及多筆採購明細
* @param model 採購單資料
* @return 新增的採購單編號
*/
internal decimal InsertPurchase([Bind] PurchaseModel model)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string sqlPurchase = @"
INSERT INTO Purchases
(SupplierID, Status, PurchaseDate, PurchaseType)
VALUES
(@SupplierID, @Status, @PurchaseDate, @PurchaseType)
SELECT SCOPE_IDENTITY(); -- 取得自動生成的 PurchaseID
";
using (var command = new SqlCommand(sqlPurchase, connection))
{
// 置換參數
command.Parameters.AddWithValue("@SupplierID", model.SupplierID);
command.Parameters.AddWithValue("@Status", model.Status);
command.Parameters.AddWithValue("@PurchaseDate", model.PurchaseDate);
command.Parameters.AddWithValue("@PurchaseType", model.PurchaseType);
// 取得自動生成的 PurchaseID
model.PurchaseID = Convert.ToInt32(command.ExecuteScalar());
}
foreach (var purchaseDetail in model.PurchaseDetails)
{
string sqlDetail = @"
INSERT INTO [Purchase Details]
(PurchaseID, ProductID, Quantity, UnitPrice)
VALUES
(@PurchaseID, @ProductID, @Quantity, @UnitPrice)
";
using (var commandDetail = new SqlCommand(sqlDetail, connection))
{
// 置換參數
commandDetail.Parameters.AddWithValue("@PurchaseID", model.PurchaseID);
commandDetail.Parameters.AddWithValue("@ProductID", purchaseDetail.ProductID);
commandDetail.Parameters.AddWithValue("@Quantity", purchaseDetail.Quantity);
commandDetail.Parameters.AddWithValue("@UnitPrice", purchaseDetail.UnitPrice);
commandDetail.ExecuteNonQuery();
}
}
return model.PurchaseID;
}
}
設定金鑰、站台網址¶
可參考 SDK 參數設定
// 設定金鑰
UofxService.Key = "xxx";
// 設定 UOF X 站台網址
UofxService.UofxServerUrl = "https://myuofx.com.tw/";
設定申請者資料¶
可參考 使用說明
//建立 外部起單物件
var doc = new Ede.Uofx.FormSchema.UofxFormSchema.UofxFormSchema()
{
//申請者帳號
Account = "USER_ACCOUNT",
//申請者部門代號
DeptCode = "USER_DEPT_CODE",
};
設定 CallBackUrl、CustomData¶
因為在起單成功時需要將表單編號寫入 ERP 資料庫,而更新表單編號需要採購單號,所以我們在設定客製資訊時,除了時間戳記外,也將採購單號新增至客製資訊中
//客製資訊
var customDataObj = new
{
Timestamp = DateTimeOffset.Now,
PurchaseID = 1
};
//建立 外部起單物件
var doc = new Ede.Uofx.FormSchema.UofxFormSchema.UofxFormSchema()
{
...
//要 CallBack 的 Url
CallBackUrl = "https://myuofx.com.tw/api/sdk/callback",
//客製資訊: 填入起單時間
CustomData = JsonConvert.SerializeObject(customDataObj),
};
設定資料庫連線字串¶
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "data source=YourIP;initial catalog=YourDBName;user id=YourId;password=YourPassword;Persist Security Info=True;MultipleActiveResultSets=True;TrustServerCertificate=True"
},
"AllowCors": [],
"UofxServiceSettings": {
"UofxServiceKey": "",
"UofxServiceUrl": ""
}
}
填寫表單欄位資料¶
//建立 表單欄位物件,填寫表單欄位資料
doc.Fields = new Ede.Uofx.FormSchema.UofxFormSchema.UofxFormSchemaFields()
{
// 採購單號
PurchaseID = "1",
// 供應商編號
SupplierID = "3",
// 採購日期
PurchaseDate = DateTimeOffset.Parse("2025/3/14"),
// 採購類型
PurchaseType = "一般採購",
// 採購明細
PurchaseDetail = new List<PurchaseDetailRow>
{
new PurchaseDetailRow
{
PurchaseDetailID = "1",
ProductID = "1",
Quantity = 10,
UnitPrice = 130,
},
new PurchaseDetailRow
{
PurchaseDetailID = "2",
ProductID = "2",
Quantity = 25,
UnitPrice = 110,
},
new PurchaseDetailRow
{
PurchaseDetailID = "3",
ProductID = "4",
Quantity = 13,
UnitPrice = 95,
}
}
};
呼叫站台進行起單¶
// 使用 表單專屬檔案 產生內容
var doc = await ByFormSchema.GenFormContentAsync();
try
{
// 呼叫站台進行起單
var traceId = await UofxService.BPM.ApplyForm(doc);
Console.WriteLine($"Trace Id: {traceId}");
}
catch (Exception e)
{
// 將 exception 轉換成較容易判斷的 model
var model = UofxService.Error.ConvertToModel(e);
// 將 model 轉成 json 格式印出
Console.WriteLine(UofxService.Json.Convert(model));
}
起單成功後將表單編號寫入 ERP 資料庫¶
在呼叫站台進行起單後,不管起單成功與否,皆會執行 Callback 方法,並且只有在起單成功時會回傳表單編號,因此我們可以在執行 Callback 方法並且起單成功時,將表單編號寫入 ERP 資料庫
[HttpPost("callback")]
public IActionResult Callback([Bind] JsonElement requestBody)
{
try
{
// 解密 api request model
var callbackModel = UofxService.DecodeCallBack<FormApplyResponseModel>(requestBody.ToString());
// 將 CallbackModel 印出
Console.WriteLine(JsonConvert.SerializeObject(callbackModel));
// 取得客製資訊
var customDataObj = JsonConvert.DeserializeObject<dynamic>(callbackModel.CustomData);
// 確認是否起單成功
if (callbackModel?.UofxData?.FormSn != null)
{
// 建立更新 BPM model
var model = new UpdateBpmModel
{
PurchaseID = customDataObj?.PurchaseID,
BpmID = callbackModel?.UofxData?.FormSn
};
// 更新採購單簽核單號
_erpService.UpdateBpm(model);
}
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/**
* 更新簽核資料
* @param model 更新簽核資料
* @return 更新後的簽核ID
*/
internal string UpdateBpm([Bind] UpdateBpmModel model)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string sql = @"
UPDATE Purchases
SET BpmID = @BpmID
WHERE PurchaseID = @PurchaseID
";
using (var command = new SqlCommand(sql, connection))
{
// 置換參數
command.Parameters.AddWithValue("@BpmID", model.BpmID);
command.Parameters.AddWithValue("@PurchaseID", model.PurchaseID);
int rowsAffected = command.ExecuteNonQuery();
}
}
return model.BpmID;
}
將單據結果寫回 ERP 資料庫¶
A. 撰寫更新採購單狀態 API¶
// 更新採購單狀態
[HttpPost("purchase/update-status")]
public IActionResult UpdatePurchaseStatus([Bind] UpdatePurchaseStatusModel model)
{
try
{
return Ok(new { purchaseId = model.PurchaseID, status = _erpService.UpdatePurchaseStatus(model) });
}
catch (Exception ex)
{
return BadRequest(new { message = ex.Message });
}
}
Approve
、Reject
、Cancel
三種狀態分別為通過、否決、作廢,我們可以置換成 ERP 系統的狀態文字
/**
* 更新採購單狀態
* @param model 更新狀態資料
* @return 更新後的採購單狀態
*/
internal string UpdatePurchaseStatus([Bind] UpdatePurchaseStatusModel model)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string sqlUpdateStatus = @"
UPDATE Purchases
SET Status = @Status
WHERE PurchaseID = @PurchaseID
";
using (var command = new SqlCommand(sqlUpdateStatus, connection))
{
// 置換參數
command.Parameters.AddWithValue("@PurchaseID", model.PurchaseID);
// 轉換狀態文字
string status = "";
switch (model.Status)
{
case "Approve":
status = "已通過";
break;
case "Reject":
status = "已否決";
break;
case "Cancel":
status = "已作廢";
break;
default:
status = model.Status;
break;
}
command.Parameters.AddWithValue("@Status", status);
int rowsAffected = command.ExecuteNonQuery();
}
}
return model.Status;
}
B. 新增外部連線設定¶
如果想要在表單結案後自動將單據結果回寫至 ERP 系統,可以在結案後流程新增一個「外部程式站點」,將單據結果傳遞到 ERP 系統,所以接著我們依序建立「外部連線設定」與「外部資料來源」。首先到 UOF X 站台後進到管理者模式,依序進到串接服務 > 外部連線設定 > 新增 API 連線,可參考 外部連線設定
C. 新增外部資料來源¶
可參考 外部資料來源
- API URL:api/northwind/purchase/update-status
- Method:Post
- 參數設定:
- PurchaseID:採購單ID、數值、設計表單時再指定
- Status:狀態、文字、設計表單時再指定
D. 結案後流程新增外部程式站點¶
可參考 外部程式站點
選擇資料來源選擇剛剛新增的外部資料來源,並將參數來源設定為採購單號及單據結果
成果展示¶
執行此範例程式時,會先呼叫 UOF X 站台進行起單,並啟動 API 服務,提供 Callback、更新採購單狀態等功能做呼叫
簽核中修改採購單資訊
若在簽核中需要修改採購單資訊,請參考欄位設定與權限來設定需修改的欄位
非 .NET 平台的解決方案
若開發環境非 .NET,但仍需使用 SDK 提供的外部起單功能,可考慮將 .NET SDK 包裝成 Web API。透過此方式,其他非 .NET 平台也能透過 HTTP 請求存取該 API,實現跨平台的外部起單功能。