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/controller/admin/salary/vo/HrmTaxRateConfigSaveReqVO.java          |    6 -
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java         |   36 ++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmTaxRateConfigServiceImpl.java                    |   25 ++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmResignationApplicationController.java |    4 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java                |  167 +++++++++++++++++++++++++++--------------
 5 files changed, 173 insertions(+), 65 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmResignationApplicationController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmResignationApplicationController.java
index 7bd6ef8..a4e33a1 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmResignationApplicationController.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmResignationApplicationController.java
@@ -83,10 +83,10 @@
     @PostMapping("/submit")
     @Operation(summary = "鎻愪氦绂昏亴鐢宠瀹℃壒")
     @Parameter(name = "id", description = "鐢宠ID", required = true)
-    @Parameter(name = "processDefinitionKey", description = "娴佺▼瀹氫箟Key", required = true)
+    @Parameter(name = "processDefinitionKey", description = "娴佺▼瀹氫箟Key锛堜笉浼犳椂鑷姩浣跨敤銆岀鑱屽鎵广�嶅垎绫讳笅鏈�鏂扮殑娲昏穬娴佺▼锛�")
     @PreAuthorize("@ss.hasPermission('hrm:resignation:submit')")
     public CommonResult<Boolean> submitResignationApplication(@RequestParam("id") Long id,
-                                                              @RequestParam("processDefinitionKey") String processDefinitionKey) {
+                                                              @RequestParam(value = "processDefinitionKey", required = false) String processDefinitionKey) {
         Long userId = SecurityFrameworkUtils.getLoginUserId();
         resignationApplicationService.submitResignationApplication(id, processDefinitionKey, userId);
         return success(true);
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmTaxRateConfigSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmTaxRateConfigSaveReqVO.java
index 75759b0..fe6172b 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmTaxRateConfigSaveReqVO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmTaxRateConfigSaveReqVO.java
@@ -15,12 +15,10 @@
     @Schema(description = "閰嶇疆ID", example = "1")
     private Long id;
 
-    @Schema(description = "閰嶇疆鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024骞翠釜绋庣◣鐜囪〃")
-    @NotBlank(message = "閰嶇疆鍚嶇О涓嶈兘涓虹┖")
+    @Schema(description = "閰嶇疆鍚嶇О锛屼笉浼犳椂鑷姩鐢熸垚锛堝 2026骞�11鏈堜釜绋庣◣鐜囪〃锛�", example = "2024骞翠釜绋庣◣鐜囪〃")
     private String name;
 
-    @Schema(description = "鐢熸晥鏃ユ湡", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "鐢熸晥鏃ユ湡涓嶈兘涓虹┖")
+    @Schema(description = "鐢熸晥鏃ユ湡锛屼笉浼犳椂榛樿褰撴湀1鍙�")
     private LocalDate effectiveDate;
 
     @Schema(description = "绋庣巼绾ф暟锛�1-7", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
index 54dfc6d..776d807 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -2,6 +2,7 @@
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+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.bpm.api.task.BpmProcessInstanceApi;
@@ -33,6 +34,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -93,12 +95,17 @@
         String no = generateUniqueNo();
         AdminUserRespDTO user = adminUserApi.getUser(userId);
         if (user == null) {
+            // 浼犲叆鐨勫彲鑳戒笉鏄郴缁熺敤鎴稩D锛堜緥濡傚憳宸D锛夛紝鍥為��鍒板綋鍓嶇櫥褰曠敤鎴峰苟閲嶆柊鏌ヨ锛岄伩鍏嶅悗缁� NPE
             userId = getCurrentUserId();
+            user = adminUserApi.getUser(userId);
         }
         HrmResignationApplicationDO application = BeanUtils.toBean(createReqVO, HrmResignationApplicationDO.class);
         application.setNo(no);
         application.setUserId(userId);
-        application.setDeptId(createReqVO.getDeptId() != null ? createReqVO.getDeptId() : user.getDeptId());
+        // 閮ㄩ棬锛氫紭鍏堝彇璇锋眰鍙傛暟锛屽叾娆″彇璐﹀彿鎵�灞為儴闂紝鏈�鍚庡厹搴� 0锛堣鍒� NOT NULL锛�
+        Long deptId = createReqVO.getDeptId() != null ? createReqVO.getDeptId()
+                : (user != null ? user.getDeptId() : null);
+        application.setDeptId(deptId != null ? deptId : 0L);
         application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
         application.setHandoverStatus(HANDOVER_STATUS_NOT_STARTED);
         resignationApplicationMapper.insert(application);
@@ -245,12 +252,14 @@
             throw exception(RESIGNATION_APPLICATION_SUBMIT_FAIL_NOT_DRAFT);
         }
         validateApproveCategoryAndProcessDefinition();
-        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
+        // 鏈寚瀹氭祦绋嬪畾涔� Key 鏃讹紝鑷姩鍙栥�岀鑱屽鎵广�嶅垎绫讳笅鏈�鏂扮殑娲昏穬娴佺▼锛岄伩鍏嶅墠绔湭浼� key 鐩存帴鎶ョ郴缁熷紓甯�
+        String key = resolveProcessDefinitionKey(processDefinitionKey);
+        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(key);
         if (processDefinition == null) {
             throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
         }
         String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
-                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
+                .setProcessDefinitionKey(key).setBusinessKey(String.valueOf(id)));
         resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder()
                 .id(id)
                 .processInstanceId(processInstanceId)
