| | |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryCalculationPageReqVO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmEmployeeScheduleDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO; |
| | |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDetailDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSocialSecuritySchemeDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmEmployeeScheduleMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRecordMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSocialSecurityMapper; |
| | |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentDetailMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSocialSecuritySchemeMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO; |
| | | import cn.iocoder.yudao.module.system.api.dept.DeptApi; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum; |
| | | import jakarta.annotation.Resource; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.YearMonth; |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | |
| | | @Resource |
| | | private HrmSocialSecuritySchemeMapper socialSecuritySchemeMapper; |
| | | @Resource |
| | | private HrmSocialSecuritySchemeService socialSecuritySchemeService; |
| | | @Resource |
| | | private HrmNoRedisDAO noRedisDAO; |
| | | @Resource |
| | | private DeptApi deptApi; |
| | |
| | | @Resource |
| | | private HrmLeaveApplicationMapper leaveApplicationMapper; |
| | | @Resource |
| | | private HrmOvertimeApplicationMapper overtimeApplicationMapper; |
| | | @Resource |
| | | private HrmSalaryStructureMapper salaryStructureMapper; |
| | | @Resource |
| | | private HrmLeaveTypeConfigMapper leaveTypeConfigMapper; |
| | | @Resource |
| | | private HrmSalaryPaymentMapper salaryPaymentMapper; |
| | | @Resource |
| | | private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper; |
| | | @Resource |
| | | private HrmEmployeeScheduleMapper employeeScheduleMapper; |
| | | @Resource |
| | | private HrmShiftMapper shiftMapper; |
| | | @Resource |
| | | private HrmAttendanceRecordMapper attendanceRecordMapper; |
| | | |
| | | @Override |
| | | public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) { |
| | |
| | | .toList(); |
| | | |
| | | // 2. 遍历员工进行薪酬核算 |
| | | // 部门 -> 方案 的解析结果在本次核算内缓存,避免逐员工重复查库 |
| | | Map<Long, HrmSocialSecuritySchemeDO> deptSchemeCache = new HashMap<>(); |
| | | for (HrmEmployeeDO employee : employees) { |
| | | // employeeId 是员工ID,用于关联薪酬档案、社保档案、核算记录 |
| | | Long employeeId = employee.getId(); |
| | |
| | | if (existCalculations.size() > 1) { |
| | | throw exception(SALARY_CALCULATION_DUPLICATE, employee.getName(), period); |
| | | } |
| | | continue; // 已存在则跳过 |
| | | // 已确认或已发放的记录不可自动改写;待确认记录允许重算,以吸收后来审批通过的加班。 |
| | | if (existCalculations.get(0).getStatus() != 0) { |
| | | continue; |
| | | } |
| | | } |
| | | |
| | | // 2.2 获取员工薪酬档案(按员工ID查询) |
| | |
| | | |
| | | // 2.3 获取员工社保公积金档案(按员工ID查询,取最新生效的) |
| | | HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId); |
| | | // 2.4 如果有社保档案,获取绑定的方案 |
| | | HrmSocialSecuritySchemeDO scheme = null; |
| | | if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) { |
| | | scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId()); |
| | | } |
| | | // 2.4 获取方案:员工档案指定 > 所在部门绑定 > 全局默认 |
| | | HrmSocialSecuritySchemeDO scheme = resolveScheme(socialSecurity, employee, effectiveDate, deptSchemeCache); |
| | | |
| | | // 2.5 构建核算记录(包含扣除计算,通过方案获取比例) |
| | | HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate); |
| | | |
| | | // 2.6 生成核算单号 |
| | | String no = noRedisDAO.generateSalaryCalculationNo(); |
| | | while (salaryCalculationMapper.selectByNo(no) != null) { |
| | | no = noRedisDAO.generateSalaryCalculationNo(); |
| | | // 2.6 保存:首次生成单号;已有待确认记录则保留单号并覆盖重算结果 |
| | | if (CollUtil.isNotEmpty(existCalculations)) { |
| | | HrmSalaryCalculationDO existing = existCalculations.get(0); |
| | | calculation.setId(existing.getId()); |
| | | calculation.setNo(existing.getNo()); |
| | | salaryCalculationMapper.updateById(calculation); |
| | | } else { |
| | | String no = noRedisDAO.generateSalaryCalculationNo(); |
| | | while (salaryCalculationMapper.selectByNo(no) != null) { |
| | | no = noRedisDAO.generateSalaryCalculationNo(); |
| | | } |
| | | calculation.setNo(no); |
| | | salaryCalculationMapper.insert(calculation); |
| | | } |
| | | calculation.setNo(no); |
| | | |
| | | // 2.7 保存核算记录 |
| | | salaryCalculationMapper.insert(calculation); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 构建默认的员工薪酬档案(基于员工表的基本工资) |
| | | * 构建默认的员工薪酬档案。基本工资统一取员工档案,绩效工资默认不自动生成。 |
| | | */ |
| | | private HrmEmployeeSalaryDO buildDefaultEmployeeSalary(Long employeeId, HrmEmployeeDO employee) { |
| | | BigDecimal baseSalary = employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO; |
| | | return HrmEmployeeSalaryDO.builder() |
| | | .userId(employeeId) // 使用员工ID |
| | | .salaryStructureId(employee.getSalaryStructureId()) |
| | | .baseSalary(employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO) |
| | | .baseSalary(baseSalary) |
| | | .performanceSalary(BigDecimal.ZERO) |
| | | .mealAllowance(BigDecimal.ZERO) |
| | | .transportAllowance(BigDecimal.ZERO) |
| | |
| | | BigDecimal mealAllowance = employeeSalary.getMealAllowance() != null ? employeeSalary.getMealAllowance() : BigDecimal.ZERO; |
| | | BigDecimal transportAllowance = employeeSalary.getTransportAllowance() != null ? employeeSalary.getTransportAllowance() : BigDecimal.ZERO; |
| | | BigDecimal otherAllowance = employeeSalary.getOtherAllowance() != null ? employeeSalary.getOtherAllowance() : BigDecimal.ZERO; |
| | | BigDecimal overtimePay = BigDecimal.ZERO; |
| | | |
| | | // 应发工资总额 |
| | | BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance) |
| | | .add(transportAllowance).add(otherAllowance).add(overtimePay); |
| | | // ========== 九泓工资表:标准项取自员工档案 ========== |
| | | BigDecimal standardSalary = nvl(employee.getStandardSalary()); |
| | | BigDecimal postSalary = nvl(employee.getPostSalary()); |
| | | BigDecimal overtimeHourlyRate = employee.getOvertimeHourlyRate(); |
| | | BigDecimal safetyAwardStandard = nvl(employee.getSafetyAwardStandard()); |
| | | BigDecimal performanceStandard = nvl(employee.getPerformanceStandard()); |
| | | // 标准工资未单独维护时,按「基本工资 + 岗位工资」合成 |
| | | if (standardSalary.signum() == 0 && (baseSalary.signum() > 0 || postSalary.signum() > 0)) { |
| | | standardSalary = baseSalary.add(postSalary); |
| | | } |
| | | |
| | | // 获取社保基数(员工档案中的个性化基数) |
| | | // 按排班累计应出勤天数与标准工时(无排班时回退 22 天 × 8 小时) |
| | | ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period); |
| | | Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22; |
| | | BigDecimal totalWorkHours = scheduleStat.totalWorkHours(); |
| | | BigDecimal standardWorkHours = scheduleStat.scheduledDays() > 0 |
| | | ? scheduleStat.totalWorkHours() |
| | | : BigDecimal.valueOf(workDays).multiply(BigDecimal.valueOf(8)); |
| | | |
| | | // 计薪出勤 = 标准出勤天数 - 缺勤天数(考勤记录中缺卡的天数,按天去重) |
| | | int absentDays = countAbsentDays(employeeId, period); |
| | | BigDecimal paidAttendanceDays = BigDecimal.valueOf(Math.max(workDays - absentDays, 0)); |
| | | |
| | | // 加班工资 / 法定假工资(加班工资标准为空时回退到 基本工资 / 21.75 / 8 × 倍率) |
| | | OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary, overtimeHourlyRate); |
| | | BigDecimal overtimeHours = overtimeSalary.hours(); |
| | | BigDecimal overtimePay = overtimeSalary.pay(); |
| | | BigDecimal legalHolidayHours = overtimeSalary.legalHolidayHours(); |
| | | BigDecimal legalHolidayPay = overtimeSalary.legalHolidayPay(); |
| | | |
| | | // 出勤折算比例,用于按实际计薪出勤折算标准项 |
| | | BigDecimal attendanceRatio = workDays > 0 |
| | | ? paidAttendanceDays.divide(BigDecimal.valueOf(workDays), 6, RoundingMode.HALF_UP) |
| | | : BigDecimal.ZERO; |
| | | BigDecimal standardAttendancePay = standardSalary.multiply(attendanceRatio).setScale(2, RoundingMode.HALF_UP); |
| | | BigDecimal safetyAward = safetyAwardStandard.multiply(attendanceRatio).setScale(2, RoundingMode.HALF_UP); |
| | | BigDecimal floatingPerformancePay = performanceStandard.multiply(attendanceRatio).setScale(2, RoundingMode.HALF_UP); |
| | | |
| | | // 应付工资 = 标准出勤工资 + 加班工资 + 法定假工资 + 安全环保奖 + 浮动绩效工资 |
| | | BigDecimal payableSalary = standardAttendancePay.add(overtimePay).add(legalHolidayPay) |
| | | .add(safetyAward).add(floatingPerformancePay); |
| | | |
| | | // 加项(工龄工资/夜班补助/学历补贴/体检/优秀员工奖/其他补助/部门绩效)与扣项 |
| | | // (漏打卡/迟到早退/其他扣款)生成时默认为 0,核算页可手工调整后保存 |
| | | BigDecimal seniorityPay = BigDecimal.ZERO; |
| | | BigDecimal nightShiftAllowance = BigDecimal.ZERO; |
| | | BigDecimal educationAllowance = BigDecimal.ZERO; |
| | | BigDecimal healthCheckAllowance = BigDecimal.ZERO; |
| | | BigDecimal excellentEmployeeAward = BigDecimal.ZERO; |
| | | BigDecimal otherSubsidy = BigDecimal.ZERO; |
| | | BigDecimal deptPerformance = BigDecimal.ZERO; |
| | | BigDecimal additionTotal = BigDecimal.ZERO; |
| | | BigDecimal missedPunchDeduction = BigDecimal.ZERO; |
| | | BigDecimal lateEarlyDeduction = BigDecimal.ZERO; |
| | | BigDecimal deductionTotal = BigDecimal.ZERO; |
| | | |
| | | // 计税口径:应付工资 + 加项合计 - 扣款合计 |
| | | BigDecimal totalIncome = payableSalary.add(additionTotal).subtract(deductionTotal); |
| | | |
| | | // 获取社保基数:员工社保档案 > 方案默认缴费基数 > 员工基本工资 |
| | | BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null) |
| | | ? socialSecurity.getSocialSecurityBase() : baseSalary; |
| | | ? socialSecurity.getSocialSecurityBase() |
| | | : (scheme != null && scheme.getDefaultSocialSecurityBase() != null |
| | | ? scheme.getDefaultSocialSecurityBase() : baseSalary); |
| | | |
| | | // 获取公积金基数(员工档案中的个性化基数) |
| | | BigDecimal housingFundBase = (socialSecurity != null && socialSecurity.getHousingFundBase() != null) |
| | | ? socialSecurity.getHousingFundBase() : baseSalary; |
| | | // 获取公积金基数:员工社保档案 > 方案默认公积金基数(大于 0 时)> 社保基数 |
| | | BigDecimal housingFundBase; |
| | | if (socialSecurity != null && socialSecurity.getHousingFundBase() != null) { |
| | | housingFundBase = socialSecurity.getHousingFundBase(); |
| | | } else if (scheme != null && scheme.getDefaultHousingFundBase() != null |
| | | && scheme.getDefaultHousingFundBase().compareTo(BigDecimal.ZERO) > 0) { |
| | | housingFundBase = scheme.getDefaultHousingFundBase(); |
| | | } else { |
| | | housingFundBase = socialSecurityBase; |
| | | } |
| | | |
| | | // 从方案中获取缴纳比例(优先使用方案中的比例,否则使用默认比例) |
| | | BigDecimal pensionPersonalRatio = (scheme != null && scheme.getPensionPersonalRatio() != null) |
| | |
| | | // 公积金扣除 = 基数 × 公积金个人比例 |
| | | BigDecimal housingFundDeduction = housingFundBase.multiply(housingFundPersonalRatio).setScale(2, RoundingMode.HALF_UP); |
| | | |
| | | // 计算请假扣款 |
| | | BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary); |
| | | // 请假扣款:新版工资表按「计薪出勤」折减标准出勤工资,不再单独重复扣一次,保留字段兼容旧数据 |
| | | BigDecimal leaveDeduction = BigDecimal.ZERO; |
| | | |
| | | // 计算个税扣除 |
| | | // 应纳税所得额 = 应发工资 - 社保扣除 - 公积金扣除 - 起征点(5000) |
| | |
| | | |
| | | BigDecimal otherDeduction = BigDecimal.ZERO; |
| | | |
| | | // 计算实发工资 = 应发 - 扣除 |
| | | BigDecimal totalDeduction = socialSecurityDeduction.add(housingFundDeduction) |
| | | .add(taxDeduction).add(otherDeduction).add(leaveDeduction); |
| | | BigDecimal actualSalary = totalIncome.subtract(totalDeduction); |
| | | // 税后工资 = 应付工资 + 加项合计 - 扣款合计 - 社保 - 公积金 - 个税 |
| | | BigDecimal actualSalary = payableSalary.add(additionTotal).subtract(deductionTotal) |
| | | .subtract(socialSecurityDeduction).subtract(housingFundDeduction) |
| | | .subtract(taxDeduction); |
| | | |
| | | return HrmSalaryCalculationDO.builder() |
| | | .period(period) |
| | | .userId(employeeId) // 使用员工ID |
| | | .deptId(employee.getDeptId()) |
| | | // 九泓工资表:标准项 |
| | | .standardSalary(standardSalary) |
| | | .postSalary(postSalary) |
| | | .overtimeHourlyRate(overtimeHourlyRate) |
| | | .safetyAwardStandard(safetyAwardStandard) |
| | | .performanceStandard(performanceStandard) |
| | | // 九泓工资表:出勤与计薪 |
| | | .paidAttendanceDays(paidAttendanceDays) |
| | | .standardWorkHours(standardWorkHours) |
| | | .legalHolidayHours(legalHolidayHours) |
| | | .standardAttendancePay(standardAttendancePay) |
| | | .legalHolidayPay(legalHolidayPay) |
| | | .safetyAward(safetyAward) |
| | | .floatingPerformancePay(floatingPerformancePay) |
| | | .payableSalary(payableSalary) |
| | | // 九泓工资表:加项 / 扣项 |
| | | .seniorityPay(seniorityPay) |
| | | .nightShiftAllowance(nightShiftAllowance) |
| | | .educationAllowance(educationAllowance) |
| | | .healthCheckAllowance(healthCheckAllowance) |
| | | .excellentEmployeeAward(excellentEmployeeAward) |
| | | .otherSubsidy(otherSubsidy) |
| | | .deptPerformance(deptPerformance) |
| | | .additionTotal(additionTotal) |
| | | .missedPunchDeduction(missedPunchDeduction) |
| | | .lateEarlyDeduction(lateEarlyDeduction) |
| | | .deductionTotal(deductionTotal) |
| | | // 兼容旧字段 |
| | | .baseSalary(baseSalary) |
| | | .performanceSalary(performanceSalary) |
| | | .overtimePay(overtimePay) |
| | |
| | | .otherDeduction(otherDeduction) |
| | | .leaveDeduction(leaveDeduction) |
| | | .actualSalary(actualSalary) |
| | | .workDays(22) // 默认工作日 |
| | | .overtimeHours(BigDecimal.ZERO) |
| | | .workDays(workDays) // 按排班累计应出勤天数,无排班时回退 22 |
| | | .overtimeHours(overtimeHours) |
| | | .totalWorkHours(totalWorkHours) |
| | | .status(0) // 待确认 |
| | | .build(); |
| | | } |
| | | |
| | | /** |
| | | * 解析员工核算用的社保公积金方案 |
| | | * |
| | | * 优先级:员工社保档案指定的方案(个别覆盖)> 员工所在部门绑定的方案 > 全局默认方案。 |
| | | * 都取不到时返回 null,由 {@link #buildSalaryCalculation} 使用默认比例常量。 |
| | | * |
| | | * @param deptSchemeCache 本次核算内「部门 -> 方案」的缓存(允许 null 值,用于缓存"该部门无方案") |
| | | */ |
| | | private HrmSocialSecuritySchemeDO resolveScheme(HrmEmployeeSocialSecurityDO socialSecurity, HrmEmployeeDO employee, |
| | | LocalDate periodDate, |
| | | Map<Long, HrmSocialSecuritySchemeDO> deptSchemeCache) { |
| | | if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) { |
| | | HrmSocialSecuritySchemeDO scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId()); |
| | | if (scheme != null) { |
| | | return scheme; |
| | | } |
| | | } |
| | | Long deptId = employee.getDeptId(); |
| | | if (!deptSchemeCache.containsKey(deptId)) { |
| | | deptSchemeCache.put(deptId, socialSecuritySchemeService.resolveSchemeByDeptId(deptId, periodDate)); |
| | | } |
| | | return deptSchemeCache.get(deptId); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | return totalLeaveDeduction; |
| | | } |
| | | |
| | | /** |
| | | * 汇总当月审批通过的加班申请。 |
| | | * |
| | | * 员工档案维护了「加班工资标准(元/小时)」时:加班工资 = 加班时长 × 单价, |
| | | * 法定假工资 = 法定假日时长 × 单价 × 法定节假日倍率(默认 3)。 |
| | | * 未维护单价时回退旧口径:基本工资 / 21.75 / 8 × 各类型倍率。 |
| | | * |
| | | * 法定节假日加班单独统计,不计入「加班时长」列,避免与「法定假工资」重复计薪。 |
| | | */ |
| | | private OvertimeSalary calculateOvertimeSalary(HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary, |
| | | String period, BigDecimal baseSalary, BigDecimal overtimeHourlyRate) { |
| | | if (employee.getUserId() == null) { |
| | | return OvertimeSalary.EMPTY; |
| | | } |
| | | YearMonth month = YearMonth.parse(period); |
| | | List<HrmOvertimeApplicationDO> applications = overtimeApplicationMapper.selectApprovedByUserAndDateRange( |
| | | employee.getUserId(), month.atDay(1), month.atEndOfMonth()); |
| | | if (CollUtil.isEmpty(applications)) { |
| | | return OvertimeSalary.EMPTY; |
| | | } |
| | | Long structureId = employeeSalary.getSalaryStructureId() != null |
| | | ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId(); |
| | | HrmSalaryStructureDO structure = structureId == null ? null : salaryStructureMapper.selectById(structureId); |
| | | BigDecimal workdayMultiplier = structure != null && structure.getOvertimePayRatio() != null |
| | | ? structure.getOvertimePayRatio() : HrmOvertimeTypeEnum.WORKDAY.getSalaryMultiplier(); |
| | | BigDecimal restDayMultiplier = structure != null && structure.getRestDayOvertimePayRatio() != null |
| | | ? structure.getRestDayOvertimePayRatio() : HrmOvertimeTypeEnum.REST_DAY.getSalaryMultiplier(); |
| | | BigDecimal legalHolidayMultiplier = structure != null && structure.getLegalHolidayOvertimePayRatio() != null |
| | | ? structure.getLegalHolidayOvertimePayRatio() : HrmOvertimeTypeEnum.LEGAL_HOLIDAY.getSalaryMultiplier(); |
| | | BigDecimal hourlySalary = overtimeHourlyRate != null && overtimeHourlyRate.signum() > 0 |
| | | ? overtimeHourlyRate |
| | | : baseSalary.divide(new BigDecimal("21.75"), 8, RoundingMode.HALF_UP) |
| | | .divide(new BigDecimal("8"), 8, RoundingMode.HALF_UP); |
| | | BigDecimal totalHours = BigDecimal.ZERO; |
| | | BigDecimal totalPay = BigDecimal.ZERO; |
| | | BigDecimal legalHolidayHours = BigDecimal.ZERO; |
| | | BigDecimal legalHolidayPay = BigDecimal.ZERO; |
| | | for (HrmOvertimeApplicationDO application : applications) { |
| | | BigDecimal hours = application.getDuration() == null ? BigDecimal.ZERO : application.getDuration(); |
| | | HrmOvertimeTypeEnum type = HrmOvertimeTypeEnum.valueOfType(application.getOvertimeType()); |
| | | if (type == null || hours.signum() <= 0) { |
| | | continue; |
| | | } |
| | | if (type == HrmOvertimeTypeEnum.LEGAL_HOLIDAY) { |
| | | legalHolidayHours = legalHolidayHours.add(hours); |
| | | legalHolidayPay = legalHolidayPay.add(hourlySalary.multiply(hours).multiply(legalHolidayMultiplier)); |
| | | continue; |
| | | } |
| | | BigDecimal multiplier = type == HrmOvertimeTypeEnum.WORKDAY ? workdayMultiplier : restDayMultiplier; |
| | | totalHours = totalHours.add(hours); |
| | | totalPay = totalPay.add(hourlySalary.multiply(hours).multiply(multiplier)); |
| | | } |
| | | return new OvertimeSalary(totalHours.setScale(1, RoundingMode.HALF_UP), |
| | | totalPay.setScale(2, RoundingMode.HALF_UP), |
| | | legalHolidayHours.setScale(1, RoundingMode.HALF_UP), |
| | | legalHolidayPay.setScale(2, RoundingMode.HALF_UP)); |
| | | } |
| | | |
| | | /** |
| | | * 统计当月考勤记录中缺卡的天数(同一日期多次缺卡只算一天) |
| | | */ |
| | | private int countAbsentDays(Long employeeId, String period) { |
| | | YearMonth month = YearMonth.parse(period); |
| | | List<HrmAttendanceRecordDO> records = attendanceRecordMapper.selectListByUserIdAndDateBetween( |
| | | employeeId, month.atDay(1), month.atEndOfMonth()); |
| | | if (CollUtil.isEmpty(records)) { |
| | | return 0; |
| | | } |
| | | return (int) records.stream() |
| | | .filter(record -> HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType()) |
| | | || HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType())) |
| | | .map(HrmAttendanceRecordDO::getDate) |
| | | .distinct() |
| | | .count(); |
| | | } |
| | | |
| | | private static BigDecimal nvl(BigDecimal value) { |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private record OvertimeSalary(BigDecimal hours, BigDecimal pay, |
| | | BigDecimal legalHolidayHours, BigDecimal legalHolidayPay) { |
| | | private static final OvertimeSalary EMPTY = new OvertimeSalary( |
| | | BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO); |
| | | } |
| | | |
| | | private record ScheduleStat(int scheduledDays, BigDecimal totalWorkHours) {} |
| | | |
| | | /** |
| | | * 按排班表累计员工的出勤天数与排班总工时。 |
| | | * 仅统计有班次的排班记录,班次标准工时之和即为排班工时。 |
| | | */ |
| | | private ScheduleStat calculateScheduleStat(Long employeeId, String period) { |
| | | YearMonth yearMonth = YearMonth.parse(period); |
| | | List<HrmEmployeeScheduleDO> schedules = employeeScheduleMapper.selectListByEmployeeIdAndPeriod( |
| | | employeeId, yearMonth.atDay(1), yearMonth.atEndOfMonth()); |
| | | if (CollUtil.isEmpty(schedules)) { |
| | | return new ScheduleStat(0, BigDecimal.ZERO); |
| | | } |
| | | Set<Long> shiftIds = schedules.stream().map(HrmEmployeeScheduleDO::getShiftId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | Map<Long, HrmShiftDO> shiftMap = CollUtil.isEmpty(shiftIds) ? Collections.emptyMap() |
| | | : shiftMapper.selectBatchIds(shiftIds).stream() |
| | | .collect(Collectors.toMap(HrmShiftDO::getId, shift -> shift, (a, b) -> a)); |
| | | int scheduledDays = 0; |
| | | BigDecimal totalWorkHours = BigDecimal.ZERO; |
| | | for (HrmEmployeeScheduleDO schedule : schedules) { |
| | | HrmShiftDO shift = schedule.getShiftId() == null ? null : shiftMap.get(schedule.getShiftId()); |
| | | if (shift == null) { |
| | | continue; |
| | | } |
| | | scheduledDays++; |
| | | totalWorkHours = totalWorkHours.add(shift.getWorkHours() != null ? shift.getWorkHours() : BigDecimal.ZERO); |
| | | } |
| | | return new ScheduleStat(scheduledDays, totalWorkHours.setScale(2, RoundingMode.HALF_UP)); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | // 2. 遍历员工实时计算薪酬 |
| | | List<HrmSalaryCalculationDO> resultList = new java.util.ArrayList<>(); |
| | | // 部门 -> 方案 的解析结果在本次预览内缓存,避免逐员工重复查库 |
| | | Map<Long, HrmSocialSecuritySchemeDO> deptSchemeCache = new HashMap<>(); |
| | | for (HrmEmployeeDO employee : employees) { |
| | | Long employeeId = employee.getId(); |
| | | |
| | |
| | | |
| | | // 2.2 获取员工社保公积金档案(按员工ID查询,取最新生效的) |
| | | HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId); |
| | | // 2.3 如果有社保档案,获取绑定的方案 |
| | | HrmSocialSecuritySchemeDO scheme = null; |
| | | if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) { |
| | | scheme = socialSecuritySchemeMapper.selectById(socialSecurity.getSocialSecuritySchemeId()); |
| | | } |
| | | // 2.3 获取方案:员工档案指定 > 所在部门绑定 > 全局默认 |
| | | HrmSocialSecuritySchemeDO scheme = resolveScheme(socialSecurity, employee, effectiveDate, deptSchemeCache); |
| | | |
| | | // 2.4 构建核算记录(实时计算,不存储) |
| | | HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate); |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void saveSalaryCalculationList(List<HrmSalaryCalculationDO> calculationList) { |
| | | for (HrmSalaryCalculationDO calculation : calculationList) { |
| | | normalizeSalaryTotals(calculation); |
| | | if (calculation.getId() == null) { |
| | | // 新增记录,生成核算单号 |
| | | String no = noRedisDAO.generateSalaryCalculationNo(); |
| | |
| | | // 更新已有记录 |
| | | salaryCalculationMapper.updateById(calculation); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 保存前补齐合计字段。 |
| | | * |
| | | * 工资表的每个数值字段都允许在自动计算后手工调整,因此这里**只补空值、不覆盖提交值**: |
| | | * 前端预览页已经按同口径重算好应付工资/加项合计/扣款合计/税后工资并一起提交, |
| | | * 直接落库才能保住用户手工改过的数;只有字段为空(旧客户端、接口直调)时才按口径补算。 |
| | | */ |
| | | private void normalizeSalaryTotals(HrmSalaryCalculationDO calculation) { |
| | | if (calculation.getPayableSalary() == null && calculation.getStandardAttendancePay() != null) { |
| | | calculation.setPayableSalary(nvl(calculation.getStandardAttendancePay()) |
| | | .add(nvl(calculation.getOvertimePay())) |
| | | .add(nvl(calculation.getLegalHolidayPay())) |
| | | .add(nvl(calculation.getSafetyAward())) |
| | | .add(nvl(calculation.getFloatingPerformancePay())) |
| | | .setScale(2, RoundingMode.HALF_UP)); |
| | | } |
| | | if (calculation.getAdditionTotal() == null) { |
| | | BigDecimal additionTotal = nvl(calculation.getSeniorityPay()) |
| | | .add(nvl(calculation.getNightShiftAllowance())) |
| | | .add(nvl(calculation.getEducationAllowance())) |
| | | .add(nvl(calculation.getHealthCheckAllowance())) |
| | | .add(nvl(calculation.getExcellentEmployeeAward())) |
| | | .add(nvl(calculation.getOtherSubsidy())) |
| | | .add(nvl(calculation.getDeptPerformance())); |
| | | calculation.setAdditionTotal(additionTotal.setScale(2, RoundingMode.HALF_UP)); |
| | | } |
| | | if (calculation.getDeductionTotal() == null) { |
| | | BigDecimal deductionTotal = nvl(calculation.getMissedPunchDeduction()) |
| | | .add(nvl(calculation.getLateEarlyDeduction())) |
| | | .add(nvl(calculation.getOtherDeduction())); |
| | | calculation.setDeductionTotal(deductionTotal.setScale(2, RoundingMode.HALF_UP)); |
| | | } |
| | | if (calculation.getActualSalary() == null && calculation.getPayableSalary() != null) { |
| | | calculation.setActualSalary(calculation.getPayableSalary() |
| | | .add(nvl(calculation.getAdditionTotal())) |
| | | .subtract(nvl(calculation.getDeductionTotal())) |
| | | .subtract(nvl(calculation.getSocialSecurityDeduction())) |
| | | .subtract(nvl(calculation.getHousingFundDeduction())) |
| | | .subtract(nvl(calculation.getTaxDeduction())) |
| | | .setScale(2, RoundingMode.HALF_UP)); |
| | | } |
| | | } |
| | | |
| | |
| | | salaryCalculationMapper.deleteById(id); |
| | | } |
| | | |
| | | } |
| | | } |