跳轉到

應用情境 - 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系統更新採購單狀態)

Image open-form-manager

欄位示意圖

Image open-form-manager

流程示意圖

UOF X 與 ERP 整合優勢

  • 自動化審核流程:減少人為錯誤,提高工作效率。
  • 流程透明:管理層可以於 UOF X 隨時查看採購單審查進度,方便控管。
  • 數據一致性:通過 ERP 與 UOF X 系統的整合,確保採購單資訊的準確性和一致性。
  • 流程設定彈性:於 UOF X 可根據不同的採購條件,自動選擇不同的審核流程。

實作

在繼續往下說明之前,請確認您下列事項皆已經準備完成:

  1. 已取得 UOF X 站台網址
  2. 已取得 金鑰
  3. 將範例表單匯入 UOF X 站台,參考新增表單
  4. 匯出一份 API 起單檔案,參考取得 API 起單檔案

匯入範例表單下載:formScript.edez
範例程式碼:UOFX-SDK-Training

以下為此情境中外部起單的實作步驟:

建立模擬 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 關聯
);

Image open-form-manager

執行後 northwind 會產生 2 張表

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);

Image open-form-manager

執行後新增採購單資料及明細到資料表中
新增採購單 API 範例程式碼
ERPController.cs
// 新增採購單
[HttpPost("purchase/add")]
public IActionResult InsertPurchase([Bind] PurchaseModel model)
{
    try
    {
        return Ok(new { purchaseId = _erpService.InsertPurchase(model) });
    }
    catch (Exception ex)
    {
        return BadRequest(new { message = ex.Message });
    }
}
ERPService.cs
/**
* 新增一筆採購單及多筆採購明細
* @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 參數設定

Program.cs
// 設定金鑰
UofxService.Key = "xxx";

// 設定 UOF X 站台網址
UofxService.UofxServerUrl = "https://myuofx.com.tw/";

設定申請者資料

可參考 使用說明

ByFormSchema.cs
//建立 外部起單物件
var doc = new Ede.Uofx.FormSchema.UofxFormSchema.UofxFormSchema()
{
    //申請者帳號
    Account = "USER_ACCOUNT",
    //申請者部門代號
    DeptCode = "USER_DEPT_CODE",
};

設定 CallBackUrl、CustomData

因為在起單成功時需要將表單編號寫入 ERP 資料庫,而更新表單編號需要採購單號,所以我們在設定客製資訊時,除了時間戳記外,也將採購單號新增至客製資訊中

ByFormSchema.cs
//客製資訊
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),
};

設定資料庫連線字串

appsettings.json
{
  "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": ""
  }
}

填寫表單欄位資料

ByFormSchema.cs
//建立 表單欄位物件,填寫表單欄位資料
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,
        }
    }
};

呼叫站台進行起單

Program.cs
// 使用 表單專屬檔案 產生內容
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 資料庫

SDKController.cs
[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);
    }
}
ERPService.cs
/**
* 更新簽核資料
* @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

ERPController.cs
// 更新採購單狀態
[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 });
    }
}
單據結果 UOF X 會回傳 ApproveRejectCancel 三種狀態分別為通過、否決、作廢,我們可以置換成 ERP 系統的狀態文字
ERPService.cs
/**
* 更新採購單狀態
* @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:狀態、文字、設計表單時再指定

Image open-form-manager

外部資料來源設定

D. 結案後流程新增外部程式站點

可參考 外部程式站點

選擇資料來源選擇剛剛新增的外部資料來源,並將參數來源設定為採購單號及單據結果

Image open-form-manager

外部程式站點設定

Image open-form-manager

結案後流程

成果展示

執行此範例程式時,會先呼叫 UOF X 站台進行起單,並啟動 API 服務,提供 Callback、更新採購單狀態等功能做呼叫

Image open-form-manager

Console

Image open-form-manager

可到 UOF X站台查看此張表單

Image open-form-manager

表單編號在呼叫 Callback 時寫入 ERP 資料庫

Image open-form-manager

從 UOF X 可看到此張表單單據結果,並且執行外部程式站點的結果

Image open-form-manager

單據結果已回寫到 ERP 資料庫

簽核中修改採購單資訊

若在簽核中需要修改採購單資訊,請參考欄位設定與權限來設定需修改的欄位

非 .NET 平台的解決方案

若開發環境非 .NET,但仍需使用 SDK 提供的外部起單功能,可考慮將 .NET SDK 包裝成 Web API。透過此方式,其他非 .NET 平台也能透過 HTTP 請求存取該 API,實現跨平台的外部起單功能。