@@ -258,6 +267,27 @@
                 .build());
     }
 
+    /**
+     * 瑙f瀽娴佺▼瀹氫箟 Key锛氫紭鍏堜娇鐢ㄨ皟鐢ㄦ柟浼犲叆鍊硷紝涓虹┖鏃跺彇銆岀鑱屽鎵广�嶅垎绫讳笅鏈�鏂扮殑娲昏穬娴佺▼
+     */
+    private String resolveProcessDefinitionKey(String processDefinitionKey) {
+        if (StrUtil.isNotBlank(processDefinitionKey)) {
+            return processDefinitionKey.trim();
+        }
+        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
+                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
+        if (CollUtil.isEmpty(definitionInfoList)) {
+            throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
+        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
+        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
+        return processDefinitions.stream()
+                .filter(pd -> !pd.isSuspended())
+                .max(Comparator.comparingInt(ProcessDefinition::getVersion))
+                .map(ProcessDefinition::getKey)
+                .orElseThrow(() -> exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS));
+    }
+
     private void validateApproveCategoryAndProcessDefinition() {
         List<String> codes = Collections.singletonList(APPROVE_CATEGORY_CODE);
         Map<String, ?> categoryMap = bpmCategoryService.getCategoryMap(codes);
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) {
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmTaxRateConfigServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmTaxRateConfigServiceImpl.java
index f8635f1..b825cdb 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmTaxRateConfigServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmTaxRateConfigServiceImpl.java
@@ -7,6 +7,7 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.List;
 
@@ -29,6 +30,7 @@
     @Transactional(rollbackFor = Exception.class)
     public void createTaxRateConfigList(List<HrmTaxRateConfigDO> configList) {
         for (HrmTaxRateConfigDO config : configList) {
+            applyDefaults(config);
             taxRateConfigMapper.insert(config);
         }
     }
@@ -36,9 +38,32 @@
     @Override
     public void updateTaxRateConfig(HrmTaxRateConfigDO config) {
         validateTaxRateConfigExists(config.getId());
+        applyDefaults(config);
         taxRateConfigMapper.updateById(config);
     }
 
+    /**
+     * 琛ュ叏榛樿鍊硷細鐢熸晥鏃ユ湡榛樿涓哄綋鏈� 1 鍙凤紝閰嶇疆鍚嶇О鑷姩鐢熸垚锛岄�熺畻鎵i櫎鏁�/涓婇檺缂虹渷鎸� 0
+     */
+    private void applyDefaults(HrmTaxRateConfigDO config) {
+        if (config.getEffectiveDate() == null) {
+            config.setEffectiveDate(LocalDate.now().withDayOfMonth(1));
+        }
+        if (config.getName() == null || config.getName().isBlank()) {
+            config.setName(config.getEffectiveDate().getYear() + "骞�"
+                    + config.getEffectiveDate().getMonthValue() + "鏈堜釜绋庣◣鐜囪〃");
+        }
+        if (config.getStatus() == null) {
+            config.setStatus(0);
+        }
+        if (config.getQuickDeduction() == null) {
+            config.setQuickDeduction(BigDecimal.ZERO);
+        }
+        if (config.getMaxAmount() == null) {
+            config.setMaxAmount(BigDecimal.ZERO);
+        }
+    }
+
     @Override
     public void deleteTaxRateConfig(Long id) {
         validateTaxRateConfigExists(id);

--
Gitblit v1.9.3