|
|
@@ -1,9 +1,11 @@
|
|
|
-using OASystem.API.OAMethodLib.QiYeWeChatAPI;
|
|
|
+using MathNet.Numerics.LinearAlgebra.Factorization;
|
|
|
+using Newtonsoft.Json.Serialization;
|
|
|
+using NodaTime;
|
|
|
+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;
|
|
|
+using System.Text.Json.Serialization;
|
|
|
|
|
|
namespace OASystem.API.OAMethodLib;
|
|
|
|
|
|
@@ -19,15 +21,31 @@ public static class PayrollConfig
|
|
|
public const decimal MealSubsidyPerDay = 10m;
|
|
|
|
|
|
// ========== 迟到早退规则 ==========
|
|
|
- public const int FreeTotalMinutes = 10; // 累计免罚分钟(超过此值才罚款)
|
|
|
- public const decimal FinePerEvent = 50m; // 每次事件罚款金额
|
|
|
+ /// <summary>
|
|
|
+ /// 累计免罚分钟(超过此值才罚款)
|
|
|
+ /// </summary>
|
|
|
+ public const int FreeTotalMinutes = 10;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 每次事件罚款金额
|
|
|
+ /// </summary>
|
|
|
+ public const decimal FinePerEvent = 50m;
|
|
|
public const int WorkStartHour = 9;
|
|
|
public const int WorkEndHour = 18;
|
|
|
- public const int DefaultAllowSeconds = 300; // 默认允许迟到/早退秒数(当规则未设置时使用)
|
|
|
+ /// <summary>
|
|
|
+ /// 默认允许迟到/早退秒数(当规则未设置时使用)
|
|
|
+ /// </summary>
|
|
|
+ public const int DefaultAllowSeconds = 300;
|
|
|
|
|
|
// ========== 旷工判定 ==========
|
|
|
- public const int HalfDayMissMinutes = 60; // 半日旷工下限(分钟)
|
|
|
- public const int FullDayMissMinutes = 180; // 全日旷工阈值(分钟)
|
|
|
+ /// <summary>
|
|
|
+ /// 半日旷工下限(分钟)
|
|
|
+ /// </summary>
|
|
|
+ public const int HalfDayMissMinutes = 60;
|
|
|
+ /// <summary>
|
|
|
+ /// 全日旷工阈值(分钟)
|
|
|
+ /// </summary>
|
|
|
+ public const int FullDayMissMinutes = 180;
|
|
|
|
|
|
// ========== 补卡规则 ==========
|
|
|
public const int ProbationFreeCardCount = 2;
|
|
|
@@ -37,125 +55,44 @@ public static class PayrollConfig
|
|
|
|
|
|
// ========== 请假餐补规则 ==========
|
|
|
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>();
|
|
|
- }
|
|
|
-
|
|
|
+ public const decimal OvertimeMealSubsidy = 12m;
|
|
|
/// <summary>
|
|
|
- /// 考勤扣款结果
|
|
|
+ /// 加班起始小时(20点)
|
|
|
/// </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; }
|
|
|
- }
|
|
|
-
|
|
|
+ public const int OvertimeStartHour = 20;
|
|
|
/// <summary>
|
|
|
- /// 扣款分类容器
|
|
|
+ /// 加班起始分钟
|
|
|
/// </summary>
|
|
|
- private class ExItems
|
|
|
- {
|
|
|
- public string? Type { get; set; }
|
|
|
- public object? ExItemInfo { get; set; }
|
|
|
- }
|
|
|
+ public const int OvertimeStartMinute = 0;
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// 请假/调休/出差明细(用于跨月拆分)
|
|
|
- /// </summary>
|
|
|
- private class LeaveDetailItem
|
|
|
+ /// <summary>公司打卡地址白名单(根据实际设备ID填写)</summary>
|
|
|
+ public static readonly List<string> CompanyLocations = new()
|
|
|
{
|
|
|
- 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; }
|
|
|
- }
|
|
|
+ "ZK-T1",
|
|
|
+ "蓝牙考勤设备",
|
|
|
+ "武侯区成都银泰中心in99(名都路西)"
|
|
|
+ };
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// 判断是否为工作日(基于规则中的特殊日期和系统日历)
|
|
|
- /// </summary>
|
|
|
- private static bool IsWorkDayForRule( DateTime date, List<Sys_Calendar> sysCalendars)
|
|
|
+ /// <summary>公司WiFi名称白名单</summary>
|
|
|
+ public static readonly List<string> CompanyWifiNames = new()
|
|
|
{
|
|
|
- var calendar = sysCalendars.FirstOrDefault(c => c.Dt == date.ToString("yyyy-MM-dd"));
|
|
|
- if (calendar != null)
|
|
|
- return calendar.IsWorkDay;
|
|
|
+ "Pan-American",
|
|
|
+ };
|
|
|
+}
|
|
|
|
|
|
- // 默认周一到周五为工作日
|
|
|
- return date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday;
|
|
|
- }
|
|
|
+/// <summary>
|
|
|
+/// 工资计算
|
|
|
+/// </summary>
|
|
|
+public static class PayrollComputation_v1
|
|
|
+{
|
|
|
+ private static Result _result = new();
|
|
|
+ private static readonly IQiYeWeChatApiService _qiYeWeChatApiService = AutofacIocManager.Instance.GetService<IQiYeWeChatApiService>();
|
|
|
+ private static readonly SqlSugarClient _sqlSugar = AutofacIocManager.Instance.GetService<SqlSugarClient>();
|
|
|
|
|
|
/// <summary>
|
|
|
/// 计算工资
|
|
|
@@ -235,11 +172,14 @@ public static class PayrollComputation_v1
|
|
|
punchResult = await CalculatePunchCorrectionAsync(acctid, pm_wsInfo.Floats, dailyWage, startDt, endDt);
|
|
|
}
|
|
|
|
|
|
- // 计算餐补
|
|
|
+ // 计算加班餐补明细(按天)
|
|
|
+ var (overtimeMeal, overtimeDetails) = await CalculateOvertimeMealSubsidyWithDetailsAsync(acctid, 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 mealActual = mealSubsidy - mealDeductionTotal + overtimeMeal; // 加班餐补累加到餐补总额
|
|
|
|
|
|
// 汇总扣款
|
|
|
decimal totalDeduction = attendanceResult.TotalDeduction + leaveResult.TotalDeduction + punchResult.TotalDeduction
|
|
|
@@ -249,21 +189,43 @@ public static class PayrollComputation_v1
|
|
|
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 });
|
|
|
+ var exItemsList = new List<ExItems>()
|
|
|
+ {
|
|
|
+ new(){ Type = "打卡", ExItemInfo = new List<ExItem>()},
|
|
|
+ new(){ Type = "假勤", ExItemInfo = new List<ExItem>()},
|
|
|
+ new(){ Type = "打卡补卡", ExItemInfo = new List<ExItem>()},
|
|
|
+ };
|
|
|
+
|
|
|
+ // 根据类型赋值
|
|
|
+ var punchItem = exItemsList.FirstOrDefault(x => x.Type == "打卡");
|
|
|
+ if (punchItem != null && attendanceResult.Details.Count > 0)
|
|
|
+ punchItem.ExItemInfo = attendanceResult.Details;
|
|
|
+
|
|
|
+ var leaveItem = exItemsList.FirstOrDefault(x => x.Type == "假勤");
|
|
|
+ if (leaveItem != null)
|
|
|
+ {
|
|
|
+ var combined = new List<ExItem>();
|
|
|
+ if (leaveResult.Details.Any()) combined.AddRange(leaveResult.Details);
|
|
|
+ if (overtimeDetails.Any()) combined.AddRange(overtimeDetails);
|
|
|
+ leaveItem.ExItemInfo = combined;
|
|
|
+ }
|
|
|
+
|
|
|
+ var punchCardItem = exItemsList.FirstOrDefault(x => x.Type == "打卡补卡");
|
|
|
+ if (punchCardItem != null && punchResult.Details.Count > 0)
|
|
|
+ punchCardItem.ExItemInfo = punchResult.Details;
|
|
|
|
|
|
- string exItemsRemark = exItemsList.Count > 0 ? JsonConvert.SerializeObject(exItemsList) : "";
|
|
|
+ string exItemsRemark = JsonConvert.SerializeObject(exItemsList,
|
|
|
+ new JsonSerializerSettings
|
|
|
+ {
|
|
|
+ DateFormatString = "yyyy-MM-dd HH:mm:ss",
|
|
|
+ NullValueHandling = NullValueHandling.Ignore
|
|
|
+ });
|
|
|
|
|
|
// 更新工资表
|
|
|
UpdateWageSheet(pm_wsInfo, thisYearMonth, startDt, endDt, workDays, actualWorkDays,
|
|
|
mealActual, leaveResult, attendanceResult, punchResult, shouldTotal, totalDeduction, afterTax, userId, exItemsRemark);
|
|
|
}
|
|
|
+
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
@@ -276,7 +238,142 @@ public static class PayrollComputation_v1
|
|
|
return _result;
|
|
|
}
|
|
|
|
|
|
- // =============================== 辅助方法 ===============================
|
|
|
+
|
|
|
+ #region 内部类定义
|
|
|
+
|
|
|
+ /// <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
|
|
|
+ {
|
|
|
+ [JsonProperty("SubTypeId")]
|
|
|
+ public int SubTypeId { get; set; }
|
|
|
+ [JsonProperty("SubType")]
|
|
|
+ public string? SubType { get; set; }
|
|
|
+ [JsonProperty("Deduction")]
|
|
|
+ public decimal Deduction { get; set; } = 0.00M;
|
|
|
+ [JsonProperty("MealDeduction")]
|
|
|
+ public decimal MealDeduction { get; set; } = 0.00M;
|
|
|
+ [JsonProperty("StartTimeDt")]
|
|
|
+ public DateTime StartTimeDt { get; set; }
|
|
|
+ [JsonProperty("EndTimeDt")]
|
|
|
+ public DateTime EndTimeDt { get; set; }
|
|
|
+ [JsonProperty("Duration")]
|
|
|
+ public decimal Duration { get; set; }
|
|
|
+ [JsonProperty("Unit")]
|
|
|
+ public string Unit { get; set; } = "小时";
|
|
|
+ [JsonProperty("Reason")]
|
|
|
+ public string? Reason { get; set; }
|
|
|
+ [JsonProperty("Apply_time_dt")]
|
|
|
+ public DateTime Apply_time_dt { get; set; }
|
|
|
+ [JsonProperty("Approval_name")]
|
|
|
+ public List<string>? Approval_name { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 扣款分类容器
|
|
|
+ /// </summary>
|
|
|
+ private class ExItems
|
|
|
+ {
|
|
|
+ [JsonProperty("Type")]
|
|
|
+ public string? Type { get; set; }
|
|
|
+
|
|
|
+ [JsonProperty("Ex_ItemInfo")]
|
|
|
+ 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; }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 辅助方法
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 加班时段(支持跨天,餐补归属开始日期)
|
|
|
+ /// </summary>
|
|
|
+ private class OvertimePeriodDetail
|
|
|
+ {
|
|
|
+ public DateTime StartDate { get; set; } // 加班开始日期(仅日期)
|
|
|
+ public DateTime EndTime { 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;
|
|
|
+ }
|
|
|
|
|
|
private static decimal GetTotalSalaryBase(Pm_WageSheet wageSheet)
|
|
|
{
|
|
|
@@ -305,8 +402,8 @@ public static class PayrollComputation_v1
|
|
|
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);
|
|
|
-
|
|
|
+ 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;
|
|
|
@@ -387,6 +484,8 @@ public static class PayrollComputation_v1
|
|
|
SubTypeId = 1,
|
|
|
SubType = "迟到",
|
|
|
Deduction = deduction,
|
|
|
+ Duration = minutes,
|
|
|
+ Unit = "分钟",
|
|
|
StartTimeDt = date,
|
|
|
Reason = reason
|
|
|
});
|
|
|
@@ -404,6 +503,8 @@ public static class PayrollComputation_v1
|
|
|
SubTypeId = 4,
|
|
|
SubType = "旷工(迟到超时)",
|
|
|
Deduction = ded,
|
|
|
+ Duration = minutes,
|
|
|
+ Unit = "分钟",
|
|
|
StartTimeDt = date,
|
|
|
Reason = reason + $"迟到{minutes}分钟,按旷工半天处理,扣除餐补"
|
|
|
});
|
|
|
@@ -418,6 +519,8 @@ public static class PayrollComputation_v1
|
|
|
SubTypeId = 4,
|
|
|
SubType = "旷工(迟到超时)",
|
|
|
Deduction = ded,
|
|
|
+ Duration = minutes,
|
|
|
+ Unit = "分钟",
|
|
|
StartTimeDt = date,
|
|
|
Reason = reason + $"迟到{minutes}分钟,按旷工全天处理,扣除餐补"
|
|
|
});
|
|
|
@@ -462,6 +565,8 @@ public static class PayrollComputation_v1
|
|
|
SubTypeId = 2,
|
|
|
SubType = "早退",
|
|
|
Deduction = deduction,
|
|
|
+ Duration = minutes,
|
|
|
+ Unit = "分钟",
|
|
|
StartTimeDt = date,
|
|
|
Reason = reason
|
|
|
});
|
|
|
@@ -477,9 +582,11 @@ public static class PayrollComputation_v1
|
|
|
result.Details.Add(new ExItem
|
|
|
{
|
|
|
SubTypeId = 4,
|
|
|
- SubType = "旷工(早退超时)",
|
|
|
+ SubType = "旷工",
|
|
|
Deduction = ded,
|
|
|
StartTimeDt = date,
|
|
|
+ Duration = minutes,
|
|
|
+ Unit = "分钟",
|
|
|
Reason = reason + $"早退{minutes}分钟,按旷工半天处理,扣除餐补"
|
|
|
});
|
|
|
}
|
|
|
@@ -491,9 +598,11 @@ public static class PayrollComputation_v1
|
|
|
result.Details.Add(new ExItem
|
|
|
{
|
|
|
SubTypeId = 4,
|
|
|
- SubType = "旷工(早退超时)",
|
|
|
+ SubType = "旷工",
|
|
|
Deduction = ded,
|
|
|
StartTimeDt = date,
|
|
|
+ Duration = minutes,
|
|
|
+ Unit = "分钟",
|
|
|
Reason = reason + $"早退{minutes}分钟,按旷工全天处理,扣除餐补"
|
|
|
});
|
|
|
}
|
|
|
@@ -509,9 +618,11 @@ public static class PayrollComputation_v1
|
|
|
result.Details.Add(new ExItem
|
|
|
{
|
|
|
SubTypeId = 4,
|
|
|
- SubType = "旷工(缺卡)",
|
|
|
+ SubType = "旷工",
|
|
|
Deduction = ded,
|
|
|
StartTimeDt = date,
|
|
|
+ Duration = 0.5m,
|
|
|
+ Unit = "天",
|
|
|
Reason = reason + "缺卡,按半天旷工处理,扣除餐补"
|
|
|
});
|
|
|
}
|
|
|
@@ -528,6 +639,8 @@ public static class PayrollComputation_v1
|
|
|
SubType = "旷工",
|
|
|
Deduction = ded,
|
|
|
StartTimeDt = date,
|
|
|
+ Duration = 0.5m,
|
|
|
+ Unit = "天",
|
|
|
Reason = reason + "旷工半天,扣除餐补"
|
|
|
});
|
|
|
}
|
|
|
@@ -541,6 +654,8 @@ public static class PayrollComputation_v1
|
|
|
SubTypeId = 4,
|
|
|
SubType = "旷工",
|
|
|
Deduction = ded,
|
|
|
+ Duration = 1m,
|
|
|
+ Unit = "天",
|
|
|
StartTimeDt = date,
|
|
|
Reason = reason + "旷工全天,扣除餐补"
|
|
|
});
|
|
|
@@ -623,12 +738,12 @@ public static class PayrollComputation_v1
|
|
|
// 年假、调休假不扣薪、视情况扣除餐补
|
|
|
if (isMealDeduct)
|
|
|
{
|
|
|
- if (item.Hours >=0 || item.Days >= 0.5m)
|
|
|
+ if (item.Hours >= 0 || item.Days >= 0.5m)
|
|
|
{
|
|
|
leaveMeal = PayrollConfig.MealSubsidyPerDay;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
result.MealDeduction += leaveMeal;
|
|
|
|
|
|
// 产生扣款时记录
|
|
|
@@ -850,6 +965,116 @@ public static class PayrollComputation_v1
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 获取审核通过的加班时段明细(结束时间 ≥ 20:00,且已裁剪至当月范围内)
|
|
|
+ /// </summary>
|
|
|
+ private static async Task<List<OvertimePeriodDetail>> GetApprovedOvertimePeriodsDetailAsync(string acctid, DateTime monthStart, DateTime monthEnd)
|
|
|
+ {
|
|
|
+ var result = new List<OvertimePeriodDetail>();
|
|
|
+ if (string.IsNullOrEmpty(acctid)) return result;
|
|
|
+
|
|
|
+ // 加班审批的 subType = 5
|
|
|
+ var overtimeApprovals = await _qiYeWeChatApiService.GetApprovalDetailsAsync(monthStart, monthEnd, acctid, 2, 5);
|
|
|
+ foreach (var sp in overtimeApprovals)
|
|
|
+ {
|
|
|
+ if (sp.sp_status != 2) continue; // 只取已通过
|
|
|
+ var applyData = sp.apply_data;
|
|
|
+ if (applyData == null) continue;
|
|
|
+
|
|
|
+ var attendanceControl = applyData.contents?.FirstOrDefault(c => c.control == "Attendance");
|
|
|
+ if (attendanceControl?.value?.attendance?.date_range == null) continue;
|
|
|
+
|
|
|
+ var dateRange = attendanceControl.value.attendance.date_range;
|
|
|
+ DateTime start = dateRange.new_begin_dt;
|
|
|
+ DateTime end = dateRange.new_end_dt;
|
|
|
+ if (start > end) (start, end) = (end, start);
|
|
|
+
|
|
|
+ // 只处理结束时间 ≥ 20:00 的加班(否则没有加班餐补)
|
|
|
+ if (end.TimeOfDay < new TimeSpan(PayrollConfig.OvertimeStartHour, PayrollConfig.OvertimeStartMinute, 0))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ // 跨月裁剪:只保留与当前月份重叠的部分(结束时间不能超出当月最后一天)
|
|
|
+ if (end < monthStart || start > monthEnd) continue;
|
|
|
+ DateTime actualEnd = end > monthEnd ? monthEnd.Date.AddDays(1).AddSeconds(-1) : end;
|
|
|
+
|
|
|
+ result.Add(new OvertimePeriodDetail
|
|
|
+ {
|
|
|
+ StartDate = start,
|
|
|
+ EndTime = actualEnd
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return result.OrderBy(x => x.StartDate).ToList();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 计算加班餐补(基于审批与打卡记录)
|
|
|
+ /// </summary>
|
|
|
+ private static async Task<(decimal total, List<ExItem> details)> CalculateOvertimeMealSubsidyWithDetailsAsync(
|
|
|
+ string acctid, DateTime monthStart, DateTime monthEnd)
|
|
|
+ {
|
|
|
+ // 结束时间 默认加一天 至第二天的凌晨零点
|
|
|
+ monthEnd = monthEnd.AddDays(1);
|
|
|
+ var details = new List<ExItem>();
|
|
|
+ var periods = await GetApprovedOvertimePeriodsDetailAsync(acctid, monthStart, monthEnd);
|
|
|
+ if (periods.Count == 0) return (0, details);
|
|
|
+
|
|
|
+ // 获取该员工当月的所有打卡明细
|
|
|
+ var resp = await _qiYeWeChatApiService.GetCheckinDataAsync(new List<string> { acctid }, 3, monthStart, monthEnd);
|
|
|
+ if (resp.errcode != 0) return (0, details);
|
|
|
+
|
|
|
+ // 筛选符合条件的打卡记录:
|
|
|
+ // 1. 下班打卡(IsEveningCheckIn)
|
|
|
+ // 2. 非外出打卡
|
|
|
+ // 3. 无缺卡异常(IsMissPunch)
|
|
|
+ // 4. 打卡地点在公司白名单内(位置名称或WiFi名称匹配)
|
|
|
+ // 5. 打卡时间 >= 当天 20:00(在后续 valid 判断中已包含,这里先放宽筛选)
|
|
|
+ var validCheckins = resp.checkindata
|
|
|
+ .Where(x => x.IsEveningCheckIn && !x.IsOutCheckIn && !x.IsMissPunch &&
|
|
|
+ (PayrollConfig.CompanyLocations.Contains(x.location_title) ||
|
|
|
+ PayrollConfig.CompanyWifiNames.Contains(x.wifiname)))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ decimal total = 0;
|
|
|
+ foreach (var period in periods)
|
|
|
+ {
|
|
|
+ // 查找是否存在一条打卡记录满足:
|
|
|
+ // - 打卡时间 >= 加班结束时间(period.EndTime)
|
|
|
+ // - 跨日允许次日凌晨 6:00 之前
|
|
|
+ bool valid = validCheckins.Any(r =>
|
|
|
+ {
|
|
|
+ DateTime checkTime = r.checkin_time_dt;
|
|
|
+ DateTime end = period.EndTime;
|
|
|
+ if (checkTime < end) return false;
|
|
|
+
|
|
|
+ // 如果打卡日期 > 加班结束日期(跨天)
|
|
|
+ if (checkTime.Date > end.Date)
|
|
|
+ {
|
|
|
+ // 只允许跨一天,且打卡时间不晚于次日 06:00
|
|
|
+ return checkTime.Date == end.Date.AddDays(1) && checkTime.TimeOfDay <= TimeSpan.FromHours(6);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (valid)
|
|
|
+ {
|
|
|
+ total += PayrollConfig.OvertimeMealSubsidy;
|
|
|
+ details.Add(new ExItem
|
|
|
+ {
|
|
|
+ SubTypeId = 6,
|
|
|
+ SubType = "加班餐补",
|
|
|
+ Deduction = 0,
|
|
|
+ MealDeduction = 0,
|
|
|
+ StartTimeDt = period.StartDate,
|
|
|
+ EndTimeDt = period.EndTime,
|
|
|
+ Duration = PayrollConfig.OvertimeMealSubsidy,
|
|
|
+ Unit = "元",
|
|
|
+ Reason = $"加班餐补({period.StartDate:yyyy-MM-dd HH:mm} 加班至 {period.EndTime:HH:mm} 后合规打卡)"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return (total, details);
|
|
|
+ }
|
|
|
+
|
|
|
private static string GetLeaveTypeName(int typeId)
|
|
|
{
|
|
|
return typeId switch
|
|
|
@@ -887,6 +1112,8 @@ public static class PayrollComputation_v1
|
|
|
ws.Ex_ItemsRemark = exItemsRemark;
|
|
|
ws.LastUpdateUserId = userId;
|
|
|
ws.LastUpdateDt = DateTime.Now;
|
|
|
+ ws.CreateUserId = userId;
|
|
|
+ ws.CreateTime = DateTime.Now;
|
|
|
}
|
|
|
|
|
|
public static async Task<int> GetWorkDays(string yearMonth)
|
|
|
@@ -900,4 +1127,6 @@ public static class PayrollComputation_v1
|
|
|
{
|
|
|
return Math.Floor(value * 100) / 100;
|
|
|
}
|
|
|
+
|
|
|
+ #endregion
|
|
|
}
|