|
|
@@ -0,0 +1,903 @@
|
|
|
+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;
|
|
|
+ }
|
|
|
+}
|