tuqin
2 天以前 1d372d5e4916eaecf9b773e52e0d887d4fd0e47a
fix(hrm): 社保扣款在员工未加社保时误扣、两方案扣款相同

客户反馈两点,实测数据确认都是代码问题。

一、没添加社保的员工也被扣了社保(102 人,多扣 21363.30 元)
根因:buildSalaryCalculation 里社保扣除无条件计算,
社会基数 = 社保档案基数 ?: 基本工资,
比例 = 方案比例 ?: 默认(养老0.08 + 医疗0.02 + 失业0.005 = 10.5%)。
于是没有社保档案的员工按「基本工资 1950 × 10.5% = 204.75」扣款,
132 条核算单里有 100 条就是这个金额。
修复:只有员工配置了社保档案才扣社保,没有档案扣款为 0
(与公积金口径保持一致)。

二、两个不同方案扣款金额完全一样(兵团五险 与 地方五险 都是 550.83)
根因:代码只累加了「养老 + 医疗 + 失业」三项个人比例,
漏掉了方案里配置的「大病医疗个人」与「长期护理个人」比例,
而这两个险种的个人比例正是两个方案唯一的差别
(兵团:大病 0.25% + 长护 0.15%;地方:大病 0.50% + 长护 0.25%)。
修复:个人应缴比例 = 养老 + 医疗 + 失业 + 大病医疗 + 长期护理。

修复前后(2026-10,132 条,均为待确认状态,已重新核算):
修复前:204.75 x 100 人、550.83 x 30 人、682.50 x 1、205.80 x 1
社保合计 37888.20
修复后:0.00 x 102 人(无社保档案,不再扣)
方案2(兵团五险)571.81 x 23 人 = 5246 x 10.90%
方案3(地方五险)590.18 x 9 人 = 5246 x 11.25%
社保合计 17319.63

顺带核对过的其他字段来源(未发现新问题):
- 基本工资/岗位工资/工龄工资 <- 员工档案
- 绩效/餐补/交通/其他补贴 <- 薪酬档案(hrm_employee_salary 当前 0 条,所以核算里全为 0,
需要客户先录薪酬档案才会有这些项)
- 加班费 <- 审批通过的加班申请(无申请为 0,管理类别不计)
- 公积金 <- 方案个人比例>0 且档案公积金基数>0 才扣(已在上轮修复)
- 个税 <- 个税税率配置,应纳税所得额已扣除社保与公积金
- 缺勤扣款 <- 考勤汇总 + 请假单
已修改1个文件
26 ■■■■■ 文件已修改
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -281,7 +281,7 @@
                .add(fullAttendanceBonus)
                .add(mealAllowance).add(transportAllowance).add(otherAllowance).add(overtimePay);
        // 获取社保基数:员工社保档案 > 员工基本工资
        // 社保基数:只取员工社保档案里配置的缴纳基数;未配置时用基本工资
        BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
                ? socialSecurity.getSocialSecurityBase() : baseSalary;
@@ -292,17 +292,31 @@
                ? scheme.getMedicalPersonalRatio() : DEFAULT_MEDICAL_PERSONAL_RATIO;
        BigDecimal unemploymentPersonalRatio = (scheme != null && scheme.getUnemploymentPersonalRatio() != null)
                ? scheme.getUnemploymentPersonalRatio() : DEFAULT_UNEMPLOYMENT_PERSONAL_RATIO;
        // 大病医疗 / 长期护理的个人比例:方案里可能为空,为空视为 0(不扣)
        BigDecimal seriousIllnessPersonalRatio = (scheme != null && scheme.getSeriousIllnessPersonalRatio() != null)
                ? scheme.getSeriousIllnessPersonalRatio() : BigDecimal.ZERO;
        BigDecimal longTermCarePersonalRatio = (scheme != null && scheme.getLongTermCarePersonalRatio() != null)
                ? scheme.getLongTermCarePersonalRatio() : BigDecimal.ZERO;
        // 公积金个人比例:只取方案配置值,不再回退默认比例。
        // 否则「公司没配公积金方案」时会被按默认 12% 扣款(这就是客户反馈的问题)。
        BigDecimal housingFundPersonalRatio = scheme != null ? scheme.getHousingFundPersonalRatio() : null;
        // 社保扣除 = 基数 × (养老个人比例 + 医疗个人比例 + 失业个人比例)
        BigDecimal socialSecurityDeduction = socialSecurityBase
                .multiply(pensionPersonalRatio)
                .add(socialSecurityBase.multiply(medicalPersonalRatio))
                .add(socialSecurityBase.multiply(unemploymentPersonalRatio))
        // 社保扣除:必须「员工配置了社保档案」才扣款。
        // 之前无论有没有社保档案都按 基数 × 默认比例(0.08+0.02+0.005=10.5%) 扣款,
        // 导致 102 个没加社保的员工被扣了 204.75(= 基本工资 1950 × 10.5%)。
        // 个人应缴 = 养老 + 医疗 + 失业 + 大病医疗 + 长期护理。
        // 注意:原来漏算了后两项,使得「兵团五险」与「地方五险」两个方案算出来的扣款完全一样。
        BigDecimal socialSecurityDeduction = BigDecimal.ZERO;
        if (socialSecurity != null) {
            BigDecimal personalTotalRatio = pensionPersonalRatio
                    .add(medicalPersonalRatio)
                    .add(unemploymentPersonalRatio)
                    .add(seriousIllnessPersonalRatio)
                    .add(longTermCarePersonalRatio);
            socialSecurityDeduction = socialSecurityBase.multiply(personalTotalRatio)
                .setScale(2, RoundingMode.HALF_UP);
        }
        // 公积金扣除:必须同时满足「方案配置了公积金比例 > 0」且「员工档案配置了公积金基数 > 0」才扣款,
        // 任一不满足都视为公司未缴纳公积金,扣款为 0。