| | |
| | | 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.HrmAttendanceSummaryDO; |
| | | 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.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.HrmAttendanceSummaryMapper; |
| | | 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; |
| | |
| | | private HrmEmployeeScheduleMapper employeeScheduleMapper; |
| | | @Resource |
| | | private HrmShiftMapper shiftMapper; |
| | | @Resource |
| | | private HrmAttendanceSummaryMapper attendanceSummaryMapper; |
| | | |
| | | @Override |
| | | public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) { |
| | |
| | | HrmSocialSecuritySchemeDO scheme, |
| | | LocalDate effectiveDate) { |
| | | // 计算应发薪资 |
| | | BigDecimal baseSalary = employeeSalary.getBaseSalary() != null ? employeeSalary.getBaseSalary() : BigDecimal.ZERO; |
| | | // 基本工资统一取「员工信息」中维护的基本薪资(薪酬档案里的 baseSalary 不再作为核算来源, |
| | | // 避免员工信息与薪酬档案不一致时,核算结果与员工档案对不上) |
| | | BigDecimal baseSalary = employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO; |
| | | BigDecimal performanceSalary = employeeSalary.getPerformanceSalary() != null ? employeeSalary.getPerformanceSalary() : BigDecimal.ZERO; |
| | | // 岗位工资 / 工龄工资:与基本工资同源,取员工信息中维护的金额 |
| | | BigDecimal postSalary = employee.getPostSalary() != null ? employee.getPostSalary() : BigDecimal.ZERO; |
| | | BigDecimal senioritySalary = employee.getSenioritySalary() != null ? employee.getSenioritySalary() : 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; |
| | | OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary); |
| | | BigDecimal overtimePay = overtimeSalary.pay(); |
| | | |
| | | // 按排班累计出勤天数与排班总工时(无排班时出勤天数回退默认 22) |
| | | // 按排班累计应出天数与排班总工时(无排班时应出天数回退默认 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); |
| | | // 实出天数:取自考勤汇总(无汇总记录时按 0 处理) |
| | | Integer actualDays = 0; |
| | | HrmAttendanceSummaryDO attendanceSummary = |
| | | attendanceSummaryMapper.selectByUserIdAndYearMonth(employeeId, period); |
| | | if (attendanceSummary != null && attendanceSummary.getActualDays() != null) { |
| | | actualDays = attendanceSummary.getActualDays(); |
| | | } |
| | | |
| | | // 全勤奖:与其他津贴一致,由人工在核算单中维护,新建时默认 0 |
| | | BigDecimal fullAttendanceBonus = BigDecimal.ZERO; |
| | | |
| | | // 应发工资总额(收入项口径必须与 calcActualSalary 保持一致) |
| | | BigDecimal totalIncome = baseSalary.add(performanceSalary).add(postSalary).add(senioritySalary) |
| | | .add(fullAttendanceBonus) |
| | | .add(mealAllowance).add(transportAllowance).add(otherAllowance).add(overtimePay); |
| | | |
| | | // 获取社保基数:员工社保档案 > 员工基本工资 |
| | | BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null) |
| | |
| | | .deptId(employee.getDeptId()) |
| | | .baseSalary(baseSalary) |
| | | .performanceSalary(performanceSalary) |
| | | .postSalary(postSalary) |
| | | .senioritySalary(senioritySalary) |
| | | .overtimePay(overtimePay) |
| | | .mealAllowance(mealAllowance) |
| | | .transportAllowance(transportAllowance) |
| | | .otherAllowance(otherAllowance) |
| | | .fullAttendanceBonus(fullAttendanceBonus) |
| | | .socialSecurityDeduction(socialSecurityDeduction) |
| | | .housingFundDeduction(housingFundDeduction) |
| | | .taxDeduction(taxDeduction) |
| | | .otherDeduction(otherDeduction) |
| | | .leaveDeduction(leaveDeduction) |
| | | .actualSalary(actualSalary) |
| | | .workDays(workDays) // 按排班累计应出勤天数,无排班时回退 22 |
| | | .workDays(workDays) // 应出天数:按排班累计,无排班时回退 22 |
| | | .actualDays(actualDays) // 实出天数:取自考勤汇总 |
| | | .overtimeHours(overtimeSalary.hours()) |
| | | .totalWorkHours(totalWorkHours) |
| | | .totalWorkHours(totalWorkHours) // 保留历史字段,业务已改用 actualDays |
| | | .status(0) // 待确认 |
| | | .build(); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateSalaryCalculation(HrmSalaryCalculationDO calculation) { |
| | | HrmSalaryCalculationDO exist = salaryCalculationMapper.selectById(calculation.getId()); |
| | | if (exist == null) { |
| | | throw exception(SALARY_CALCULATION_NOT_EXISTS); |
| | | } |
| | | // 仅待确认状态可修改:已确认需先撤销;已发放不可改 |
| | | if (exist.getStatus() == null || !Objects.equals(exist.getStatus(), 0)) { |
| | | throw exception(SALARY_CALCULATION_UPDATE_FAIL); |
| | | } |
| | | // 以下字段不允许通过修改接口变更,一律以库中为准 |
| | | calculation.setNo(exist.getNo()); |
| | | calculation.setPeriod(exist.getPeriod()); |
| | | calculation.setUserId(exist.getUserId()); |
| | | calculation.setDeptId(exist.getDeptId()); |
| | | calculation.setStatus(exist.getStatus()); |
| | | calculation.setConfirmTime(exist.getConfirmTime()); |
| | | calculation.setConfirmUserId(exist.getConfirmUserId()); |
| | | calculation.setPaymentTime(exist.getPaymentTime()); |
| | | // 金额字段为 null 时统一按 0 处理:MyBatis-Plus 的 updateById 默认忽略 null, |
| | | // 否则会出现「实发工资按 0 计算、但明细仍保留旧值」的不一致 |
| | | calculation.setBaseSalary(nvl(calculation.getBaseSalary())); |
| | | calculation.setPerformanceSalary(nvl(calculation.getPerformanceSalary())); |
| | | calculation.setPostSalary(nvl(calculation.getPostSalary())); |
| | | calculation.setSenioritySalary(nvl(calculation.getSenioritySalary())); |
| | | calculation.setOvertimePay(nvl(calculation.getOvertimePay())); |
| | | calculation.setMealAllowance(nvl(calculation.getMealAllowance())); |
| | | calculation.setTransportAllowance(nvl(calculation.getTransportAllowance())); |
| | | calculation.setOtherAllowance(nvl(calculation.getOtherAllowance())); |
| | | calculation.setFullAttendanceBonus(nvl(calculation.getFullAttendanceBonus())); |
| | | calculation.setSocialSecurityDeduction(nvl(calculation.getSocialSecurityDeduction())); |
| | | calculation.setHousingFundDeduction(nvl(calculation.getHousingFundDeduction())); |
| | | calculation.setTaxDeduction(nvl(calculation.getTaxDeduction())); |
| | | calculation.setOtherDeduction(nvl(calculation.getOtherDeduction())); |
| | | calculation.setLeaveDeduction(nvl(calculation.getLeaveDeduction())); |
| | | // 实发工资由后端重算,避免前端传入值与明细不一致 |
| | | calculation.setActualSalary(calcActualSalary(calculation)); |
| | | salaryCalculationMapper.updateById(calculation); |
| | | } |
| | | |
| | | /** |
| | | * 计算实发工资 = 收入项合计 - 扣除项合计,保留两位小数 |
| | | * |
| | | * 收入项必须与 buildSalaryCalculation 中 totalIncome 的口径完全一致, |
| | | * 否则会出现「新建核算算得对、修改一次核算单实发就变少」的不一致。 |
| | | */ |
| | | private static BigDecimal calcActualSalary(HrmSalaryCalculationDO c) { |
| | | BigDecimal income = nvl(c.getBaseSalary()) |
| | | .add(nvl(c.getPostSalary())) |
| | | .add(nvl(c.getSenioritySalary())) |
| | | .add(nvl(c.getPerformanceSalary())) |
| | | .add(nvl(c.getFullAttendanceBonus())) |
| | | .add(nvl(c.getOvertimePay())) |
| | | .add(nvl(c.getMealAllowance())) |
| | | .add(nvl(c.getTransportAllowance())) |
| | | .add(nvl(c.getOtherAllowance())); |
| | | BigDecimal deduction = nvl(c.getSocialSecurityDeduction()) |
| | | .add(nvl(c.getHousingFundDeduction())) |
| | | .add(nvl(c.getTaxDeduction())) |
| | | .add(nvl(c.getOtherDeduction())) |
| | | .add(nvl(c.getLeaveDeduction())); |
| | | return income.subtract(deduction).setScale(2, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | private static BigDecimal nvl(BigDecimal v) { |
| | | return v == null ? BigDecimal.ZERO : v; |
| | | } |
| | | |
| | | /** |
| | | * 内部生成发放台账方法(供自动生成调用) |
| | | */ |