| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903 |
- 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;
- /// <summary>
- /// 工资计算公共参数配置
- /// </summary>
- 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;
- }
- /// <summary>
- /// 工资计算
- /// </summary>
- public static class PayrollComputation_v1
- {
- private static Result _result = new Result();
- private static readonly IQiYeWeChatApiService _qiYeWeChatApiService = AutofacIocManager.Instance.GetService<IQiYeWeChatApiService>();
- private static readonly SqlSugarClient _sqlSugar = AutofacIocManager.Instance.GetService<SqlSugarClient>();
- // =============================== 内部类定义 ===============================
- /// <summary>
- /// 解析后的打卡规则(仅用于当前计算,不缓存)
- /// </summary>
- private class ParsedCheckInRule
- {
- public uint GroupId { get; set; }
- public int FlexOnDutyTime { get; set; } = 0; // 允许迟到秒数
- public int FlexOffDutyTime { get; set; } = 0; // 允许早退秒数
- public HashSet<DateTime> SpecialWorkDays { get; set; } = new HashSet<DateTime>();
- public HashSet<DateTime> SpecialOffDays { get; set; } = new HashSet<DateTime>();
- }
- /// <summary>
- /// 考勤扣款结果
- /// </summary>
- 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<ExItem> Details { get; set; } = new List<ExItem>();
- }
- /// <summary>
- /// 假勤扣款结果
- /// </summary>
- 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<ExItem> Details { get; set; } = new List<ExItem>();
- }
- /// <summary>
- /// 补卡扣款结果
- /// </summary>
- 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<ExItem> Details { get; set; } = new List<ExItem>();
- }
- /// <summary>
- /// 扣款明细项(用于序列化)
- /// </summary>
- 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<string>? Approval_name { get; set; }
- }
- /// <summary>
- /// 扣款分类容器
- /// </summary>
- private class ExItems
- {
- public string? Type { get; set; }
- public object? ExItemInfo { get; set; }
- }
- /// <summary>
- /// 请假/调休/出差明细(用于跨月拆分)
- /// </summary>
- 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; }
- }
- /// <summary>
- /// 判断是否为工作日(基于规则中的特殊日期和系统日历)
- /// </summary>
- private static bool IsWorkDayForRule( DateTime date, List<Sys_Calendar> 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;
- }
- /// <summary>
- /// 计算工资
- /// </summary>
- public static async Task<Result> SalaryCalculatorAsync(
- List<Pm_WageSheet> 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<string> 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_Calendar> 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<ExItems>();
- 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<List<Sys_Calendar>> 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<Sys_Calendar>(sql).ToListAsync();
- }
- private static async Task<List<UserWeChatIdView>> GetSysUserWeChatIds(List<int> ints)
- {
- return await _sqlSugar.Queryable<Sys_Users>().Where(u => ints.Contains(u.Id) && u.IsDel == 0)
- .Select(u => new UserWeChatIdView { Id = u.Id, CnName = u.CnName, WeChatId = u.QiyeChatUserId })
- .ToListAsync();
- }
- /// <summary>
- /// 获取员工每日的打卡日报数据(按日期索引)
- /// </summary>
- private static Dictionary<DateTime, CheckInDayRoot> GetUserDailyRecords(CheckInDayDataView view, string name)
- {
- var dict = new Dictionary<DateTime, CheckInDayRoot>();
- var allRecords = view.datas ?? new List<CheckInDayRoot>();
- IEnumerable<CheckInDayRoot> 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;
- }
- /// <summary>
- /// 基于日报数据中的 exception_infos 计算考勤扣款(次数累计 + 分时段罚款)
- /// </summary>
- private static AttendancePenaltyResult CalculateAttendanceFromDailyRecords(
- Dictionary<DateTime, CheckInDayRoot> dailyRecords,
- decimal dailyWage,
- List<Sys_Calendar> 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<ExceptionInfo>();
- 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;
- }
- /// <summary>
- /// 计算假勤扣款(请假、出差等)
- /// </summary>
- private static async Task<LeaveDeductionResult> CalculateLeaveDeductionAsync(string acctid, decimal dailyWage, decimal sickLeaveDailyWage,
- decimal amountPayable, int workDays, DateTime startDt, DateTime endDt, List<Sys_Calendar> sysCalendars)
- {
- var result = new LeaveDeductionResult();
- if (string.IsNullOrEmpty(acctid))
- return result;
- // 获取请假审批
- List<Sp_Detail> spLeaveDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 1);
- if (spLeaveDetails.Count > 0)
- {
- var leaveDetails = new List<LeaveDetailItem>();
- 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<Sp_Detail> 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;
- }
- /// <summary>
- /// 计算补卡扣款
- /// </summary>
- private static async Task<PunchCorrectionResult> 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;
- }
- /// <summary>
- /// 出差按月份拆分(支持 halfday / hour 类型,按自然日扣除餐补)
- /// </summary>
- private static List<LeaveDetailItem> SplitTripByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd)
- {
- var result = new List<LeaveDetailItem>();
- 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;
- }
- /// <summary>
- /// 请假按月份拆分(支持 halfday / hour,区分工作日)
- /// </summary>
- private static List<LeaveDetailItem> SplitLeaveByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd,
- List<Sys_Calendar> sysCalendars, int leaveType, Slice_info? sliceInfo)
- {
- var result = new List<LeaveDetailItem>();
- 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<int> GetWorkDays(string yearMonth)
- {
- string sql = $"Select * From Pm_WageIssueWorkingDay Where Isdel = 0 And YearMonth = '{yearMonth}'";
- var data = await _sqlSugar.SqlQueryable<WageYearMonthView>(sql).FirstAsync();
- return data?.Workdays ?? 0;
- }
- public static decimal ConvertToDecimal(decimal value)
- {
- return Math.Floor(value * 100) / 100;
- }
- }
|