using Newtonsoft.Json; using NPOI.SS.Formula.Functions; using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Linq; using WebLib; namespace Education { /// /// 領據匯入類別 /// public class 領據匯入 : ImportBase { public 領據匯入() { // 定義欄位 columns.Add(new ImportColumn("活動日期", 12, required: true)); columns.Add(new ImportColumn("身分別", 10, required: true)); columns.Add(new ImportColumn("領款人姓名", 10, required: true)); columns.Add(new ImportColumn("身分證字號", 15)); columns.Add(new ImportColumn("憑證類別", 10, required: true)); columns.Add(new ImportColumn("計畫代號", 15, required: true)); columns.Add(new ImportColumn("活動地點", 20)); columns.Add(new ImportColumn("活動類別", 10)); columns.Add(new ImportColumn("付款方式", 10)); columns.Add(new ImportColumn("活動名稱/事由", 30, fieldName: "活動名稱事由")); columns.Add(new ImportColumn("勞務費費用別", 15)); columns.Add(new ImportColumn("扣繳類別", 15)); columns.Add(new ImportColumn("單價", 10)); columns.Add(new ImportColumn("數量", 10)); columns.Add(new ImportColumn("單位", 10)); columns.Add(new ImportColumn("服務費", 10)); columns.Add(new ImportColumn("差旅票價資訊", 15, fieldName: "票價資訊")); columns.Add(new ImportColumn("起點", 15)); columns.Add(new ImportColumn("訖點", 15)); columns.Add(new ImportColumn("捷運/公車", 10, fieldName: "捷運公車")); columns.Add(new ImportColumn("火車", 10)); columns.Add(new ImportColumn("計程車", 10)); columns.Add(new ImportColumn("高鐵/飛機", 10, fieldName: "高鐵飛機")); columns.Add(new ImportColumn("自行開車", 10)); columns.Add(new ImportColumn("過路費/停車費", 10, fieldName: "過路費停車費")); columns.Add(new ImportColumn("住宿費", 10)); } /// /// 進行額外的資料驗證(欄位內容檢查) /// public List ValidateContent(string filePath) { List errors = new List(); var workbook = LoadWorkbook(filePath); var sheet = workbook.GetSheetAt(0); // 從標頭列建立欄位名稱 → 欄位索引對照表 var columnMap = new Dictionary(); var headerRow = sheet.GetRow(0); if (headerRow != null) { for (int i = 0; i <= headerRow.LastCellNum; i++) { var cell = headerRow.GetCell(i); if (cell == null) continue; string title = cell.ToString().Trim(); if (!string.IsNullOrEmpty(title) && !columnMap.ContainsKey(title)) columnMap[title] = i; } } // 差旅費數值欄位清單 var travelFeeColumns = new List { "捷運/公車", "火車", "計程車", "高鐵/飛機", "自行開車", "過路費/停車費", "住宿費" }; var dac講師 = new DAC_講師(connection); var dac非講師 = new DAC_非講師(connection); for (int rowId = 1; rowId <= sheet.LastRowNum; rowId++) { var row = sheet.GetRow(rowId); if (row == null) break; // 以欄位名稱取得每欄的值 var cellValues = new Dictionary(); foreach (var column in columns) { string cellValue = ""; if (columnMap.ContainsKey(column.Title)) { var cell = row.GetCell(columnMap[column.Title]); cellValue = cell == null ? "" : excel.CellValueToString(cell); } cellValues[column.Title] = cellValue; } // 若所有欄位皆為空則停止 bool allEmpty = true; foreach (var v in cellValues.Values) { if (!string.IsNullOrEmpty(v)) { allEmpty = false; break; } } if (allEmpty) break; // 驗證活動日期 if (!string.IsNullOrEmpty(cellValues["活動日期"])) { // 日期要能接受兩種:字串與 Excel 裡日期 if (!領據匯入驗證.IsValidDate(cellValues["活動日期"])) { errors.Add($"第 {rowId + 1} 列,欄位「活動日期」:日期格式不正確,值為「{cellValues["活動日期"]}」"); } } else { errors.Add($"第 {rowId + 1} 列,欄位「活動日期」:必填欄位不可為空"); } // 驗證身分別 if (string.IsNullOrEmpty(cellValues["身分別"])) { errors.Add($"第 {rowId + 1} 列,欄位「身分別」:必填欄位不可為空"); } else if (cellValues["身分別"] != "講師" && cellValues["身分別"] != "非講師") { errors.Add($"第 {rowId + 1} 列,欄位「身分別」:只允許「講師」或「非講師」,實際值為「{cellValues["身分別"]}」"); } // 驗證領款人姓名 if (string.IsNullOrEmpty(cellValues["領款人姓名"])) { errors.Add($"第 {rowId + 1} 列,欄位「領款人姓名」:必填欄位不可為空"); } else { var 講師_非講師編號 = string.Empty; if (cellValues["身分別"] == "講師") { var t = dac講師.Select(false, cellValues["領款人姓名"], null, null) .Where(x => string.Compare(x.姓名, cellValues["領款人姓名"]) == 0 && string.Compare(x.身份證字號, cellValues["身分證字號"]) == 0) .FirstOrDefault(); if (t != null) 講師_非講師編號 = t.編號.ToString(); } else if (cellValues["身分別"] == "非講師") { var t = dac非講師.Select(false, null, cellValues["領款人姓名"], null, null, null) .Where(x => string.Compare(x.姓名, cellValues["領款人姓名"]) == 0 && string.Compare(x.身分證字號, cellValues["身分證字號"]) == 0) .FirstOrDefault(); if (t != null) 講師_非講師編號 = t.編號; } if (講師_非講師編號 == string.Empty) { errors.Add($"第 {rowId + 1} 列,欄位「領款人姓名」:不存在於系統內"); } } // 驗證憑證類別 if (string.IsNullOrEmpty(cellValues["憑證類別"])) { errors.Add($"第 {rowId + 1} 列,欄位「憑證類別」:必填欄位不可為空"); } else if (!領據匯入驗證.IsValidProofType(cellValues["憑證類別"])) { errors.Add($"第 {rowId + 1} 列,欄位「憑證類別」:無效的值「{cellValues["憑證類別"]}」,允許值為「領據」、「發票」、「收據」"); } // 驗證計畫代號 if (string.IsNullOrEmpty(cellValues["計畫代號"])) { errors.Add($"第 {rowId + 1} 列,欄位「計畫代號」:必填欄位不可為空"); } else { var planCode = cellValues["計畫代號"]; bool isTYCode = System.Text.RegularExpressions.Regex.IsMatch(planCode, @"^TY\d{4}$"); if (!isTYCode && !領據匯入驗證.IsValidProjectCode(planCode)) { errors.Add($"第 {rowId + 1} 列,欄位「計畫代號」:計畫代號「{planCode}」不存在於系統中"); } } // 驗證付款方式(選填) if (!string.IsNullOrEmpty(cellValues["付款方式"]) && !領據匯入驗證.IsValidPaymentMethod(cellValues["付款方式"])) { errors.Add($"第 {rowId + 1} 列,欄位「付款方式」:無效的值「{cellValues["付款方式"]}」,允許值為「匯款」、「支票」、「現金」"); } // 驗證勞務費費用別(選填) if (!string.IsNullOrEmpty(cellValues["勞務費費用別"]) && !領據匯入驗證.IsValidLaborExpenseType(cellValues["勞務費費用別"])) { errors.Add($"第 {rowId + 1} 列,欄位「勞務費費用別」:無效的值「{cellValues["勞務費費用別"]}」"); } // 驗證扣繳類別(選填) if (!string.IsNullOrEmpty(cellValues["扣繳類別"])) { cellValues["扣繳類別"] = 扣繳類別.MapValue(cellValues["扣繳類別"]); if (!領據匯入驗證.IsValidWithholdingCategory(cellValues["扣繳類別"])) { errors.Add($"第 {rowId + 1} 列,欄位「扣繳類別」:無效的值「{cellValues["扣繳類別"]}」"); } } // 驗證扣繳類別於費用別的對應關係 if (cellValues["勞務費費用別"] == "演講費" || cellValues["勞務費費用別"] == "撰稿費" && cellValues["扣繳類別"] == "50") { errors.Add($"第 {rowId + 1} 列,欄位「扣繳類別」:扣繳類別只能是執行業務所得"); } // 驗證單價、數量、服務費(選填) int 單價 = 0; decimal 數量 = 0; bool 有勞務費金額 = false; if (!string.IsNullOrEmpty(cellValues["單價"])) { if (!領據匯入驗證.IsValidDecimal(cellValues["單價"])) errors.Add($"第 {rowId + 1} 列,欄位「單價」:應為數值,實際值為「{cellValues["單價"]}」"); else { 單價 = DAC.GetInt32(cellValues["單價"]); 有勞務費金額 = true; } } if (!string.IsNullOrEmpty(cellValues["數量"])) { if (!領據匯入驗證.IsValidDecimal(cellValues["數量"])) errors.Add($"第 {rowId + 1} 列,欄位「數量」:應為數值,實際值為「{cellValues["數量"]}」"); else { 數量 = DAC.GetDecimal(cellValues["數量"]); 有勞務費金額 = true; } } if (!string.IsNullOrEmpty(cellValues["服務費"])) { if (!領據匯入驗證.IsValidDecimal(cellValues["服務費"])) errors.Add($"第 {rowId + 1} 列,欄位「服務費」:應為數值,實際值為「{cellValues["服務費"]}」"); else 有勞務費金額 = true; } // 有勞務費金額但費用別或單位未填 if (有勞務費金額 && string.IsNullOrEmpty(cellValues["勞務費費用別"])) errors.Add($"第 {rowId + 1} 列,欄位「勞務費費用別」:已輸入金額,此欄位不可為空"); if (有勞務費金額 && string.IsNullOrEmpty(cellValues["單位"])) errors.Add($"第 {rowId + 1} 列,欄位「單位」:已輸入金額,此欄位不可為空"); // 合計金額驗證 int 合計金額 = (int)((decimal)單價 * 數量); if (cellValues["扣繳類別"] == "50" && 合計金額 >= PublicVariable.領據基本工資) { errors.Add($"第 {rowId + 1} 列:薪資不得超過基本工資 {PublicVariable.領據基本工資}(含),超過請拆成不同日期的領據"); } if ((cellValues["扣繳類別"].StartsWith("9A") || cellValues["扣繳類別"].StartsWith("9B")) && 合計金額 >= PublicVariable.領據執行業務所得課稅門檻) { errors.Add($"第 {rowId + 1} 列:執行業務所得不得超過 {PublicVariable.領據執行業務所得課稅門檻}(含),超過請拆成不同日期的領據"); } // 驗證差旅費的數值欄位 foreach (var colTitle in travelFeeColumns) { if (cellValues.ContainsKey(colTitle) && !string.IsNullOrEmpty(cellValues[colTitle]) && !領據匯入驗證.IsValidDecimal(cellValues[colTitle])) { errors.Add($"第 {rowId + 1} 列,欄位「{colTitle}」:應為數值,實際值為「{cellValues[colTitle]}」"); } } } return errors; } /// /// 驗證重複資料 /// - 同一檔案內重複(允許匯入,但回傳警告) /// - 本次匯入資料與資料庫現有資料重複的總筆數超過 5 筆(不允許匯入,回傳錯誤) /// 重複條件:活動日期 + 領款人姓名 + 計畫代號 + 活動名稱/事由 /// public void ValidateDuplicates(string filePath, DbConnection conn, out List errors, out List warnings) { errors = new List(); warnings = new List(); var workbook = LoadWorkbook(filePath); var sheet = workbook.GetSheetAt(0); // 建立欄位名稱 → 欄位索引對照表 var columnMap = new Dictionary(); var headerRow = sheet.GetRow(0); if (headerRow != null) { for (int i = 0; i <= headerRow.LastCellNum; i++) { var cell = headerRow.GetCell(i); if (cell == null) continue; string title = cell.ToString().Trim(); if (!string.IsNullOrEmpty(title) && !columnMap.ContainsKey(title)) columnMap[title] = i; } } // 收集每一列的鍵值:key → List Excel 顯示列號 var fileKeyRows = new Dictionary>(); for (int rowId = 1; rowId <= sheet.LastRowNum; rowId++) { var row = sheet.GetRow(rowId); if (row == null) break; string GetCellValue(string colTitle) { if (!columnMap.ContainsKey(colTitle)) return ""; var c = row.GetCell(columnMap[colTitle]); return c == null ? "" : excel.CellValueToString(c).Trim(); } string 活動日期Raw = GetCellValue("活動日期"); string 領款人姓名 = GetCellValue("領款人姓名"); string 計畫代號 = GetCellValue("計畫代號"); string 活動名稱事由 = GetCellValue("活動名稱/事由"); // 若整列為空則停止 if (string.IsNullOrEmpty(活動日期Raw) && string.IsNullOrEmpty(領款人姓名) && string.IsNullOrEmpty(計畫代號)) break; // 重複鍵需要四個欄位都有值才做比對 if (string.IsNullOrEmpty(活動日期Raw) || string.IsNullOrEmpty(領款人姓名) || string.IsNullOrEmpty(計畫代號)) continue; string key = string.Format("{0}|{1}|{2}|{3}", 活動日期Raw, 領款人姓名, 計畫代號, 活動名稱事由); if (!fileKeyRows.ContainsKey(key)) fileKeyRows[key] = new List(); fileKeyRows[key].Add(rowId + 1); // +1 因為 rowId=1 對應 Excel 第 2 列(含標頭) } // ── 1. 檔案內重複(警告,不阻擋)──────────────────────────────── foreach (var kv in fileKeyRows) { if (kv.Value.Count <= 1) continue; string[] parts = kv.Key.Split('|'); warnings.Add(string.Format( "檔案內重複:活動日期「{0}」、領款人「{1}」、計畫代號「{2}」、活動名稱事由「{3}」出現於第 {4} 列(共 {5} 筆)", parts[0], parts[1], parts[2], parts[3], string.Join("、", kv.Value.ConvertAll(r => r.ToString()).ToArray()), kv.Value.Count)); } // ── 2. 與資料庫現有資料比對 ────────────────────────────────────── // 每個唯一鍵查一次 DB,累計所有重複筆數;每筆有重複的都顯示給使用者 var dac = new DAC_領據(conn); int totalDbDuplicates = 0; var dbDuplicateMessages = new List(); foreach (var kv in fileKeyRows) { string[] parts = kv.Key.Split('|'); if (!DateTime.TryParse(parts[0], out DateTime 活動日期)) continue; string 領款人姓名DB = parts[1]; string 計畫代號DB = parts[2]; string 活動名稱事由DB = parts[3]; int dbCount = dac.CountDuplicates(活動日期, 領款人姓名DB, 計畫代號DB, 活動名稱事由DB); if (dbCount <= 0) continue; totalDbDuplicates += dbCount; dbDuplicateMessages.Add(string.Format( "與現有資料重複:活動日期「{0}」、領款人「{1}」、計畫代號「{2}」、活動名稱事由「{3}」已有 {4} 筆", parts[0], 領款人姓名DB, 計畫代號DB, 活動名稱事由DB, dbCount)); } // 不論是否超過 5 筆,有重複的都要顯示;超過 5 筆則放入 errors 阻擋匯入 if (dbDuplicateMessages.Count > 0) { if (totalDbDuplicates > 5) { errors.Add(string.Format("與現有資料重複共 {0} 筆(超過 5 筆),不允許匯入:", totalDbDuplicates)); errors.AddRange(dbDuplicateMessages); } else { warnings.Add(string.Format("與現有資料重複共 {0} 筆:", totalDbDuplicates)); warnings.AddRange(dbDuplicateMessages); } } } public override void InsertOne(T item) { var row = item as 領據匯入Item; if (row == null) return; using (var ts = DAC.NewTransactionScope()) { try { // 1. 插入領據資料 var dac領據 = new DAC_領據(connection); var dac講師 = new DAC_講師(connection); var dac非講師 = new DAC_非講師(connection); var dacBPAA = new DAC_BPAA(); var 領據Item = new 領據Item(); var 講師_非講師編號 = string.Empty; if (row.身分別 == "講師") { var t = dac講師.Select(false, row.領款人姓名, null, null) .Where(x => string.Compare(x.姓名, row.領款人姓名) == 0 && string.Compare(x.身份證字號, row.身分證字號) == 0) .FirstOrDefault(); if (t != null) 講師_非講師編號 = t.編號.ToString(); } else if (row.身分別 == "非講師") { var t = dac非講師.Select(false, null, row.領款人姓名, null, null, null) .Where(x => string.Compare(x.姓名, row.領款人姓名) == 0 && string.Compare(x.身分證字號, row.身分證字號) == 0) .FirstOrDefault(); if (t != null) 講師_非講師編號 = t.編號; } if (講師_非講師編號 == string.Empty) { throw new Exception($"插入資料時發生錯誤: {row.領款人姓名} 不存在於系統內"); } var dataTable = dacBPAA.GetBpaaWithTYPlusNone(); foreach (DataRow r in dataTable.Rows) { if (r["BPPYN"].ToString() == row.計畫代號) { 領據Item.計畫名稱 = DAC.GetString(r["BPPYM"]); } } 領據Item.活動日期 = row.活動日期.Value; 領據Item.身分別 = row.身分別; 領據Item.講師_非講師編號 = 講師_非講師編號; 領據Item.領款人姓名 = row.領款人姓名; 領據Item.身分證字號 = row.身分證字號; 領據Item.憑證類別 = row.憑證類別; 領據Item.計畫代號 = row.計畫代號; 領據Item.活動地點 = row.活動地點; 領據Item.活動類別 = row.活動類別; 領據Item.活動名稱事由 = row.活動名稱事由; 領據Item.付款方式 = row.付款方式; 領據Item.製單人員 = dac領據.CurrentUserId; dac領據.InsertOne(領據Item); int 領據編號 = 領據Item.編號; // 2. 如果有勞務費資料,插入勞務費表 if (!string.IsNullOrEmpty(row.勞務費費用別) || row.單價.HasValue || row.數量.HasValue || row.服務費.HasValue) { var dac勞務費 = new DAC_勞務費(connection); var 勞務費Item = new 勞務費Item(); var a扣繳類別 = 扣繳類別.MapValue(row.扣繳類別); var 扣繳稅額 = 0; var 扣繳稅額result = new Services.領據處理().CalcTax((int)((decimal)row.單價 * row.數量), a扣繳類別); if (扣繳稅額result.Success) { 扣繳稅額 = DAC.GetInt32(扣繳稅額result.Data); } 勞務費Item.領據編號 = 領據編號; 勞務費Item.費用別 = row.勞務費費用別; 勞務費Item.扣繳類別 = a扣繳類別; 勞務費Item.單價 = row.單價.HasValue ? row.單價.Value : 0; 勞務費Item.數量 = row.數量.HasValue ? row.數量.Value : 0; 勞務費Item.單位 = row.單位; 勞務費Item.服務費 = row.服務費.HasValue ? row.服務費.Value : 0; 勞務費Item.應扣繳稅額 = 扣繳稅額; dac勞務費.InsertOne(勞務費Item); } // 3. 如果有差旅費資料,插入差旅費表 if (!string.IsNullOrEmpty(row.差旅票價資訊) || row.捷運公車.HasValue || row.火車.HasValue || row.計程車.HasValue || row.高鐵飛機.HasValue || row.自行開車.HasValue || row.過路費停車費.HasValue || row.住宿費.HasValue) { var dac差旅費 = new DAC_差旅費(connection); var 差旅費Item = new 差旅費Item(); 差旅費Item.領據編號 = 領據編號; 差旅費Item.費用別 = 差旅費_費用別.交通住宿費; 差旅費Item.票價資訊 = row.差旅票價資訊; 差旅費Item.起點 = row.起點; 差旅費Item.訖點 = row.訖點; 差旅費Item.捷運公車 = row.捷運公車.HasValue ? row.捷運公車.Value : 0; 差旅費Item.火車 = row.火車.HasValue ? row.火車.Value : 0; 差旅費Item.計程車 = row.計程車.HasValue ? row.計程車.Value : 0; 差旅費Item.高鐵飛機 = row.高鐵飛機.HasValue ? row.高鐵飛機.Value : 0; 差旅費Item.自行開車 = row.自行開車.HasValue ? row.自行開車.Value : 0; 差旅費Item.過路費停車費 = row.過路費停車費.HasValue ? row.過路費停車費.Value : 0; 差旅費Item.住宿費 = row.住宿費.HasValue ? row.住宿費.Value : 0; dac差旅費.InsertOne(差旅費Item); } ts.Complete(); } catch (Exception ex) { throw new Exception($"插入資料時發生錯誤: {ex.Message}", ex); } } } public override string ImportRowErrorMessage(T item, string errorMessage) { var row = item as 領據匯入Item; if (row != null) { return $"領款人:{row.領款人姓名},憑證類別:{row.憑證類別},計畫代號:{row.計畫代號},錯誤原因:{errorMessage}"; } else { return base.ImportRowErrorMessage(item, errorMessage); } } } }