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