| | |
| | | package cn.iocoder.yudao.module.hrm.service.salary; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | 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.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.mes.api.feedback.MesProFeedbackWageApi; |
| | | import cn.iocoder.yudao.module.mes.api.feedback.dto.MesProFeedbackWageDetailRespDTO; |
| | | import cn.iocoder.yudao.module.system.api.dept.DeptApi; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum; |
| | | import jakarta.annotation.Resource; |
| | |
| | | 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; |
| | |
| | | private HrmEmployeeScheduleMapper employeeScheduleMapper; |
| | | @Resource |
| | | private HrmShiftMapper shiftMapper; |
| | | @Resource |
| | | private MesProFeedbackWageApi feedbackWageApi; |
| | | |
| | | @Override |
| | | public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) { |
| | |
| | | .filter(e -> deptId == null || deptId.equals(e.getDeptId())) |
| | | .toList(); |
| | | |
| | | // 1.1 一次汇总所有员工当月的生产报工工资,避免逐人查询 |
| | | Map<Long, BigDecimal> workWageMap = loadWorkWageMap(employees, period); |
| | | // 1.2 一次加载涉及部门的最新社保公积金档案,避免逐人查询 |
| | | Map<Long, HrmEmployeeSocialSecurityDO> deptSocialSecurityMap = loadDeptSocialSecurityMap(employees); |
| | | |
| | | // 2. 遍历员工进行薪酬核算 |
| | | for (HrmEmployeeDO employee : employees) { |
| | | // employeeId 是员工ID,用于关联薪酬档案、社保档案、核算记录 |
| | |
| | | employeeSalary = buildDefaultEmployeeSalary(employeeId, employee); |
| | | } |
| | | |
| | | // 2.3 获取员工社保公积金档案(按员工ID查询,取最新生效的) |
| | | HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId); |
| | | // 2.3 获取员工所在部门的社保公积金档案(按部门取最新生效的) |
| | | HrmEmployeeSocialSecurityDO socialSecurity = employee.getDeptId() == null |
| | | ? null : deptSocialSecurityMap.get(employee.getDeptId()); |
| | | // 2.4 如果有社保档案,获取绑定的方案 |
| | | HrmSocialSecuritySchemeDO scheme = null; |
| | | if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) { |
| | |
| | | } |
| | | |
| | | // 2.5 构建核算记录(包含扣除计算,通过方案获取比例) |
| | | HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate); |
| | | HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, |
| | | socialSecurity, scheme, effectiveDate, workWageMap); |
| | | |
| | | // 2.6 保存:首次生成单号;已有待确认记录则保留单号并覆盖重算结果 |
| | | if (CollUtil.isNotEmpty(existCalculations)) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 汇总核算周期内所有员工的生产报工工资。 |
| | | * |
| | | * <p>报工单记录的是报工人(系统用户ID),薪酬核算记录的是员工ID,两者需通过 |
| | | * {@link HrmEmployeeDO#getUserId()} 映射;没有绑定系统账号的员工不产生报工工资。 |
| | | */ |
| | | private Map<Long, BigDecimal> loadWorkWageMap(List<HrmEmployeeDO> employees, String period) { |
| | | Set<Long> userIds = employees.stream() |
| | | .map(HrmEmployeeDO::getUserId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | if (CollUtil.isEmpty(userIds)) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | YearMonth month = YearMonth.parse(period); |
| | | return feedbackWageApi.getWageAmountMap(userIds, month.atDay(1).atStartOfDay(), |
| | | month.atEndOfMonth().atTime(23, 59, 59)); |
| | | } |
| | | |
| | | /** |
| | | * 加载涉及部门各自最新生效的社保公积金档案。 |
| | | * |
| | | * <p>档案按部门维护,部门下所有员工共用同一份档案;查询结果已按生效日期倒序, |
| | | * 每个部门取第一条即可。 |
| | | */ |
| | | private Map<Long, HrmEmployeeSocialSecurityDO> loadDeptSocialSecurityMap(List<HrmEmployeeDO> employees) { |
| | | Set<Long> deptIds = employees.stream() |
| | | .map(HrmEmployeeDO::getDeptId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | if (CollUtil.isEmpty(deptIds)) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | Map<Long, HrmEmployeeSocialSecurityDO> result = new HashMap<>(); |
| | | for (HrmEmployeeSocialSecurityDO socialSecurity : employeeSocialSecurityMapper.selectListByDeptIds(deptIds)) { |
| | | result.putIfAbsent(socialSecurity.getDeptId(), socialSecurity); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 构建默认的员工薪酬档案。基本工资统一取员工档案,绩效工资默认不自动生成。 |
| | | */ |
| | | private HrmEmployeeSalaryDO buildDefaultEmployeeSalary(Long employeeId, HrmEmployeeDO employee) { |
| | |
| | | HrmEmployeeSalaryDO employeeSalary, |
| | | HrmEmployeeSocialSecurityDO socialSecurity, |
| | | HrmSocialSecuritySchemeDO scheme, |
| | | LocalDate effectiveDate) { |
| | | LocalDate effectiveDate, |
| | | Map<Long, BigDecimal> workWageMap) { |
| | | // 计算应发薪资 |
| | | BigDecimal baseSalary = employeeSalary.getBaseSalary() != null ? employeeSalary.getBaseSalary() : BigDecimal.ZERO; |
| | | BigDecimal performanceSalary = employeeSalary.getPerformanceSalary() != null ? employeeSalary.getPerformanceSalary() : BigDecimal.ZERO; |
| | |
| | | OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary); |
| | | BigDecimal overtimePay = overtimeSalary.pay(); |
| | | |
| | | // 报工工资:报工人(系统用户ID)在核算周期内已审批通过的报工工资合计 |
| | | BigDecimal workWage = employee.getUserId() != null |
| | | ? workWageMap.getOrDefault(employee.getUserId(), BigDecimal.ZERO) |
| | | : BigDecimal.ZERO; |
| | | |
| | | // 按排班累计出勤天数与排班总工时(无排班时出勤天数回退默认 22) |
| | | ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period); |
| | | Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22; |
| | | BigDecimal totalWorkHours = scheduleStat.totalWorkHours(); |
| | | |
| | | // 应发工资总额 |
| | | // 应发工资总额(含报工工资) |
| | | BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance) |
| | | .add(transportAllowance).add(otherAllowance).add(overtimePay); |
| | | .add(transportAllowance).add(otherAllowance).add(overtimePay).add(workWage); |
| | | |
| | | // 获取社保基数:员工社保档案 > 员工基本工资 |
| | | BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null) |
| | |
| | | .mealAllowance(mealAllowance) |
| | | .transportAllowance(transportAllowance) |
| | | .otherAllowance(otherAllowance) |
| | | .workWage(workWage) |
| | | .socialSecurityDeduction(socialSecurityDeduction) |
| | | .housingFundDeduction(housingFundDeduction) |
| | | .taxDeduction(taxDeduction) |
| | |
| | | .filter(e -> deptId == null || deptId.equals(e.getDeptId())) |
| | | .toList(); |
| | | |
| | | // 2. 遍历员工实时计算薪酬 |
| | | // 2. 一次汇总所有员工当月的生产报工工资 |
| | | Map<Long, BigDecimal> workWageMap = loadWorkWageMap(employees, period); |
| | | // 2.1 一次加载涉及部门的最新社保公积金档案 |
| | | Map<Long, HrmEmployeeSocialSecurityDO> deptSocialSecurityMap = loadDeptSocialSecurityMap(employees); |
| | | |
| | | // 3. 遍历员工实时计算薪酬 |
| | | List<HrmSalaryCalculationDO> resultList = new java.util.ArrayList<>(); |
| | | for (HrmEmployeeDO employee : employees) { |
| | | Long employeeId = employee.getId(); |
| | |
| | | employeeSalary = buildDefaultEmployeeSalary(employeeId, employee); |
| | | } |
| | | |
| | | // 2.2 获取员工社保公积金档案(按员工ID查询,取最新生效的) |
| | | HrmEmployeeSocialSecurityDO socialSecurity = employeeSocialSecurityMapper.selectLatestByUserId(employeeId); |
| | | // 2.2 获取员工所在部门的社保公积金档案(按部门取最新生效的) |
| | | HrmEmployeeSocialSecurityDO socialSecurity = employee.getDeptId() == null |
| | | ? null : deptSocialSecurityMap.get(employee.getDeptId()); |
| | | // 2.3 如果有社保档案,获取绑定的方案 |
| | | HrmSocialSecuritySchemeDO scheme = null; |
| | | if (socialSecurity != null && socialSecurity.getSocialSecuritySchemeId() != null) { |
| | |
| | | } |
| | | |
| | | // 2.4 构建核算记录(实时计算,不存储) |
| | | HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate); |
| | | HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, |
| | | socialSecurity, scheme, effectiveDate, workWageMap); |
| | | resultList.add(calculation); |
| | | } |
| | | return resultList; |
| | |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public List<MesProFeedbackWageDetailRespDTO> getWorkWageDetailList(String period, Long employeeId) { |
| | | if (employeeId == null || StrUtil.isBlank(period)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | HrmEmployeeDO employee = employeeMapper.selectById(employeeId); |
| | | if (employee == null || employee.getUserId() == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | YearMonth month = YearMonth.parse(period); |
| | | return feedbackWageApi.getWageDetailList(employee.getUserId(), |
| | | month.atDay(1).atStartOfDay(), month.atEndOfMonth().atTime(23, 59, 59)); |
| | | } |
| | | |
| | | /** |
| | | * 内部生成发放台账方法(供自动生成调用) |
| | | */ |