using OASystem.API.OAMethodLib.QiYeWeChatAPI; using OASystem.Domain.Entities.PersonnelModule; using OASystem.Domain.ViewModels.CRM; using OASystem.Domain.ViewModels.PersonnelModule; using OASystem.Domain.ViewModels.QiYeWeChat; using System.Diagnostics.Tracing; namespace OASystem.API.OAMethodLib; /// /// 工资计算公共参数配置 /// public static class PayrollConfig { // ========== 基础参数 ========== public const decimal ChengduMinimumWage = 2100m; public const decimal SickLeaveWageRatio = 0.8m; public const decimal WorkHoursPerDay = 7.5m; public const decimal MealSubsidyPerDay = 10m; // ========== 迟到早退规则 ========== public const int FreeTotalMinutes = 10; // 累计免罚分钟(超过此值才罚款) public const decimal FinePerEvent = 50m; // 每次事件罚款金额 public const int WorkStartHour = 9; public const int WorkEndHour = 18; public const int DefaultAllowSeconds = 300; // 默认允许迟到/早退秒数(当规则未设置时使用) // ========== 旷工判定 ========== public const int HalfDayMissMinutes = 60; // 半日旷工下限(分钟) public const int FullDayMissMinutes = 180; // 全日旷工阈值(分钟) // ========== 补卡规则 ========== public const int ProbationFreeCardCount = 2; public const int RegularFreeCardCount = 3; public const decimal PunchCorrectionLowFine = 10m; public const bool PunchCorrectionToAbsentAfterFourth = true; // ========== 请假餐补规则 ========== public const int MealDeductionLeaveHours = 3; } /// /// 工资计算 /// public static class PayrollComputation_v1 { private static Result _result = new Result(); private static readonly IQiYeWeChatApiService _qiYeWeChatApiService = AutofacIocManager.Instance.GetService(); private static readonly SqlSugarClient _sqlSugar = AutofacIocManager.Instance.GetService(); // =============================== 内部类定义 =============================== /// /// 解析后的打卡规则(仅用于当前计算,不缓存) /// private class ParsedCheckInRule { public uint GroupId { get; set; } public int FlexOnDutyTime { get; set; } = 0; // 允许迟到秒数 public int FlexOffDutyTime { get; set; } = 0; // 允许早退秒数 public HashSet SpecialWorkDays { get; set; } = new HashSet(); public HashSet SpecialOffDays { get; set; } = new HashSet(); } /// /// 考勤扣款结果 /// private class AttendancePenaltyResult { public decimal LateFine { get; set; } public decimal EarlyFine { get; set; } public decimal AbsenteeismDeduction { get; set; } public decimal MealDeduction { get; set; } public decimal TotalDeduction => LateFine + EarlyFine + AbsenteeismDeduction; public List Details { get; set; } = new List(); } /// /// 假勤扣款结果 /// private class LeaveDeductionResult { public decimal PersonalLeaveTotal { get; set; } public decimal SickLeaveTotal { get; set; } public decimal MealDeduction { get; set; } public decimal TotalDeduction => PersonalLeaveTotal + SickLeaveTotal; public List Details { get; set; } = new List(); } /// /// 补卡扣款结果 /// private class PunchCorrectionResult { public decimal MissPunchFine { get; set; } public decimal AbsenteeismDeduction { get; set; } public decimal MealDeduction { get; set; } public decimal TotalDeduction => MissPunchFine + AbsenteeismDeduction; public List Details { get; set; } = new List(); } /// /// 扣款明细项(用于序列化) /// private class ExItem { public int SubTypeId { get; set; } public string? SubType { get; set; } public decimal Deduction { get; set; } = 0.00M; public decimal MealDeduction { get; set; } = 0.00M; public DateTime StartTimeDt { get; set; } public DateTime EndTimeDt { get; set; } public decimal Duration { get; set; } public string Unit { get; set; } = "小时"; public string? Reason { get; set; } public DateTime Apply_time_dt { get; set; } public List? Approval_name { get; set; } } /// /// 扣款分类容器 /// private class ExItems { public string? Type { get; set; } public object? ExItemInfo { get; set; } } /// /// 请假/调休/出差明细(用于跨月拆分) /// private class LeaveDetailItem { public int TypeId { get; set; } public string TypeName { get; set; } = ""; public DateTime Start { get; set; } public DateTime End { get; set; } public decimal Days { get; set; } public decimal Hours { get; set; } public string DtType { get; set; } = ""; public decimal NewDuration { get; set; } public Slice_info SliceInfo { get; set; } = new Slice_info(); public DateTime ApplyDt { get; set; } public bool IsBusinessTrip { get; set; } } /// /// 判断是否为工作日(基于规则中的特殊日期和系统日历) /// private static bool IsWorkDayForRule( DateTime date, List sysCalendars) { var calendar = sysCalendars.FirstOrDefault(c => c.Dt == date.ToString("yyyy-MM-dd")); if (calendar != null) return calendar.IsWorkDay; // 默认周一到周五为工作日 return date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday; } /// /// 计算工资 /// public static async Task SalaryCalculatorAsync( List pm_WageSheetDattaSources, int userId, string thisYearMonth, DateTime startDt, DateTime endDt) { if (pm_WageSheetDattaSources.Count <= 0) { _result.Msg = "计算工资传入数据为空!"; return _result; } // 获取当月应出勤天数 int workDays = await GetWorkDays(thisYearMonth); if (workDays <= 0) { _result.Msg = $"{thisYearMonth} 工作日未设置,请前往《工作日管理页面》设置!"; return _result; } var userIds = pm_WageSheetDattaSources.Select(ws => ws.UserId).Distinct().ToList(); var userIdInfos = await GetSysUserWeChatIds(userIds); if (!userIdInfos.Any()) { _result.Msg = $"企业微信Id获取失败,请联系管理员!"; return _result; } List qyWhchatIdList = userIdInfos.Select(it => it.WeChatId ?? "").Distinct().ToList(); // 获取打卡日报数据(包含 exception_infos) CheckInDayDataView checkInDayDataView = await _qiYeWeChatApiService.GetCheckInDayDataAsync(qyWhchatIdList, startDt, endDt); if (checkInDayDataView.errcode != 0) { _result.Msg = $"【企业微信】【打卡】【获取时间段内所有日打卡】【Msg】{checkInDayDataView.errmsg}"; return _result; } // 获取系统日历(用于判断工作日) List sys_Calendars = await GetSysCalendars(startDt, endDt); string _name = ""; try { foreach (var pm_wsInfo in pm_WageSheetDattaSources) { _name = userIdInfos.Find(it => it.Id == pm_wsInfo.UserId)?.CnName ?? "Unknown"; // 获取员工当月的打卡日报数据(按日期分组) var userDailyRecords = GetUserDailyRecords(checkInDayDataView, _name); // 计算日薪 decimal amountPayable = GetTotalSalaryBase(pm_wsInfo); decimal dailyWage = ConvertToDecimal(amountPayable / workDays); decimal sickLeaveDailyWage = ConvertToDecimal((PayrollConfig.ChengduMinimumWage * PayrollConfig.SickLeaveWageRatio) / workDays); // 获取员工企业微信ID(用于获取审批数据) string acctid = userDailyRecords.Count > 0 ? userDailyRecords.First().Value.base_info.acctid : ""; // 计算考勤扣款(使用日报中的 exception_infos) var attendanceResult = CalculateAttendanceFromDailyRecords(userDailyRecords, dailyWage, sys_Calendars); // 计算假勤扣款(请假、出差等) var leaveResult = new LeaveDeductionResult(); // 计算补卡扣款 var punchResult = new PunchCorrectionResult(); // 当月假勤次数 var spCount = userDailyRecords.Sum(x => x.Value.sp_items?.Sum(y => y.count) ?? 0); //当月存在假勤的情况,执行假勤扣除 if (spCount > 0) { leaveResult = await CalculateLeaveDeductionAsync(acctid, dailyWage, sickLeaveDailyWage, amountPayable, workDays, startDt, endDt, sys_Calendars); punchResult = await CalculatePunchCorrectionAsync(acctid, pm_wsInfo.Floats, dailyWage, startDt, endDt); } // 计算餐补 int actualWorkDays = userDailyRecords.Count(r => IsWorkDayForRule(r.Key, sys_Calendars)); decimal mealSubsidy = actualWorkDays * PayrollConfig.MealSubsidyPerDay; decimal mealDeductionTotal = attendanceResult.MealDeduction + leaveResult.MealDeduction + punchResult.MealDeduction; decimal mealActual = mealSubsidy - mealDeductionTotal; // 汇总扣款 decimal totalDeduction = attendanceResult.TotalDeduction + leaveResult.TotalDeduction + punchResult.TotalDeduction + pm_wsInfo.WithholdingInsurance + pm_wsInfo.ReservedFunds + pm_wsInfo.OtherDeductions; // 应发合计 decimal shouldTotal = amountPayable + mealActual + pm_wsInfo.OtherHandle; decimal afterTax = Math.Floor((shouldTotal - totalDeduction - pm_wsInfo.WithholdingTax) * 100) / 100; // 组装扣款明细JSON var exItemsList = new List(); if (attendanceResult.Details.Count > 0) exItemsList.Add(new ExItems { Type = "打卡", ExItemInfo = attendanceResult.Details }); if (leaveResult.Details.Count > 0) exItemsList.Add(new ExItems { Type = "假勤", ExItemInfo = leaveResult.Details }); if (punchResult.Details.Count > 0) exItemsList.Add(new ExItems { Type = "补卡", ExItemInfo = punchResult.Details }); string exItemsRemark = exItemsList.Count > 0 ? JsonConvert.SerializeObject(exItemsList) : ""; // 更新工资表 UpdateWageSheet(pm_wsInfo, thisYearMonth, startDt, endDt, workDays, actualWorkDays, mealActual, leaveResult, attendanceResult, punchResult, shouldTotal, totalDeduction, afterTax, userId, exItemsRemark); } } catch (Exception ex) { _result.Msg = $"【{_name}】【Msg:{ex.Message}】"; return _result; } _result.Code = 0; _result.Data = pm_WageSheetDattaSources; return _result; } // =============================== 辅助方法 =============================== private static decimal GetTotalSalaryBase(Pm_WageSheet wageSheet) { return wageSheet.Basic + wageSheet.Floats + wageSheet.PostAllowance + wageSheet.InformationSecurityFee + wageSheet.OtherSubsidies; } private static async Task> GetSysCalendars(DateTime startDt, DateTime endDt) { string sql = string.Format("Select * From Sys_Calendar Where Isdel = 0 And Dt between '{0}' And '{1}'", startDt.ToString("yyyy-MM-dd"), endDt.ToString("yyyy-MM-dd")); return await _sqlSugar.SqlQueryable(sql).ToListAsync(); } private static async Task> GetSysUserWeChatIds(List ints) { return await _sqlSugar.Queryable().Where(u => ints.Contains(u.Id) && u.IsDel == 0) .Select(u => new UserWeChatIdView { Id = u.Id, CnName = u.CnName, WeChatId = u.QiyeChatUserId }) .ToListAsync(); } /// /// 获取员工每日的打卡日报数据(按日期索引) /// private static Dictionary GetUserDailyRecords(CheckInDayDataView view, string name) { var dict = new Dictionary(); var allRecords = view.datas ?? new List(); IEnumerable userRecords = allRecords.Where(r => r.base_info.name == name || r.base_info.name.Contains(name)).OrderBy(x => x.base_info.DateDt); foreach (var record in userRecords) { DateTime date = record.base_info.DateDt.Date; dict[date] = record; } return dict; } /// /// 基于日报数据中的 exception_infos 计算考勤扣款(次数累计 + 分时段罚款) /// private static AttendancePenaltyResult CalculateAttendanceFromDailyRecords( Dictionary dailyRecords, decimal dailyWage, List sysCalendars) { var result = new AttendancePenaltyResult(); // 统计本月内迟到/早退次数(仅统计时长<60分钟的事件) int lateEarlyCount = 0; // 总次数(用于判定是否超过2次) int lateCount = 0; // 迟到次数(保留,暂未独立使用) int earlyCount = 0; // 早退次数 foreach (var kvp in dailyRecords) { DateTime date = kvp.Key; var record = kvp.Value; // 判断是否为工作日(若不是工作日,不计算考勤扣款) bool isWorkDay = IsWorkDayForRule(date, sysCalendars); if (!isWorkDay) continue; var exceptions = record.exception_infos ?? new List(); foreach (var ex in exceptions) { int exceptionType = ex.exception; // 1迟到 2早退 3缺卡 4旷工 int minutes = ex.duration / 60; // 时长(分钟) string reason = $"{date:yyyy-MM-dd} "; decimal deduction = 0m; switch (exceptionType) { case 1: // 迟到 if (minutes < 60) { // 迟到时长小于60分钟,计入次数,并根据分钟数确定罚款 lateEarlyCount++; lateCount++; if (minutes < 10) // 不足10分钟:第3次起罚50 { if (lateEarlyCount >= 3) { deduction = PayrollConfig.FinePerEvent; // 50元 result.LateFine += deduction; reason += $"迟到{minutes}分钟(第{lateEarlyCount}次),按次数罚款{deduction}元"; } else { reason += $"迟到{minutes}分钟(第{lateEarlyCount}次),前2次不处罚"; } } else if (minutes >= 10 && minutes < 30) // 9:10~9:30 => 罚款20 { deduction = 20m; result.LateFine += deduction; reason += $"迟到{minutes}分钟,罚款{deduction}元"; } else if (minutes >= 30 && minutes < 60) // 9:30~9:59 => 罚款50 { deduction = PayrollConfig.FinePerEvent; // 50元 result.LateFine += deduction; reason += $"迟到{minutes}分钟,罚款{deduction}元"; } result.Details.Add(new ExItem { SubTypeId = 1, SubType = "迟到", Deduction = deduction, StartTimeDt = date, Reason = reason }); } else { // 迟到≥60分钟,按旷工处理 if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes) { decimal ded = ConvertToDecimal(dailyWage / 2); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工(迟到超时)", Deduction = ded, StartTimeDt = date, Reason = reason + $"迟到{minutes}分钟,按旷工半天处理,扣除餐补" }); } else // minutes > 180 { decimal ded = ConvertToDecimal(dailyWage); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工(迟到超时)", Deduction = ded, StartTimeDt = date, Reason = reason + $"迟到{minutes}分钟,按旷工全天处理,扣除餐补" }); } } break; case 2: // 早退 if (minutes < 60) { lateEarlyCount++; earlyCount++; if (minutes < 10) { if (lateEarlyCount >= 3) { deduction = PayrollConfig.FinePerEvent; result.EarlyFine += deduction; reason += $"早退{minutes}分钟(第{lateEarlyCount}次),按次数罚款{deduction}元"; } else { reason += $"早退{minutes}分钟(第{lateEarlyCount}次),前2次不处罚"; } } else if (minutes >= 10 && minutes < 30) { deduction = 20m; result.EarlyFine += deduction; reason += $"早退{minutes}分钟,罚款{deduction}元"; } else if (minutes >= 30 && minutes < 60) { deduction = PayrollConfig.FinePerEvent; // 50元 result.EarlyFine += deduction; reason += $"早退{minutes}分钟,罚款{deduction}元"; } result.Details.Add(new ExItem { SubTypeId = 2, SubType = "早退", Deduction = deduction, StartTimeDt = date, Reason = reason }); } else { // 早退≥60分钟,按旷工处理 if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes) { decimal ded = ConvertToDecimal(dailyWage / 2); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工(早退超时)", Deduction = ded, StartTimeDt = date, Reason = reason + $"早退{minutes}分钟,按旷工半天处理,扣除餐补" }); } else { decimal ded = ConvertToDecimal(dailyWage); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工(早退超时)", Deduction = ded, StartTimeDt = date, Reason = reason + $"早退{minutes}分钟,按旷工全天处理,扣除餐补" }); } } break; // 缺卡、旷工处理保持不变 case 3: { decimal ded = ConvertToDecimal(dailyWage / 2); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工(缺卡)", Deduction = ded, StartTimeDt = date, Reason = reason + "缺卡,按半天旷工处理,扣除餐补" }); } break; case 4: if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes) { decimal ded = ConvertToDecimal(dailyWage / 2); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工", Deduction = ded, StartTimeDt = date, Reason = reason + "旷工半天,扣除餐补" }); } else { decimal ded = ConvertToDecimal(dailyWage); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工", Deduction = ded, StartTimeDt = date, Reason = reason + "旷工全天,扣除餐补" }); } break; } } } return result; } /// /// 计算假勤扣款(请假、出差等) /// private static async Task CalculateLeaveDeductionAsync(string acctid, decimal dailyWage, decimal sickLeaveDailyWage, decimal amountPayable, int workDays, DateTime startDt, DateTime endDt, List sysCalendars) { var result = new LeaveDeductionResult(); if (string.IsNullOrEmpty(acctid)) return result; // 获取请假审批 List spLeaveDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 1); if (spLeaveDetails.Count > 0) { var leaveDetails = new List(); foreach (var sp in spLeaveDetails) { var applyData = sp.apply_data; if (applyData == null) continue; var vacationControl = applyData.contents?.FirstOrDefault(c => c.control == "Vacation"); if (vacationControl?.value?.vacation == null) continue; var vac = vacationControl.value.vacation; var attendance = vac.attendance; var selector = vac.selector; if (selector?.options == null || selector.options.Count == 0) continue; int leaveType = int.Parse(selector.options[0].key); var dateRange = attendance.date_range; // 只处理当月的假勤 if (dateRange.new_begin_dt < startDt || dateRange.new_begin_dt > endDt) continue; var sliceInfo = (leaveType == 2 || leaveType == 3) ? attendance.slice_info : new Slice_info(); var splitItems = SplitLeaveByMonth(dateRange, startDt, endDt, sysCalendars, leaveType, sliceInfo); leaveDetails.AddRange(splitItems); } if (leaveDetails.Any()) leaveDetails = leaveDetails.OrderBy(x => x.Start).ToList(); foreach (var item in leaveDetails) { decimal leaveMeal = 0; decimal deduction = 0; bool isMealDeduct = (item.Hours >= PayrollConfig.MealDeductionLeaveHours || item.Days >= 0.5m); if (item.TypeId == 2) // 事假 { deduction = ConvertToDecimal(dailyWage * item.Days); if (deduction == 0 && item.Hours > 0) deduction = ConvertToDecimal((dailyWage / PayrollConfig.WorkHoursPerDay) * item.Hours); result.PersonalLeaveTotal += deduction; } else if (item.TypeId == 3) // 病假 { decimal deductPerDay = ConvertToDecimal(dailyWage - sickLeaveDailyWage); if (deductPerDay > 0) { deduction = ConvertToDecimal(deductPerDay * item.Days); if (deduction == 0 && item.Hours > 0) deduction = ConvertToDecimal((deductPerDay / PayrollConfig.WorkHoursPerDay) * item.Hours); } result.SickLeaveTotal += deduction; } // 年假、调休假不扣薪、视情况扣除餐补 if (isMealDeduct) { if (item.Hours >=0 || item.Days >= 0.5m) { leaveMeal = PayrollConfig.MealSubsidyPerDay; } } result.MealDeduction += leaveMeal; // 产生扣款时记录 if ((deduction + leaveMeal) > 0m) { result.Details.Add(new ExItem { SubTypeId = item.TypeId, SubType = GetLeaveTypeName(item.TypeId), Deduction = deduction, MealDeduction = leaveMeal, StartTimeDt = item.Start, EndTimeDt = item.End, Duration = item.Days > 0 ? item.Days : item.Hours, Unit = item.Days > 0 ? "天" : "小时", Reason = $"{GetLeaveTypeName(item.TypeId)} {(item.Days > 0 ? item.Days.ToString() + "天" : item.Hours.ToString() + "小时")}" }); } } } #region 处理出差 // 处理出差 List spTripDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 3); if (spTripDetails.Count > 0) { foreach (var sp in spTripDetails) { var applyData = sp.apply_data; if (applyData == null) continue; var attendanceControl = applyData.contents?.FirstOrDefault(c => c.control == "Attendance"); if (attendanceControl?.value?.attendance == null) continue; var dateRange = attendanceControl.value.attendance.date_range; var splitTrips = SplitTripByMonth(dateRange, startDt, endDt); foreach (var trip in splitTrips) { decimal tripMeal = trip.Days * PayrollConfig.MealSubsidyPerDay; result.MealDeduction += tripMeal; result.Details.Add(new ExItem { SubTypeId = 5, SubType = "出差", Deduction = 0, MealDeduction = tripMeal, StartTimeDt = trip.Start, EndTimeDt = trip.End, Duration = trip.Days, Unit = "天", Reason = $"出差 {trip.Days}天,无餐补" }); } } } #endregion return result; } /// /// 计算补卡扣款 /// private static async Task CalculatePunchCorrectionAsync(string acctid, decimal floats, decimal dailyWage, DateTime startDt, DateTime endDt) { var result = new PunchCorrectionResult(); if (string.IsNullOrEmpty(acctid)) return result; var bukaDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 2); if (bukaDetails.Count == 0) return result; bool isProbation = floats == 0; int freeCount = isProbation ? PayrollConfig.ProbationFreeCardCount : PayrollConfig.RegularFreeCardCount; int bukaNum = 1; foreach (var sp in bukaDetails) { var applyData = sp.apply_data; if (applyData == null) continue; var punchCorrControl = applyData.contents?.FirstOrDefault(c => c.control == "PunchCorrection"); if (punchCorrControl?.value?.punch_correction == null) continue; DateTime missTime = punchCorrControl.value.punch_correction.time_dt; DateTime missDate = missTime.Date; if (missDate < startDt || missDate > endDt) continue; if (bukaNum <= freeCount + 1) { result.MissPunchFine += PayrollConfig.PunchCorrectionLowFine; result.Details.Add(new ExItem { SubTypeId = 7, SubType = "打卡补卡", Deduction = PayrollConfig.PunchCorrectionLowFine, StartTimeDt = missTime, Reason = $"第{bukaNum}次补卡,罚款{PayrollConfig.PunchCorrectionLowFine}元" }); } else if (PayrollConfig.PunchCorrectionToAbsentAfterFourth) { // 第4次起按旷工半天处理 decimal ded = ConvertToDecimal(dailyWage / 2); result.AbsenteeismDeduction += ded; result.MealDeduction += PayrollConfig.MealSubsidyPerDay; result.Details.Add(new ExItem { SubTypeId = 4, SubType = "旷工(补卡超限)", Deduction = ded, StartTimeDt = missDate, Reason = $"第{bukaNum}次补卡超限,按旷工半天处理" }); } bukaNum++; } return result; } /// /// 出差按月份拆分(支持 halfday / hour 类型,按自然日扣除餐补) /// private static List SplitTripByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd) { var result = new List(); if (dateRange == null) return result; // 验证起止时间有效性 DateTime start = dateRange.new_begin_dt; DateTime end = dateRange.new_end_dt; if (start > end) (start, end) = (end, start); // 跨月范围检查 if (end < monthStart || start > monthEnd) return result; DateTime actualStart = start < monthStart ? monthStart : start; DateTime actualEnd = end > monthEnd ? monthEnd : end; // 计算该月内的出差天数(按自然日,包含首尾) int days = (actualEnd.Date - actualStart.Date).Days + 1; // 如果出差跨月,且开始/结束是半天,仍按整天扣除餐补(业务规则:出差即无餐补) result.Add(new LeaveDetailItem { TypeId = 5, TypeName = "出差", Start = actualStart, End = actualEnd, Days = days, Hours = days * PayrollConfig.WorkHoursPerDay, // 仅用于记录,不影响扣款 DtType = "halfday", IsBusinessTrip = true }); return result; } /// /// 请假按月份拆分(支持 halfday / hour,区分工作日) /// private static List SplitLeaveByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd, List sysCalendars, int leaveType, Slice_info? sliceInfo) { var result = new List(); if (dateRange == null) return result; DateTime start = dateRange.new_begin_dt; DateTime end = dateRange.new_end_dt; if (start > end) (start, end) = (end, start); if (end < monthStart || start > monthEnd) return result; DateTime actualStart = start < monthStart ? monthStart : start; DateTime actualEnd = end > monthEnd ? monthEnd : end; decimal days = 0, hours = 0; for (var dt = actualStart.Date; dt <= actualEnd.Date; dt = dt.AddDays(1)) { bool isWorkDay = sysCalendars.Any(c => c.Dt == dt.ToString("yyyy-MM-dd") && c.IsWorkDay); if (!isWorkDay) continue; if (dt == actualStart.Date && dt == actualEnd.Date) { if (dateRange.type.Equals("halfday")) { days = dateRange.new_duration / 86400m; } else if (dateRange.type.Equals("hour")) { hours = dateRange.new_duration / 3600m; } } else if (dt == actualStart.Date || dt == actualEnd.Date) { days += 0.5M; hours += PayrollConfig.WorkHoursPerDay / 2; } else { days += 1; hours += PayrollConfig.WorkHoursPerDay; } } result.Add(new LeaveDetailItem { TypeId = leaveType, TypeName = GetLeaveTypeName(leaveType), Start = actualStart, End = actualEnd, Days = days, Hours = hours, DtType = dateRange.type }); return result; } private static string GetLeaveTypeName(int typeId) { return typeId switch { 1 => "年假", 2 => "事假", 3 => "病假", 4 => "调休假", 5 => "出差", _ => "其他" }; } private static void UpdateWageSheet(Pm_WageSheet ws, string yearMonth, DateTime start, DateTime end, int workDays, int actualWorkDays, decimal mealActual, LeaveDeductionResult leave, AttendancePenaltyResult attendance, PunchCorrectionResult punch, decimal shouldTotal, decimal totalDeduction, decimal afterTax, int userId, string exItemsRemark) { ws.YearMonth = yearMonth; ws.StartDate = start.ToString("yyyy-MM-dd"); ws.EndDate = end.ToString("yyyy-MM-dd"); ws.WorkDays = workDays; ws.RegularDays = actualWorkDays; ws.SickLeave = leave.SickLeaveTotal; ws.SomethingFalse = leave.PersonalLeaveTotal; ws.LateTo = attendance.LateFine; ws.LeaveEarly = attendance.EarlyFine; ws.Absenteeism = attendance.AbsenteeismDeduction + punch.AbsenteeismDeduction; ws.NotPunch = punch.MissPunchFine; ws.Mealsupplement = mealActual; ws.Should = ConvertToDecimal(shouldTotal); ws.TotalDeductions = ConvertToDecimal(totalDeduction); ws.TotalRealHair = ConvertToDecimal(afterTax); ws.AfterTax = ConvertToDecimal(afterTax); ws.Ex_ItemsRemark = exItemsRemark; ws.LastUpdateUserId = userId; ws.LastUpdateDt = DateTime.Now; } public static async Task GetWorkDays(string yearMonth) { string sql = $"Select * From Pm_WageIssueWorkingDay Where Isdel = 0 And YearMonth = '{yearMonth}'"; var data = await _sqlSugar.SqlQueryable(sql).FirstAsync(); return data?.Workdays ?? 0; } public static decimal ConvertToDecimal(decimal value) { return Math.Floor(value * 100) / 100; } }