| | |
| | | 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.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.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.employee.HrmEmployeeMapper; |
| | | import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper; |
| | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.YearMonth; |
| | | import java.time.temporal.ChronoUnit; |
| | | import java.util.Collections; |
| | | 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; |
| | |
| | | private HrmSalaryPaymentMapper salaryPaymentMapper; |
| | | @Resource |
| | | private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper; |
| | | @Resource |
| | | private HrmEmployeeScheduleMapper employeeScheduleMapper; |
| | | @Resource |
| | | private HrmShiftMapper shiftMapper; |
| | | |
| | | @Override |
| | | public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) { |
| | |
| | | OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary); |
| | | BigDecimal overtimePay = overtimeSalary.pay(); |
| | | |
| | | // 按排班累计出勤天数与排班总工时(无排班时出勤天数回退默认 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); |
| | |
| | | .otherDeduction(otherDeduction) |
| | | .leaveDeduction(leaveDeduction) |
| | | .actualSalary(actualSalary) |
| | | .workDays(22) // 默认工作日 |
| | | .workDays(workDays) // 按排班累计应出勤天数,无排班时回退 22 |
| | | .overtimeHours(overtimeSalary.hours()) |
| | | .totalWorkHours(totalWorkHours) |
| | | .status(0) // 待确认 |
| | | .build(); |
| | | } |
| | |
| | | BigDecimal totalLeaveDeduction = BigDecimal.ZERO; |
| | | for (HrmLeaveApplicationDO application : leaveApplications) { |
| | | Integer leaveType = application.getLeaveType(); |
| | | BigDecimal duration = application.getDuration() != null ? application.getDuration() : BigDecimal.ZERO; |
| | | // 跨月请假按落在本核算月内的天数折算,避免整条请假被跳过或被重复计入 |
| | | BigDecimal duration = resolveDurationInMonth(application, monthStart, monthEnd); |
| | | BigDecimal deductionRatio = configMap.getOrDefault(leaveType, BigDecimal.ZERO); |
| | | |
| | | // 请假扣款 = 日工资 × 请假天数 × 扣款比例 |
| | |
| | | } |
| | | |
| | | return totalLeaveDeduction; |
| | | } |
| | | |
| | | /** |
| | | * 计算一笔请假落在本核算月内的请假时长 |
| | | * |
| | | * 1)请假整段都在本月内:直接取请假单上的时长; |
| | | * 2)请假跨月:按「本月内占用的自然日 / 请假总自然日」折算,例如 10-28 ~ 11-03 共 7 天, |
| | | * 其中 10 月占 4 天,则 10 月核算按 时长 × 4 / 7 计,11 月按 时长 × 3 / 7 计,避免重复或漏算。 |
| | | */ |
| | | private BigDecimal resolveDurationInMonth(HrmLeaveApplicationDO application, |
| | | LocalDateTime monthStart, LocalDateTime monthEnd) { |
| | | BigDecimal duration = application.getDuration() != null ? application.getDuration() : BigDecimal.ZERO; |
| | | LocalDateTime start = application.getStartTime(); |
| | | LocalDateTime end = application.getEndTime(); |
| | | if (start == null || end == null) { |
| | | return duration; |
| | | } |
| | | // 整段都在本月内 |
| | | if (!start.isBefore(monthStart) && !end.isAfter(monthEnd)) { |
| | | return duration; |
| | | } |
| | | long totalDays = ChronoUnit.DAYS.between(start.toLocalDate(), end.toLocalDate()) + 1; |
| | | if (totalDays <= 0) { |
| | | return duration; |
| | | } |
| | | LocalDateTime overlapStart = start.isBefore(monthStart) ? monthStart : start; |
| | | LocalDateTime overlapEnd = end.isAfter(monthEnd) ? monthEnd : end; |
| | | if (overlapStart.isAfter(overlapEnd)) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | long overlapDays = ChronoUnit.DAYS.between(overlapStart.toLocalDate(), overlapEnd.toLocalDate()) + 1; |
| | | return duration.multiply(BigDecimal.valueOf(overlapDays)) |
| | | .divide(BigDecimal.valueOf(totalDays), 4, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {} |
| | | |
| | | 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 |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void confirmSalaryCalculation(List<Long> ids) { |