From 1fd52d7784a8b0b963cf38d63175cdf9082e8f43 Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期二, 03 十一月 2026 19:40:39 +0800
Subject: [PATCH] fix(hrm): 薪酬核算按出勤天数联动扣款、个税配置可保存、离职提交报错
---
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java | 167 +++++++++++++++++++++++++++++++++++++------------------
1 files changed, 111 insertions(+), 56 deletions(-)
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
index 858e2b1..faa80ef 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -252,17 +252,25 @@
OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
BigDecimal overtimePay = overtimeSalary.pay();
- // 鎸夋帓鐝疮璁″簲鍑哄ぉ鏁颁笌鎺掔彮鎬诲伐鏃讹紙鏃犳帓鐝椂搴斿嚭澶╂暟鍥為��榛樿 22锛�
+ // 搴斿嚭澶╂暟锛氫紭鍏堝彇鑰冨嫟姹囨�婚噷鐨勩�屽簲鍑哄嫟澶╂暟銆嶏紙瀹㈡埛鎸夊疄闄呮湀浠界淮鎶わ紝濡� 30 澶╋級锛�
+ // 娌℃湁姹囨�绘椂閫�鍥炴帓鐝疮璁″ぉ鏁帮紝鍐嶉��鍥為粯璁� 22
ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period);
Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22;
BigDecimal totalWorkHours = scheduleStat.totalWorkHours();
- // 瀹炲嚭澶╂暟锛氬彇鑷�冨嫟姹囨�伙紙鏃犳眹鎬昏褰曟椂鎸� 0 澶勭悊锛�
- Integer actualDays = 0;
+ // 瀹炲嚭澶╂暟锛氬彇鑷�冨嫟姹囨�汇��
+ // 娉ㄦ剰锛歯ull 琛ㄧず璇ユ湀灏氭湭鐢熸垚鑰冨嫟姹囨�伙紝姝ゆ椂涓嶅仛銆岀己鍕ゆ墸娆俱�嶏紙瑙嗕负婊″嫟锛屽彧鎸夎鍋囧崟鎵o級锛�
+ // 鍚﹀垯涓�閲嶇畻灏变細鎶婃墍鏈夋病褰曡�冨嫟鐨勫憳宸ュ伐璧勬墸鍏夈��
+ Integer actualDays = null;
HrmAttendanceSummaryDO attendanceSummary =
attendanceSummaryMapper.selectByUserIdAndYearMonth(employeeId, period);
- if (attendanceSummary != null && attendanceSummary.getActualDays() != null) {
- actualDays = attendanceSummary.getActualDays();
+ if (attendanceSummary != null) {
+ if (attendanceSummary.getActualDays() != null) {
+ actualDays = attendanceSummary.getActualDays();
+ }
+ if (attendanceSummary.getWorkDays() != null && attendanceSummary.getWorkDays() > 0) {
+ workDays = attendanceSummary.getWorkDays();
+ }
}
// 鍏ㄥ嫟濂栵細涓庡叾浠栨触璐翠竴鑷达紝鐢变汉宸ュ湪鏍哥畻鍗曚腑缁存姢锛屾柊寤烘椂榛樿 0
@@ -307,7 +315,8 @@
}
// 璁$畻璇峰亣鎵f
- BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary);
+ // 璁$畻缂哄嫟鎵f锛堣鍋� + 鏃峰伐锛�
+ BigDecimal leaveDeduction = calculateAttendanceDeduction(employee, period, baseSalary, workDays, actualDays);
// 璁$畻涓◣鎵i櫎
// 搴旂撼绋庢墍寰楅 = 搴斿彂宸ヨ祫 - 绀句繚鎵i櫎 - 鍏Н閲戞墸闄� - 璧峰緛鐐�(5000)
@@ -323,12 +332,10 @@
BigDecimal otherDeduction = BigDecimal.ZERO;
- // 璁$畻瀹炲彂宸ヨ祫 = 搴斿彂 - 鎵i櫎
- BigDecimal totalDeduction = socialSecurityDeduction.add(housingFundDeduction)
- .add(taxDeduction).add(otherDeduction).add(leaveDeduction);
- BigDecimal actualSalary = totalIncome.subtract(totalDeduction);
+ // 璇存槑锛氬疄鍙戝伐璧勭粺涓�璧� calcActualSalary 璁$畻锛堟敹鍏� - 鎵i櫎锛屼笖涓嶅厑璁镐负璐燂級锛�
+ // 涓庛�屼慨鏀规牳绠楀崟 / 鎵归噺淇濆瓨銆嶄繚鎸佸悓涓�鍙e緞锛岄伩鍏嶅嚭鐜拌礋鏁板伐璧�
- return HrmSalaryCalculationDO.builder()
+ HrmSalaryCalculationDO calculation = HrmSalaryCalculationDO.builder()
.period(period)
.userId(employeeId) // 浣跨敤鍛樺伐ID
.deptId(employee.getDeptId())
@@ -346,65 +353,83 @@
.taxDeduction(taxDeduction)
.otherDeduction(otherDeduction)
.leaveDeduction(leaveDeduction)
- .actualSalary(actualSalary)
- .workDays(workDays) // 搴斿嚭澶╂暟锛氭寜鎺掔彮绱锛屾棤鎺掔彮鏃跺洖閫� 22
+ .workDays(workDays) // 搴斿嚭澶╂暟锛氫紭鍏堣�冨嫟姹囨�伙紝鍏舵鎺掔彮绱锛屾渶鍚庡洖閫� 22
.actualDays(actualDays) // 瀹炲嚭澶╂暟锛氬彇鑷�冨嫟姹囨��
.overtimeHours(overtimeSalary.hours())
.totalWorkHours(totalWorkHours) // 淇濈暀鍘嗗彶瀛楁锛屼笟鍔″凡鏀圭敤 actualDays
.status(0) // 寰呯‘璁�
.build();
+ calculation.setActualSalary(calcActualSalary(calculation));
+ return calculation;
}
/**
- * 璁$畻璇峰亣鎵f
+ * 璁$畻銆岀己鍕ゆ墸娆俱��= 璇峰亣鎵f + 鏃峰伐鎵f
+ *
+ * <p>瑙勫垯锛�
+ * <ol>
+ * <li>鏃ュ伐璧� = 鍩烘湰宸ヨ祫 / 搴斿嚭澶╂暟锛堜笌搴斿嚭澶╂暟鍙e緞涓�鑷达紝閬垮厤缂哄嫟澶╂暟瓒呰繃璁¤柂澶╂暟鏃舵墸瓒咃級锛�</li>
+ * <li>璇峰亣澶╂暟鎸夈�岃鍋囨墸娆鹃厤缃�嶇殑姣斾緥鎵o細璋冧紤绛夋湭閰嶇疆鐨勭被鍨嬫瘮渚嬭涓� 0锛堜笉鎵o級锛岀梾鍋囨寜閰嶇疆姣斾緥鎵o紱</li>
+ * <li>鑻ヨ鏈堝凡鐢熸垚鑰冨嫟姹囨�伙紙actualDays 涓嶄负 null锛夛細
+ * 鏃峰伐澶╂暟 = 搴斿嚭澶╂暟 鈭� 瀹炲嚭澶╂暟 鈭� 璇峰亣澶╂暟锛堜笉涓鸿礋锛夛紝鎸� 100% 鎵o紱</li>
+ * <li>鑻ユ湭鐢熸垚鑰冨嫟姹囨�伙紙actualDays 涓� null锛夛紝瑙嗕负婊″嫟锛屼笉鍋氭椃宸ユ墸娆撅紝鍙寜璇峰亣鍗曟墸锛�</li>
+ * <li>鎵f鎬婚灏侀《涓哄熀鏈伐璧勶紝閬垮厤鍑虹幇璐熷伐璧勩��</li>
+ * </ol>
*
* @param employee 鍛樺伐淇℃伅
- * @param period 鏍哥畻鍛ㄦ湡锛堝2026-02锛�
+ * @param period 鏍哥畻鍛ㄦ湡锛堝 2026-02锛�
* @param baseSalary 鍩烘湰宸ヨ祫
- * @return 璇峰亣鎵f閲戦
+ * @param workDays 搴斿嚭澶╂暟锛堟帓鐝疮璁★紝鏃犳帓鐝椂浼� 22锛�
+ * @param actualDays 瀹炲嚭澶╂暟锛堝彇鑷�冨嫟姹囨�伙紱null 琛ㄧず璇ユ湀鏈敓鎴愭眹鎬伙級
+ * @return 缂哄嫟鎵f閲戦
*/
- private BigDecimal calculateLeaveDeduction(HrmEmployeeDO employee, String period, BigDecimal baseSalary) {
- // 瑙f瀽鍛ㄦ湡鑾峰彇鏈堜唤寮�濮嬪拰缁撴潫鏃堕棿
- YearMonth yearMonth = YearMonth.parse(period);
- LocalDateTime monthStart = yearMonth.atDay(1).atStartOfDay();
- LocalDateTime monthEnd = yearMonth.atEndOfMonth().atTime(23, 59, 59);
+ private BigDecimal calculateAttendanceDeduction(HrmEmployeeDO employee, String period,
+ BigDecimal baseSalary, Integer workDays, Integer actualDays) {
+ int shouldDays = (workDays != null && workDays > 0) ? workDays : 22;
+ // 鏃ュ伐璧� = 鍩烘湰宸ヨ祫 / 搴斿嚭澶╂暟锛堜繚鐣� 4 浣嶏紝鏈�鍚庣粺涓�淇濈暀 2 浣嶏級
+ BigDecimal dailySalary = baseSalary.divide(new BigDecimal(shouldDays), 4, RoundingMode.HALF_UP);
- // 鏌ヨ鍛樺伐鍦ㄨ鏈堝凡瀹℃壒閫氳繃鐨勮鍋囪褰�
- // 娉ㄦ剰锛氳鍋囩敵璇风殑 userId 鏄郴缁熺敤鎴稩D锛岄渶瑕侀�氳繃鍛樺伐琛ㄨ幏鍙�
+ BigDecimal totalDeduction = BigDecimal.ZERO;
+ BigDecimal leaveDays = BigDecimal.ZERO;
+
+ // 1. 璇峰亣鎵f锛氭寜璇峰亣绫诲瀷鐨勯厤缃瘮渚�
Long systemUserId = employee.getUserId();
- if (systemUserId == null) {
- return BigDecimal.ZERO;
- }
- List<HrmLeaveApplicationDO> leaveApplications = leaveApplicationMapper
- .selectApprovedListByUserIdAndTimeRange(systemUserId, monthStart, monthEnd);
-
- if (leaveApplications.isEmpty()) {
- return BigDecimal.ZERO;
+ if (systemUserId != null) {
+ YearMonth yearMonth = YearMonth.parse(period);
+ LocalDateTime monthStart = yearMonth.atDay(1).atStartOfDay();
+ LocalDateTime monthEnd = yearMonth.atEndOfMonth().atTime(23, 59, 59);
+ List<HrmLeaveApplicationDO> leaveApplications = leaveApplicationMapper
+ .selectApprovedListByUserIdAndTimeRange(systemUserId, monthStart, monthEnd);
+ if (CollUtil.isNotEmpty(leaveApplications)) {
+ List<HrmLeaveTypeConfigDO> configList = leaveTypeConfigMapper.selectList();
+ Map<Integer, BigDecimal> configMap = configList.stream()
+ .filter(c -> c.getStatus() != null && c.getStatus() == 0) // 鍙彇鍚敤鐨勯厤缃�
+ .collect(Collectors.toMap(HrmLeaveTypeConfigDO::getLeaveType,
+ HrmLeaveTypeConfigDO::getDeductionRatio));
+ for (HrmLeaveApplicationDO application : leaveApplications) {
+ BigDecimal duration = application.getDuration() != null
+ ? application.getDuration() : BigDecimal.ZERO;
+ // 鏈厤缃殑绫诲瀷锛堝璋冧紤锛夋瘮渚嬫寜 0 澶勭悊 = 涓嶆墸娆�
+ BigDecimal deductionRatio = configMap.getOrDefault(application.getLeaveType(), BigDecimal.ZERO);
+ totalDeduction = totalDeduction.add(
+ dailySalary.multiply(duration).multiply(deductionRatio));
+ leaveDays = leaveDays.add(duration);
+ }
+ }
}
- // 鑾峰彇鎵�鏈夎鍋囩被鍨嬮厤缃�
- List<HrmLeaveTypeConfigDO> configList = leaveTypeConfigMapper.selectList();
- Map<Integer, BigDecimal> configMap = configList.stream()
- .filter(c -> c.getStatus() == 0) // 鍙彇鍚敤鐨勯厤缃�
- .collect(Collectors.toMap(HrmLeaveTypeConfigDO::getLeaveType, HrmLeaveTypeConfigDO::getDeductionRatio));
-
- // 璁$畻鏃ュ伐璧勶紙鍩烘湰宸ヨ祫 / 21.75锛屾寜鍥藉瑙勫畾鐨勫伐浣滄棩璁$畻锛�
- BigDecimal dailySalary = baseSalary.divide(new BigDecimal("21.75"), 2, RoundingMode.HALF_UP);
-
- // 绱璇峰亣鎵f
- BigDecimal totalLeaveDeduction = BigDecimal.ZERO;
- for (HrmLeaveApplicationDO application : leaveApplications) {
- Integer leaveType = application.getLeaveType();
- BigDecimal duration = application.getDuration() != null ? application.getDuration() : BigDecimal.ZERO;
- BigDecimal deductionRatio = configMap.getOrDefault(leaveType, BigDecimal.ZERO);
-
- // 璇峰亣鎵f = 鏃ュ伐璧� 脳 璇峰亣澶╂暟 脳 鎵f姣斾緥
- BigDecimal deduction = dailySalary.multiply(duration).multiply(deductionRatio)
- .setScale(2, RoundingMode.HALF_UP);
- totalLeaveDeduction = totalLeaveDeduction.add(deduction);
+ // 2. 鏃峰伐鎵f锛氱己鍕ゅぉ鏁颁腑鏈璇峰亣鍗曡鐩栫殑閮ㄥ垎锛屾寜 100% 鎵o紙浠呭綋鏈夎�冨嫟姹囨�绘椂锛�
+ if (actualDays != null) {
+ BigDecimal absentDays = new BigDecimal(shouldDays - actualDays).subtract(leaveDays);
+ if (absentDays.signum() > 0) {
+ totalDeduction = totalDeduction.add(dailySalary.multiply(absentDays));
+ }
}
- return totalLeaveDeduction;
+ // 3. 灏侀《锛氭墸娆句笉瓒呰繃鍩烘湰宸ヨ祫
+ BigDecimal result = totalDeduction.setScale(2, RoundingMode.HALF_UP);
+ BigDecimal cap = baseSalary.setScale(2, RoundingMode.HALF_UP);
+ return result.compareTo(cap) > 0 ? cap : result;
}
/**
@@ -677,6 +702,9 @@
@Transactional(rollbackFor = Exception.class)
public void saveSalaryCalculationList(List<HrmSalaryCalculationDO> calculationList) {
for (HrmSalaryCalculationDO calculation : calculationList) {
+ // 缂哄嫟鎵f锛堣鍋� + 鏃峰伐锛変笌瀹炲彂宸ヨ祫缁熶竴鐢卞悗绔噸绠楋紝
+ // 淇濊瘉鍒楄〃閲屾敼銆屽簲鍑哄ぉ鏁� / 瀹炲嚭澶╂暟銆嶅悗閲戦璺熺潃鍙�
+ recalcAttendanceDeduction(calculation);
if (calculation.getId() == null) {
// 鏂板璁板綍锛岀敓鎴愭牳绠楀崟鍙�
String no = noRedisDAO.generateSalaryCalculationNo();
@@ -728,10 +756,33 @@
calculation.setHousingFundDeduction(nvl(calculation.getHousingFundDeduction()));
calculation.setTaxDeduction(nvl(calculation.getTaxDeduction()));
calculation.setOtherDeduction(nvl(calculation.getOtherDeduction()));
- calculation.setLeaveDeduction(nvl(calculation.getLeaveDeduction()));
- // 瀹炲彂宸ヨ祫鐢卞悗绔噸绠楋紝閬垮厤鍓嶇浼犲叆鍊间笌鏄庣粏涓嶄竴鑷�
- calculation.setActualSalary(calcActualSalary(calculation));
+ // 缂哄嫟鎵f锛堣鍋� + 鏃峰伐锛変笌瀹炲彂宸ヨ祫缁熶竴鐢卞悗绔噸绠楋紝
+ // 淇濊瘉鍒楄〃閲屾敼銆屽簲鍑哄ぉ鏁� / 瀹炲嚭澶╂暟銆嶅悗閲戦璺熺潃鍙橈紝涓斾笉鍙楀墠绔紶鍏ョ殑 leaveDeduction 褰卞搷
+ recalcAttendanceDeduction(calculation);
salaryCalculationMapper.updateById(calculation);
+ }
+
+ /**
+ * 閲嶇畻銆岀己鍕ゆ墸娆俱�嶄笌銆屽疄鍙戝伐璧勩��
+ *
+ * <p>鏍哥畻鍗� userId 瀛樼殑鏄憳宸D锛坔rm_employee.id锛夈�傛寜銆屽簲鍑哄ぉ鏁� / 瀹炲嚭澶╂暟 + 璇峰亣鍗曘�嶉噸绠�
+ * 缂哄嫟鎵f锛堣鍋囨寜閰嶇疆姣斾緥鎵c�佹椃宸ユ寜 100% 鎵c�佸皝椤跺熀鏈伐璧勶級锛屽啀閲嶇畻瀹炲彂宸ヨ祫锛堜笉鍙负璐燂級銆�
+ * 鏌ヤ笉鍒板憳宸ユ垨鏍哥畻鍛ㄦ湡鏃讹紝淇濈暀鍘熺己鍕ゆ墸娆撅紝鍙噸绠楀疄鍙戙��
+ */
+ private void recalcAttendanceDeduction(HrmSalaryCalculationDO calculation) {
+ HrmEmployeeDO employee = calculation.getUserId() != null
+ ? employeeMapper.selectById(calculation.getUserId()) : null;
+ if (employee != null && calculation.getPeriod() != null) {
+ BigDecimal deduction = calculateAttendanceDeduction(
+ employee, calculation.getPeriod(),
+ nvl(calculation.getBaseSalary()),
+ calculation.getWorkDays(),
+ calculation.getActualDays());
+ calculation.setLeaveDeduction(deduction);
+ } else {
+ calculation.setLeaveDeduction(nvl(calculation.getLeaveDeduction()));
+ }
+ calculation.setActualSalary(calcActualSalary(calculation));
}
/**
@@ -739,6 +790,9 @@
*
* 鏀跺叆椤瑰繀椤讳笌 buildSalaryCalculation 涓� totalIncome 鐨勫彛寰勫畬鍏ㄤ竴鑷达紝
* 鍚﹀垯浼氬嚭鐜般�屾柊寤烘牳绠楃畻寰楀銆佷慨鏀逛竴娆℃牳绠楀崟瀹炲彂灏卞彉灏戙�嶇殑涓嶄竴鑷淬��
+ *
+ * 瀹炲彂宸ヨ祫涓嶅厑璁镐负璐燂細缂哄嫟鎵f绛夊凡灏侀《涓哄熀鏈伐璧勶紝浣嗙ぞ淇�/鍏Н閲�/涓◣鍙犲姞鍚庝粛鍙兘涓鸿礋锛�
+ * 涓氬姟涓婃寜銆屾湰鏈堟棤宸ヨ祫銆嶅鐞嗭紙缃� 0锛夛紝閬垮厤鍑虹幇鍛樺伐鍊掓瑺鍏徃鐨勬儏鍐点��
*/
private static BigDecimal calcActualSalary(HrmSalaryCalculationDO c) {
BigDecimal income = nvl(c.getBaseSalary())
@@ -755,7 +809,8 @@
.add(nvl(c.getTaxDeduction()))
.add(nvl(c.getOtherDeduction()))
.add(nvl(c.getLeaveDeduction()));
- return income.subtract(deduction).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal actual = income.subtract(deduction).setScale(2, RoundingMode.HALF_UP);
+ return actual.signum() < 0 ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : actual;
}
private static BigDecimal nvl(BigDecimal v) {
--
Gitblit v1.9.3