PayrollComputation_v1.cs 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132
  1. using MathNet.Numerics.LinearAlgebra.Factorization;
  2. using Newtonsoft.Json.Serialization;
  3. using NodaTime;
  4. using OASystem.API.OAMethodLib.QiYeWeChatAPI;
  5. using OASystem.Domain.Entities.PersonnelModule;
  6. using OASystem.Domain.ViewModels.PersonnelModule;
  7. using OASystem.Domain.ViewModels.QiYeWeChat;
  8. using System.Text.Json.Serialization;
  9. namespace OASystem.API.OAMethodLib;
  10. /// <summary>
  11. /// 工资计算公共参数配置
  12. /// </summary>
  13. public static class PayrollConfig
  14. {
  15. // ========== 基础参数 ==========
  16. public const decimal ChengduMinimumWage = 2100m;
  17. public const decimal SickLeaveWageRatio = 0.8m;
  18. public const decimal WorkHoursPerDay = 7.5m;
  19. public const decimal MealSubsidyPerDay = 10m;
  20. // ========== 迟到早退规则 ==========
  21. /// <summary>
  22. /// 累计免罚分钟(超过此值才罚款)
  23. /// </summary>
  24. public const int FreeTotalMinutes = 10;
  25. /// <summary>
  26. /// 每次事件罚款金额
  27. /// </summary>
  28. public const decimal FinePerEvent = 50m;
  29. public const int WorkStartHour = 9;
  30. public const int WorkEndHour = 18;
  31. /// <summary>
  32. /// 默认允许迟到/早退秒数(当规则未设置时使用)
  33. /// </summary>
  34. public const int DefaultAllowSeconds = 300;
  35. // ========== 旷工判定 ==========
  36. /// <summary>
  37. /// 半日旷工下限(分钟)
  38. /// </summary>
  39. public const int HalfDayMissMinutes = 60;
  40. /// <summary>
  41. /// 全日旷工阈值(分钟)
  42. /// </summary>
  43. public const int FullDayMissMinutes = 180;
  44. // ========== 补卡规则 ==========
  45. public const int ProbationFreeCardCount = 2;
  46. public const int RegularFreeCardCount = 3;
  47. public const decimal PunchCorrectionLowFine = 10m;
  48. public const bool PunchCorrectionToAbsentAfterFourth = true;
  49. // ========== 请假餐补规则 ==========
  50. public const int MealDeductionLeaveHours = 3;
  51. // ========== 加班餐补 ==========
  52. /// <summary>
  53. /// 加班餐补金额(元/天)
  54. /// </summary>
  55. public const decimal OvertimeMealSubsidy = 12m;
  56. /// <summary>
  57. /// 加班起始小时(20点)
  58. /// </summary>
  59. public const int OvertimeStartHour = 20;
  60. /// <summary>
  61. /// 加班起始分钟
  62. /// </summary>
  63. public const int OvertimeStartMinute = 0;
  64. /// <summary>公司打卡地址白名单(根据实际设备ID填写)</summary>
  65. public static readonly List<string> CompanyLocations = new()
  66. {
  67. "ZK-T1",
  68. "蓝牙考勤设备",
  69. "武侯区成都银泰中心in99(名都路西)"
  70. };
  71. /// <summary>公司WiFi名称白名单</summary>
  72. public static readonly List<string> CompanyWifiNames = new()
  73. {
  74. "Pan-American",
  75. };
  76. }
  77. /// <summary>
  78. /// 工资计算
  79. /// </summary>
  80. public static class PayrollComputation_v1
  81. {
  82. private static Result _result = new();
  83. private static readonly IQiYeWeChatApiService _qiYeWeChatApiService = AutofacIocManager.Instance.GetService<IQiYeWeChatApiService>();
  84. private static readonly SqlSugarClient _sqlSugar = AutofacIocManager.Instance.GetService<SqlSugarClient>();
  85. /// <summary>
  86. /// 计算工资
  87. /// </summary>
  88. public static async Task<Result> SalaryCalculatorAsync(
  89. List<Pm_WageSheet> pm_WageSheetDattaSources, int userId, string thisYearMonth, DateTime startDt, DateTime endDt)
  90. {
  91. if (pm_WageSheetDattaSources.Count <= 0)
  92. {
  93. _result.Msg = "计算工资传入数据为空!";
  94. return _result;
  95. }
  96. // 获取当月应出勤天数
  97. int workDays = await GetWorkDays(thisYearMonth);
  98. if (workDays <= 0)
  99. {
  100. _result.Msg = $"{thisYearMonth} 工作日未设置,请前往《工作日管理页面》设置!";
  101. return _result;
  102. }
  103. var userIds = pm_WageSheetDattaSources.Select(ws => ws.UserId).Distinct().ToList();
  104. var userIdInfos = await GetSysUserWeChatIds(userIds);
  105. if (!userIdInfos.Any())
  106. {
  107. _result.Msg = $"企业微信Id获取失败,请联系管理员!";
  108. return _result;
  109. }
  110. List<string> qyWhchatIdList = userIdInfos.Select(it => it.WeChatId ?? "").Distinct().ToList();
  111. // 获取打卡日报数据(包含 exception_infos)
  112. CheckInDayDataView checkInDayDataView = await _qiYeWeChatApiService.GetCheckInDayDataAsync(qyWhchatIdList, startDt, endDt);
  113. if (checkInDayDataView.errcode != 0)
  114. {
  115. _result.Msg = $"【企业微信】【打卡】【获取时间段内所有日打卡】【Msg】{checkInDayDataView.errmsg}";
  116. return _result;
  117. }
  118. // 获取系统日历(用于判断工作日)
  119. List<Sys_Calendar> sys_Calendars = await GetSysCalendars(startDt, endDt);
  120. string _name = "";
  121. try
  122. {
  123. foreach (var pm_wsInfo in pm_WageSheetDattaSources)
  124. {
  125. _name = userIdInfos.Find(it => it.Id == pm_wsInfo.UserId)?.CnName ?? "Unknown";
  126. // 获取员工当月的打卡日报数据(按日期分组)
  127. var userDailyRecords = GetUserDailyRecords(checkInDayDataView, _name);
  128. // 计算日薪
  129. decimal amountPayable = GetTotalSalaryBase(pm_wsInfo);
  130. decimal dailyWage = ConvertToDecimal(amountPayable / workDays);
  131. decimal sickLeaveDailyWage = ConvertToDecimal((PayrollConfig.ChengduMinimumWage * PayrollConfig.SickLeaveWageRatio) / workDays);
  132. // 获取员工企业微信ID(用于获取审批数据)
  133. string acctid = userDailyRecords.Count > 0 ? userDailyRecords.First().Value.base_info.acctid : "";
  134. // 计算考勤扣款(使用日报中的 exception_infos)
  135. var attendanceResult = CalculateAttendanceFromDailyRecords(userDailyRecords, dailyWage, sys_Calendars);
  136. // 计算假勤扣款(请假、出差等)
  137. var leaveResult = new LeaveDeductionResult();
  138. // 计算补卡扣款
  139. var punchResult = new PunchCorrectionResult();
  140. // 当月假勤次数
  141. var spCount = userDailyRecords.Sum(x => x.Value.sp_items?.Sum(y => y.count) ?? 0);
  142. //当月存在假勤的情况,执行假勤扣除
  143. if (spCount > 0)
  144. {
  145. leaveResult = await CalculateLeaveDeductionAsync(acctid, dailyWage, sickLeaveDailyWage, amountPayable, workDays, startDt, endDt, sys_Calendars);
  146. punchResult = await CalculatePunchCorrectionAsync(acctid, pm_wsInfo.Floats, dailyWage, startDt, endDt);
  147. }
  148. // 计算加班餐补明细(按天)
  149. var (overtimeMeal, overtimeDetails) = await CalculateOvertimeMealSubsidyWithDetailsAsync(acctid, startDt, endDt);
  150. // 原有餐补计算
  151. int actualWorkDays = userDailyRecords.Count(r => IsWorkDayForRule(r.Key, sys_Calendars));
  152. decimal mealSubsidy = actualWorkDays * PayrollConfig.MealSubsidyPerDay;
  153. decimal mealDeductionTotal = attendanceResult.MealDeduction + leaveResult.MealDeduction + punchResult.MealDeduction;
  154. decimal mealActual = mealSubsidy - mealDeductionTotal + overtimeMeal; // 加班餐补累加到餐补总额
  155. // 汇总扣款
  156. decimal totalDeduction = attendanceResult.TotalDeduction + leaveResult.TotalDeduction + punchResult.TotalDeduction
  157. + pm_wsInfo.WithholdingInsurance + pm_wsInfo.ReservedFunds + pm_wsInfo.OtherDeductions;
  158. // 应发合计
  159. decimal shouldTotal = amountPayable + mealActual + pm_wsInfo.OtherHandle;
  160. decimal afterTax = Math.Floor((shouldTotal - totalDeduction - pm_wsInfo.WithholdingTax) * 100) / 100;
  161. var exItemsList = new List<ExItems>()
  162. {
  163. new(){ Type = "打卡", ExItemInfo = new List<ExItem>()},
  164. new(){ Type = "假勤", ExItemInfo = new List<ExItem>()},
  165. new(){ Type = "打卡补卡", ExItemInfo = new List<ExItem>()},
  166. };
  167. // 根据类型赋值
  168. var punchItem = exItemsList.FirstOrDefault(x => x.Type == "打卡");
  169. if (punchItem != null && attendanceResult.Details.Count > 0)
  170. punchItem.ExItemInfo = attendanceResult.Details;
  171. var leaveItem = exItemsList.FirstOrDefault(x => x.Type == "假勤");
  172. if (leaveItem != null)
  173. {
  174. var combined = new List<ExItem>();
  175. if (leaveResult.Details.Any()) combined.AddRange(leaveResult.Details);
  176. if (overtimeDetails.Any()) combined.AddRange(overtimeDetails);
  177. leaveItem.ExItemInfo = combined;
  178. }
  179. var punchCardItem = exItemsList.FirstOrDefault(x => x.Type == "打卡补卡");
  180. if (punchCardItem != null && punchResult.Details.Count > 0)
  181. punchCardItem.ExItemInfo = punchResult.Details;
  182. string exItemsRemark = JsonConvert.SerializeObject(exItemsList,
  183. new JsonSerializerSettings
  184. {
  185. DateFormatString = "yyyy-MM-dd HH:mm:ss",
  186. NullValueHandling = NullValueHandling.Ignore
  187. });
  188. // 更新工资表
  189. UpdateWageSheet(pm_wsInfo, thisYearMonth, startDt, endDt, workDays, actualWorkDays,
  190. mealActual, leaveResult, attendanceResult, punchResult, shouldTotal, totalDeduction, afterTax, userId, exItemsRemark);
  191. }
  192. }
  193. catch (Exception ex)
  194. {
  195. _result.Msg = $"【{_name}】【Msg:{ex.Message}】";
  196. return _result;
  197. }
  198. _result.Code = 0;
  199. _result.Data = pm_WageSheetDattaSources;
  200. return _result;
  201. }
  202. #region 内部类定义
  203. /// <summary>
  204. /// 解析后的打卡规则(仅用于当前计算,不缓存)
  205. /// </summary>
  206. private class ParsedCheckInRule
  207. {
  208. public uint GroupId { get; set; }
  209. public int FlexOnDutyTime { get; set; } = 0; // 允许迟到秒数
  210. public int FlexOffDutyTime { get; set; } = 0; // 允许早退秒数
  211. public HashSet<DateTime> SpecialWorkDays { get; set; } = new HashSet<DateTime>();
  212. public HashSet<DateTime> SpecialOffDays { get; set; } = new HashSet<DateTime>();
  213. }
  214. /// <summary>
  215. /// 考勤扣款结果
  216. /// </summary>
  217. private class AttendancePenaltyResult
  218. {
  219. public decimal LateFine { get; set; }
  220. public decimal EarlyFine { get; set; }
  221. public decimal AbsenteeismDeduction { get; set; }
  222. public decimal MealDeduction { get; set; }
  223. public decimal TotalDeduction => LateFine + EarlyFine + AbsenteeismDeduction;
  224. public List<ExItem> Details { get; set; } = new List<ExItem>();
  225. }
  226. /// <summary>
  227. /// 假勤扣款结果
  228. /// </summary>
  229. private class LeaveDeductionResult
  230. {
  231. public decimal PersonalLeaveTotal { get; set; }
  232. public decimal SickLeaveTotal { get; set; }
  233. public decimal MealDeduction { get; set; }
  234. public decimal TotalDeduction => PersonalLeaveTotal + SickLeaveTotal;
  235. public List<ExItem> Details { get; set; } = new List<ExItem>();
  236. }
  237. /// <summary>
  238. /// 补卡扣款结果
  239. /// </summary>
  240. private class PunchCorrectionResult
  241. {
  242. public decimal MissPunchFine { get; set; }
  243. public decimal AbsenteeismDeduction { get; set; }
  244. public decimal MealDeduction { get; set; }
  245. public decimal TotalDeduction => MissPunchFine + AbsenteeismDeduction;
  246. public List<ExItem> Details { get; set; } = new List<ExItem>();
  247. }
  248. /// <summary>
  249. /// 扣款明细项(用于序列化)
  250. /// </summary>
  251. private class ExItem
  252. {
  253. [JsonProperty("SubTypeId")]
  254. public int SubTypeId { get; set; }
  255. [JsonProperty("SubType")]
  256. public string? SubType { get; set; }
  257. [JsonProperty("Deduction")]
  258. public decimal Deduction { get; set; } = 0.00M;
  259. [JsonProperty("MealDeduction")]
  260. public decimal MealDeduction { get; set; } = 0.00M;
  261. [JsonProperty("StartTimeDt")]
  262. public DateTime StartTimeDt { get; set; }
  263. [JsonProperty("EndTimeDt")]
  264. public DateTime EndTimeDt { get; set; }
  265. [JsonProperty("Duration")]
  266. public decimal Duration { get; set; }
  267. [JsonProperty("Unit")]
  268. public string Unit { get; set; } = "小时";
  269. [JsonProperty("Reason")]
  270. public string? Reason { get; set; }
  271. [JsonProperty("Apply_time_dt")]
  272. public DateTime Apply_time_dt { get; set; }
  273. [JsonProperty("Approval_name")]
  274. public List<string>? Approval_name { get; set; }
  275. }
  276. /// <summary>
  277. /// 扣款分类容器
  278. /// </summary>
  279. private class ExItems
  280. {
  281. [JsonProperty("Type")]
  282. public string? Type { get; set; }
  283. [JsonProperty("Ex_ItemInfo")]
  284. public object? ExItemInfo { get; set; }
  285. }
  286. /// <summary>
  287. /// 请假/调休/出差明细(用于跨月拆分)
  288. /// </summary>
  289. private class LeaveDetailItem
  290. {
  291. public int TypeId { get; set; }
  292. public string TypeName { get; set; } = "";
  293. public DateTime Start { get; set; }
  294. public DateTime End { get; set; }
  295. public decimal Days { get; set; }
  296. public decimal Hours { get; set; }
  297. public string DtType { get; set; } = "";
  298. public decimal NewDuration { get; set; }
  299. public Slice_info SliceInfo { get; set; } = new Slice_info();
  300. public DateTime ApplyDt { get; set; }
  301. public bool IsBusinessTrip { get; set; }
  302. }
  303. #endregion
  304. #region 辅助方法
  305. /// <summary>
  306. /// 加班时段(支持跨天,餐补归属开始日期)
  307. /// </summary>
  308. private class OvertimePeriodDetail
  309. {
  310. public DateTime StartDate { get; set; } // 加班开始日期(仅日期)
  311. public DateTime EndTime { get; set; } // 加班结束时间(包含时分秒,可能跨天)
  312. }
  313. /// <summary>
  314. /// 判断是否为工作日(基于规则中的特殊日期和系统日历)
  315. /// </summary>
  316. private static bool IsWorkDayForRule(DateTime date, List<Sys_Calendar> sysCalendars)
  317. {
  318. var calendar = sysCalendars.FirstOrDefault(c => c.Dt == date.ToString("yyyy-MM-dd"));
  319. if (calendar != null)
  320. return calendar.IsWorkDay;
  321. // 默认周一到周五为工作日
  322. return date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday;
  323. }
  324. private static decimal GetTotalSalaryBase(Pm_WageSheet wageSheet)
  325. {
  326. return wageSheet.Basic + wageSheet.Floats + wageSheet.PostAllowance + wageSheet.InformationSecurityFee + wageSheet.OtherSubsidies;
  327. }
  328. private static async Task<List<Sys_Calendar>> GetSysCalendars(DateTime startDt, DateTime endDt)
  329. {
  330. string sql = string.Format("Select * From Sys_Calendar Where Isdel = 0 And Dt between '{0}' And '{1}'",
  331. startDt.ToString("yyyy-MM-dd"), endDt.ToString("yyyy-MM-dd"));
  332. return await _sqlSugar.SqlQueryable<Sys_Calendar>(sql).ToListAsync();
  333. }
  334. private static async Task<List<UserWeChatIdView>> GetSysUserWeChatIds(List<int> ints)
  335. {
  336. return await _sqlSugar.Queryable<Sys_Users>().Where(u => ints.Contains(u.Id) && u.IsDel == 0)
  337. .Select(u => new UserWeChatIdView { Id = u.Id, CnName = u.CnName, WeChatId = u.QiyeChatUserId })
  338. .ToListAsync();
  339. }
  340. /// <summary>
  341. /// 获取员工每日的打卡日报数据(按日期索引)
  342. /// </summary>
  343. private static Dictionary<DateTime, CheckInDayRoot> GetUserDailyRecords(CheckInDayDataView view, string name)
  344. {
  345. var dict = new Dictionary<DateTime, CheckInDayRoot>();
  346. var allRecords = view.datas ?? new List<CheckInDayRoot>();
  347. IEnumerable<CheckInDayRoot> userRecords = allRecords.Where(r => r.base_info.name == name || r.base_info.name.Contains(name)).OrderBy(x => x.base_info.DateDt);
  348. foreach (var record in userRecords)
  349. {
  350. DateTime date = record.base_info.DateDt.Date;
  351. dict[date] = record;
  352. }
  353. return dict;
  354. }
  355. /// <summary>
  356. /// 基于日报数据中的 exception_infos 计算考勤扣款(次数累计 + 分时段罚款)
  357. /// </summary>
  358. private static AttendancePenaltyResult CalculateAttendanceFromDailyRecords(
  359. Dictionary<DateTime, CheckInDayRoot> dailyRecords,
  360. decimal dailyWage,
  361. List<Sys_Calendar> sysCalendars)
  362. {
  363. var result = new AttendancePenaltyResult();
  364. // 统计本月内迟到/早退次数(仅统计时长<60分钟的事件)
  365. int lateEarlyCount = 0; // 总次数(用于判定是否超过2次)
  366. int lateCount = 0; // 迟到次数(保留,暂未独立使用)
  367. int earlyCount = 0; // 早退次数
  368. foreach (var kvp in dailyRecords)
  369. {
  370. DateTime date = kvp.Key;
  371. var record = kvp.Value;
  372. // 判断是否为工作日(若不是工作日,不计算考勤扣款)
  373. bool isWorkDay = IsWorkDayForRule(date, sysCalendars);
  374. if (!isWorkDay) continue;
  375. var exceptions = record.exception_infos ?? new List<ExceptionInfo>();
  376. foreach (var ex in exceptions)
  377. {
  378. int exceptionType = ex.exception; // 1迟到 2早退 3缺卡 4旷工
  379. int minutes = ex.duration / 60; // 时长(分钟)
  380. string reason = $"{date:yyyy-MM-dd} ";
  381. decimal deduction = 0m;
  382. switch (exceptionType)
  383. {
  384. case 1: // 迟到
  385. if (minutes < 60)
  386. {
  387. // 迟到时长小于60分钟,计入次数,并根据分钟数确定罚款
  388. lateEarlyCount++;
  389. lateCount++;
  390. if (minutes < 10) // 不足10分钟:第3次起罚50
  391. {
  392. if (lateEarlyCount >= 3)
  393. {
  394. deduction = PayrollConfig.FinePerEvent; // 50元
  395. result.LateFine += deduction;
  396. reason += $"迟到{minutes}分钟(第{lateEarlyCount}次),按次数罚款{deduction}元";
  397. }
  398. else
  399. {
  400. reason += $"迟到{minutes}分钟(第{lateEarlyCount}次),前2次不处罚";
  401. }
  402. }
  403. else if (minutes >= 10 && minutes < 30) // 9:10~9:30 => 罚款20
  404. {
  405. deduction = 20m;
  406. result.LateFine += deduction;
  407. reason += $"迟到{minutes}分钟,罚款{deduction}元";
  408. }
  409. else if (minutes >= 30 && minutes < 60) // 9:30~9:59 => 罚款50
  410. {
  411. deduction = PayrollConfig.FinePerEvent; // 50元
  412. result.LateFine += deduction;
  413. reason += $"迟到{minutes}分钟,罚款{deduction}元";
  414. }
  415. result.Details.Add(new ExItem
  416. {
  417. SubTypeId = 1,
  418. SubType = "迟到",
  419. Deduction = deduction,
  420. Duration = minutes,
  421. Unit = "分钟",
  422. StartTimeDt = date,
  423. Reason = reason
  424. });
  425. }
  426. else
  427. {
  428. // 迟到≥60分钟,按旷工处理
  429. if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes)
  430. {
  431. decimal ded = ConvertToDecimal(dailyWage / 2);
  432. result.AbsenteeismDeduction += ded;
  433. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  434. result.Details.Add(new ExItem
  435. {
  436. SubTypeId = 4,
  437. SubType = "旷工(迟到超时)",
  438. Deduction = ded,
  439. Duration = minutes,
  440. Unit = "分钟",
  441. StartTimeDt = date,
  442. Reason = reason + $"迟到{minutes}分钟,按旷工半天处理,扣除餐补"
  443. });
  444. }
  445. else // minutes > 180
  446. {
  447. decimal ded = ConvertToDecimal(dailyWage);
  448. result.AbsenteeismDeduction += ded;
  449. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  450. result.Details.Add(new ExItem
  451. {
  452. SubTypeId = 4,
  453. SubType = "旷工(迟到超时)",
  454. Deduction = ded,
  455. Duration = minutes,
  456. Unit = "分钟",
  457. StartTimeDt = date,
  458. Reason = reason + $"迟到{minutes}分钟,按旷工全天处理,扣除餐补"
  459. });
  460. }
  461. }
  462. break;
  463. case 2: // 早退
  464. if (minutes < 60)
  465. {
  466. lateEarlyCount++;
  467. earlyCount++;
  468. if (minutes < 10)
  469. {
  470. if (lateEarlyCount >= 3)
  471. {
  472. deduction = PayrollConfig.FinePerEvent;
  473. result.EarlyFine += deduction;
  474. reason += $"早退{minutes}分钟(第{lateEarlyCount}次),按次数罚款{deduction}元";
  475. }
  476. else
  477. {
  478. reason += $"早退{minutes}分钟(第{lateEarlyCount}次),前2次不处罚";
  479. }
  480. }
  481. else if (minutes >= 10 && minutes < 30)
  482. {
  483. deduction = 20m;
  484. result.EarlyFine += deduction;
  485. reason += $"早退{minutes}分钟,罚款{deduction}元";
  486. }
  487. else if (minutes >= 30 && minutes < 60)
  488. {
  489. deduction = PayrollConfig.FinePerEvent; // 50元
  490. result.EarlyFine += deduction;
  491. reason += $"早退{minutes}分钟,罚款{deduction}元";
  492. }
  493. result.Details.Add(new ExItem
  494. {
  495. SubTypeId = 2,
  496. SubType = "早退",
  497. Deduction = deduction,
  498. Duration = minutes,
  499. Unit = "分钟",
  500. StartTimeDt = date,
  501. Reason = reason
  502. });
  503. }
  504. else
  505. {
  506. // 早退≥60分钟,按旷工处理
  507. if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes)
  508. {
  509. decimal ded = ConvertToDecimal(dailyWage / 2);
  510. result.AbsenteeismDeduction += ded;
  511. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  512. result.Details.Add(new ExItem
  513. {
  514. SubTypeId = 4,
  515. SubType = "旷工",
  516. Deduction = ded,
  517. StartTimeDt = date,
  518. Duration = minutes,
  519. Unit = "分钟",
  520. Reason = reason + $"早退{minutes}分钟,按旷工半天处理,扣除餐补"
  521. });
  522. }
  523. else
  524. {
  525. decimal ded = ConvertToDecimal(dailyWage);
  526. result.AbsenteeismDeduction += ded;
  527. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  528. result.Details.Add(new ExItem
  529. {
  530. SubTypeId = 4,
  531. SubType = "旷工",
  532. Deduction = ded,
  533. StartTimeDt = date,
  534. Duration = minutes,
  535. Unit = "分钟",
  536. Reason = reason + $"早退{minutes}分钟,按旷工全天处理,扣除餐补"
  537. });
  538. }
  539. }
  540. break;
  541. // 缺卡、旷工处理保持不变
  542. case 3:
  543. {
  544. decimal ded = ConvertToDecimal(dailyWage / 2);
  545. result.AbsenteeismDeduction += ded;
  546. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  547. result.Details.Add(new ExItem
  548. {
  549. SubTypeId = 4,
  550. SubType = "旷工",
  551. Deduction = ded,
  552. StartTimeDt = date,
  553. Duration = 0.5m,
  554. Unit = "天",
  555. Reason = reason + "缺卡,按半天旷工处理,扣除餐补"
  556. });
  557. }
  558. break;
  559. case 4:
  560. if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes)
  561. {
  562. decimal ded = ConvertToDecimal(dailyWage / 2);
  563. result.AbsenteeismDeduction += ded;
  564. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  565. result.Details.Add(new ExItem
  566. {
  567. SubTypeId = 4,
  568. SubType = "旷工",
  569. Deduction = ded,
  570. StartTimeDt = date,
  571. Duration = 0.5m,
  572. Unit = "天",
  573. Reason = reason + "旷工半天,扣除餐补"
  574. });
  575. }
  576. else
  577. {
  578. decimal ded = ConvertToDecimal(dailyWage);
  579. result.AbsenteeismDeduction += ded;
  580. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  581. result.Details.Add(new ExItem
  582. {
  583. SubTypeId = 4,
  584. SubType = "旷工",
  585. Deduction = ded,
  586. Duration = 1m,
  587. Unit = "天",
  588. StartTimeDt = date,
  589. Reason = reason + "旷工全天,扣除餐补"
  590. });
  591. }
  592. break;
  593. }
  594. }
  595. }
  596. return result;
  597. }
  598. /// <summary>
  599. /// 计算假勤扣款(请假、出差等)
  600. /// </summary>
  601. private static async Task<LeaveDeductionResult> CalculateLeaveDeductionAsync(string acctid, decimal dailyWage, decimal sickLeaveDailyWage,
  602. decimal amountPayable, int workDays, DateTime startDt, DateTime endDt, List<Sys_Calendar> sysCalendars)
  603. {
  604. var result = new LeaveDeductionResult();
  605. if (string.IsNullOrEmpty(acctid))
  606. return result;
  607. // 获取请假审批
  608. List<Sp_Detail> spLeaveDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 1);
  609. if (spLeaveDetails.Count > 0)
  610. {
  611. var leaveDetails = new List<LeaveDetailItem>();
  612. foreach (var sp in spLeaveDetails)
  613. {
  614. var applyData = sp.apply_data;
  615. if (applyData == null) continue;
  616. var vacationControl = applyData.contents?.FirstOrDefault(c => c.control == "Vacation");
  617. if (vacationControl?.value?.vacation == null) continue;
  618. var vac = vacationControl.value.vacation;
  619. var attendance = vac.attendance;
  620. var selector = vac.selector;
  621. if (selector?.options == null || selector.options.Count == 0) continue;
  622. int leaveType = int.Parse(selector.options[0].key);
  623. var dateRange = attendance.date_range;
  624. // 只处理当月的假勤
  625. if (dateRange.new_begin_dt < startDt || dateRange.new_begin_dt > endDt) continue;
  626. var sliceInfo = (leaveType == 2 || leaveType == 3) ? attendance.slice_info : new Slice_info();
  627. var splitItems = SplitLeaveByMonth(dateRange, startDt, endDt, sysCalendars, leaveType, sliceInfo);
  628. leaveDetails.AddRange(splitItems);
  629. }
  630. if (leaveDetails.Any()) leaveDetails = leaveDetails.OrderBy(x => x.Start).ToList();
  631. foreach (var item in leaveDetails)
  632. {
  633. decimal leaveMeal = 0;
  634. decimal deduction = 0;
  635. bool isMealDeduct = (item.Hours >= PayrollConfig.MealDeductionLeaveHours || item.Days >= 0.5m);
  636. if (item.TypeId == 2) // 事假
  637. {
  638. deduction = ConvertToDecimal(dailyWage * item.Days);
  639. if (deduction == 0 && item.Hours > 0)
  640. deduction = ConvertToDecimal((dailyWage / PayrollConfig.WorkHoursPerDay) * item.Hours);
  641. result.PersonalLeaveTotal += deduction;
  642. }
  643. else if (item.TypeId == 3) // 病假
  644. {
  645. decimal deductPerDay = ConvertToDecimal(dailyWage - sickLeaveDailyWage);
  646. if (deductPerDay > 0)
  647. {
  648. deduction = ConvertToDecimal(deductPerDay * item.Days);
  649. if (deduction == 0 && item.Hours > 0)
  650. deduction = ConvertToDecimal((deductPerDay / PayrollConfig.WorkHoursPerDay) * item.Hours);
  651. }
  652. result.SickLeaveTotal += deduction;
  653. }
  654. // 年假、调休假不扣薪、视情况扣除餐补
  655. if (isMealDeduct)
  656. {
  657. if (item.Hours >= 0 || item.Days >= 0.5m)
  658. {
  659. leaveMeal = PayrollConfig.MealSubsidyPerDay;
  660. }
  661. }
  662. result.MealDeduction += leaveMeal;
  663. // 产生扣款时记录
  664. if ((deduction + leaveMeal) > 0m)
  665. {
  666. result.Details.Add(new ExItem
  667. {
  668. SubTypeId = item.TypeId,
  669. SubType = GetLeaveTypeName(item.TypeId),
  670. Deduction = deduction,
  671. MealDeduction = leaveMeal,
  672. StartTimeDt = item.Start,
  673. EndTimeDt = item.End,
  674. Duration = item.Days > 0 ? item.Days : item.Hours,
  675. Unit = item.Days > 0 ? "天" : "小时",
  676. Reason = $"{GetLeaveTypeName(item.TypeId)} {(item.Days > 0 ? item.Days.ToString() + "天" : item.Hours.ToString() + "小时")}"
  677. });
  678. }
  679. }
  680. }
  681. #region 处理出差
  682. // 处理出差
  683. List<Sp_Detail> spTripDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 3);
  684. if (spTripDetails.Count > 0)
  685. {
  686. foreach (var sp in spTripDetails)
  687. {
  688. var applyData = sp.apply_data;
  689. if (applyData == null) continue;
  690. var attendanceControl = applyData.contents?.FirstOrDefault(c => c.control == "Attendance");
  691. if (attendanceControl?.value?.attendance == null) continue;
  692. var dateRange = attendanceControl.value.attendance.date_range;
  693. var splitTrips = SplitTripByMonth(dateRange, startDt, endDt);
  694. foreach (var trip in splitTrips)
  695. {
  696. decimal tripMeal = trip.Days * PayrollConfig.MealSubsidyPerDay;
  697. result.MealDeduction += tripMeal;
  698. result.Details.Add(new ExItem
  699. {
  700. SubTypeId = 5,
  701. SubType = "出差",
  702. Deduction = 0,
  703. MealDeduction = tripMeal,
  704. StartTimeDt = trip.Start,
  705. EndTimeDt = trip.End,
  706. Duration = trip.Days,
  707. Unit = "天",
  708. Reason = $"出差 {trip.Days}天,无餐补"
  709. });
  710. }
  711. }
  712. }
  713. #endregion
  714. return result;
  715. }
  716. /// <summary>
  717. /// 计算补卡扣款
  718. /// </summary>
  719. private static async Task<PunchCorrectionResult> CalculatePunchCorrectionAsync(string acctid, decimal floats, decimal dailyWage,
  720. DateTime startDt, DateTime endDt)
  721. {
  722. var result = new PunchCorrectionResult();
  723. if (string.IsNullOrEmpty(acctid)) return result;
  724. var bukaDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 2);
  725. if (bukaDetails.Count == 0) return result;
  726. bool isProbation = floats == 0;
  727. int freeCount = isProbation ? PayrollConfig.ProbationFreeCardCount : PayrollConfig.RegularFreeCardCount;
  728. int bukaNum = 1;
  729. foreach (var sp in bukaDetails)
  730. {
  731. var applyData = sp.apply_data;
  732. if (applyData == null) continue;
  733. var punchCorrControl = applyData.contents?.FirstOrDefault(c => c.control == "PunchCorrection");
  734. if (punchCorrControl?.value?.punch_correction == null) continue;
  735. DateTime missTime = punchCorrControl.value.punch_correction.time_dt;
  736. DateTime missDate = missTime.Date;
  737. if (missDate < startDt || missDate > endDt) continue;
  738. if (bukaNum <= freeCount + 1)
  739. {
  740. result.MissPunchFine += PayrollConfig.PunchCorrectionLowFine;
  741. result.Details.Add(new ExItem
  742. {
  743. SubTypeId = 7,
  744. SubType = "打卡补卡",
  745. Deduction = PayrollConfig.PunchCorrectionLowFine,
  746. StartTimeDt = missTime,
  747. Reason = $"第{bukaNum}次补卡,罚款{PayrollConfig.PunchCorrectionLowFine}元"
  748. });
  749. }
  750. else if (PayrollConfig.PunchCorrectionToAbsentAfterFourth)
  751. {
  752. // 第4次起按旷工半天处理
  753. decimal ded = ConvertToDecimal(dailyWage / 2);
  754. result.AbsenteeismDeduction += ded;
  755. result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
  756. result.Details.Add(new ExItem
  757. {
  758. SubTypeId = 4,
  759. SubType = "旷工(补卡超限)",
  760. Deduction = ded,
  761. StartTimeDt = missDate,
  762. Reason = $"第{bukaNum}次补卡超限,按旷工半天处理"
  763. });
  764. }
  765. bukaNum++;
  766. }
  767. return result;
  768. }
  769. /// <summary>
  770. /// 出差按月份拆分(支持 halfday / hour 类型,按自然日扣除餐补)
  771. /// </summary>
  772. private static List<LeaveDetailItem> SplitTripByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd)
  773. {
  774. var result = new List<LeaveDetailItem>();
  775. if (dateRange == null) return result;
  776. // 验证起止时间有效性
  777. DateTime start = dateRange.new_begin_dt;
  778. DateTime end = dateRange.new_end_dt;
  779. if (start > end) (start, end) = (end, start);
  780. // 跨月范围检查
  781. if (end < monthStart || start > monthEnd) return result;
  782. DateTime actualStart = start < monthStart ? monthStart : start;
  783. DateTime actualEnd = end > monthEnd ? monthEnd : end;
  784. // 计算该月内的出差天数(按自然日,包含首尾)
  785. int days = (actualEnd.Date - actualStart.Date).Days + 1;
  786. // 如果出差跨月,且开始/结束是半天,仍按整天扣除餐补(业务规则:出差即无餐补)
  787. result.Add(new LeaveDetailItem
  788. {
  789. TypeId = 5,
  790. TypeName = "出差",
  791. Start = actualStart,
  792. End = actualEnd,
  793. Days = days,
  794. Hours = days * PayrollConfig.WorkHoursPerDay, // 仅用于记录,不影响扣款
  795. DtType = "halfday",
  796. IsBusinessTrip = true
  797. });
  798. return result;
  799. }
  800. /// <summary>
  801. /// 请假按月份拆分(支持 halfday / hour,区分工作日)
  802. /// </summary>
  803. private static List<LeaveDetailItem> SplitLeaveByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd,
  804. List<Sys_Calendar> sysCalendars, int leaveType, Slice_info? sliceInfo)
  805. {
  806. var result = new List<LeaveDetailItem>();
  807. if (dateRange == null) return result;
  808. DateTime start = dateRange.new_begin_dt;
  809. DateTime end = dateRange.new_end_dt;
  810. if (start > end) (start, end) = (end, start);
  811. if (end < monthStart || start > monthEnd) return result;
  812. DateTime actualStart = start < monthStart ? monthStart : start;
  813. DateTime actualEnd = end > monthEnd ? monthEnd : end;
  814. decimal days = 0, hours = 0;
  815. for (var dt = actualStart.Date; dt <= actualEnd.Date; dt = dt.AddDays(1))
  816. {
  817. bool isWorkDay = sysCalendars.Any(c => c.Dt == dt.ToString("yyyy-MM-dd") && c.IsWorkDay);
  818. if (!isWorkDay) continue;
  819. if (dt == actualStart.Date && dt == actualEnd.Date)
  820. {
  821. if (dateRange.type.Equals("halfday"))
  822. {
  823. days = dateRange.new_duration / 86400m;
  824. }
  825. else if (dateRange.type.Equals("hour"))
  826. {
  827. hours = dateRange.new_duration / 3600m;
  828. }
  829. }
  830. else if (dt == actualStart.Date || dt == actualEnd.Date)
  831. {
  832. days += 0.5M;
  833. hours += PayrollConfig.WorkHoursPerDay / 2;
  834. }
  835. else
  836. {
  837. days += 1;
  838. hours += PayrollConfig.WorkHoursPerDay;
  839. }
  840. }
  841. result.Add(new LeaveDetailItem
  842. {
  843. TypeId = leaveType,
  844. TypeName = GetLeaveTypeName(leaveType),
  845. Start = actualStart,
  846. End = actualEnd,
  847. Days = days,
  848. Hours = hours,
  849. DtType = dateRange.type
  850. });
  851. return result;
  852. }
  853. /// <summary>
  854. /// 获取审核通过的加班时段明细(结束时间 ≥ 20:00,且已裁剪至当月范围内)
  855. /// </summary>
  856. private static async Task<List<OvertimePeriodDetail>> GetApprovedOvertimePeriodsDetailAsync(string acctid, DateTime monthStart, DateTime monthEnd)
  857. {
  858. var result = new List<OvertimePeriodDetail>();
  859. if (string.IsNullOrEmpty(acctid)) return result;
  860. // 加班审批的 subType = 5
  861. var overtimeApprovals = await _qiYeWeChatApiService.GetApprovalDetailsAsync(monthStart, monthEnd, acctid, 2, 5);
  862. foreach (var sp in overtimeApprovals)
  863. {
  864. if (sp.sp_status != 2) continue; // 只取已通过
  865. var applyData = sp.apply_data;
  866. if (applyData == null) continue;
  867. var attendanceControl = applyData.contents?.FirstOrDefault(c => c.control == "Attendance");
  868. if (attendanceControl?.value?.attendance?.date_range == null) continue;
  869. var dateRange = attendanceControl.value.attendance.date_range;
  870. DateTime start = dateRange.new_begin_dt;
  871. DateTime end = dateRange.new_end_dt;
  872. if (start > end) (start, end) = (end, start);
  873. // 只处理结束时间 ≥ 20:00 的加班(否则没有加班餐补)
  874. if (end.TimeOfDay < new TimeSpan(PayrollConfig.OvertimeStartHour, PayrollConfig.OvertimeStartMinute, 0))
  875. continue;
  876. // 跨月裁剪:只保留与当前月份重叠的部分(结束时间不能超出当月最后一天)
  877. if (end < monthStart || start > monthEnd) continue;
  878. DateTime actualEnd = end > monthEnd ? monthEnd.Date.AddDays(1).AddSeconds(-1) : end;
  879. result.Add(new OvertimePeriodDetail
  880. {
  881. StartDate = start,
  882. EndTime = actualEnd
  883. });
  884. }
  885. return result.OrderBy(x => x.StartDate).ToList();
  886. }
  887. /// <summary>
  888. /// 计算加班餐补(基于审批与打卡记录)
  889. /// </summary>
  890. private static async Task<(decimal total, List<ExItem> details)> CalculateOvertimeMealSubsidyWithDetailsAsync(
  891. string acctid, DateTime monthStart, DateTime monthEnd)
  892. {
  893. // 结束时间 默认加一天 至第二天的凌晨零点
  894. monthEnd = monthEnd.AddDays(1);
  895. var details = new List<ExItem>();
  896. var periods = await GetApprovedOvertimePeriodsDetailAsync(acctid, monthStart, monthEnd);
  897. if (periods.Count == 0) return (0, details);
  898. // 获取该员工当月的所有打卡明细
  899. var resp = await _qiYeWeChatApiService.GetCheckinDataAsync(new List<string> { acctid }, 3, monthStart, monthEnd);
  900. if (resp.errcode != 0) return (0, details);
  901. // 筛选符合条件的打卡记录:
  902. // 1. 下班打卡(IsEveningCheckIn)
  903. // 2. 非外出打卡
  904. // 3. 无缺卡异常(IsMissPunch)
  905. // 4. 打卡地点在公司白名单内(位置名称或WiFi名称匹配)
  906. // 5. 打卡时间 >= 当天 20:00(在后续 valid 判断中已包含,这里先放宽筛选)
  907. var validCheckins = resp.checkindata
  908. .Where(x => x.IsEveningCheckIn && !x.IsOutCheckIn && !x.IsMissPunch &&
  909. (PayrollConfig.CompanyLocations.Contains(x.location_title) ||
  910. PayrollConfig.CompanyWifiNames.Contains(x.wifiname)))
  911. .ToList();
  912. decimal total = 0;
  913. foreach (var period in periods)
  914. {
  915. // 查找是否存在一条打卡记录满足:
  916. // - 打卡时间 >= 加班结束时间(period.EndTime)
  917. // - 跨日允许次日凌晨 6:00 之前
  918. bool valid = validCheckins.Any(r =>
  919. {
  920. DateTime checkTime = r.checkin_time_dt;
  921. DateTime end = period.EndTime;
  922. if (checkTime < end) return false;
  923. // 如果打卡日期 > 加班结束日期(跨天)
  924. if (checkTime.Date > end.Date)
  925. {
  926. // 只允许跨一天,且打卡时间不晚于次日 06:00
  927. return checkTime.Date == end.Date.AddDays(1) && checkTime.TimeOfDay <= TimeSpan.FromHours(6);
  928. }
  929. return true;
  930. });
  931. if (valid)
  932. {
  933. total += PayrollConfig.OvertimeMealSubsidy;
  934. details.Add(new ExItem
  935. {
  936. SubTypeId = 6,
  937. SubType = "加班餐补",
  938. Deduction = 0,
  939. MealDeduction = 0,
  940. StartTimeDt = period.StartDate,
  941. EndTimeDt = period.EndTime,
  942. Duration = PayrollConfig.OvertimeMealSubsidy,
  943. Unit = "元",
  944. Reason = $"加班餐补({period.StartDate:yyyy-MM-dd HH:mm} 加班至 {period.EndTime:HH:mm} 后合规打卡)"
  945. });
  946. }
  947. }
  948. return (total, details);
  949. }
  950. private static string GetLeaveTypeName(int typeId)
  951. {
  952. return typeId switch
  953. {
  954. 1 => "年假",
  955. 2 => "事假",
  956. 3 => "病假",
  957. 4 => "调休假",
  958. 5 => "出差",
  959. _ => "其他"
  960. };
  961. }
  962. private static void UpdateWageSheet(Pm_WageSheet ws, string yearMonth, DateTime start, DateTime end,
  963. int workDays, int actualWorkDays, decimal mealActual, LeaveDeductionResult leave,
  964. AttendancePenaltyResult attendance, PunchCorrectionResult punch,
  965. decimal shouldTotal, decimal totalDeduction, decimal afterTax, int userId, string exItemsRemark)
  966. {
  967. ws.YearMonth = yearMonth;
  968. ws.StartDate = start.ToString("yyyy-MM-dd");
  969. ws.EndDate = end.ToString("yyyy-MM-dd");
  970. ws.WorkDays = workDays;
  971. ws.RegularDays = actualWorkDays;
  972. ws.SickLeave = leave.SickLeaveTotal;
  973. ws.SomethingFalse = leave.PersonalLeaveTotal;
  974. ws.LateTo = attendance.LateFine;
  975. ws.LeaveEarly = attendance.EarlyFine;
  976. ws.Absenteeism = attendance.AbsenteeismDeduction + punch.AbsenteeismDeduction;
  977. ws.NotPunch = punch.MissPunchFine;
  978. ws.Mealsupplement = mealActual;
  979. ws.Should = ConvertToDecimal(shouldTotal);
  980. ws.TotalDeductions = ConvertToDecimal(totalDeduction);
  981. ws.TotalRealHair = ConvertToDecimal(afterTax);
  982. ws.AfterTax = ConvertToDecimal(afterTax);
  983. ws.Ex_ItemsRemark = exItemsRemark;
  984. ws.LastUpdateUserId = userId;
  985. ws.LastUpdateDt = DateTime.Now;
  986. ws.CreateUserId = userId;
  987. ws.CreateTime = DateTime.Now;
  988. }
  989. public static async Task<int> GetWorkDays(string yearMonth)
  990. {
  991. string sql = $"Select * From Pm_WageIssueWorkingDay Where Isdel = 0 And YearMonth = '{yearMonth}'";
  992. var data = await _sqlSugar.SqlQueryable<WageYearMonthView>(sql).FirstAsync();
  993. return data?.Workdays ?? 0;
  994. }
  995. public static decimal ConvertToDecimal(decimal value)
  996. {
  997. return Math.Floor(value * 100) / 100;
  998. }
  999. #endregion
  1000. }