fix: blank_employees 模式不再自动带出基本工资

本月空白模式应所有金额默认 0,去掉从 emp.monthlySalary 自动填充
基本工资的逻辑,保持与"空白"语义一致。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-16 14:57:10 +08:00
parent 63a0a6934b
commit a5911d1874
6 changed files with 396 additions and 12 deletions
+191 -7
View File
@@ -59,6 +59,7 @@ calcBatchEntry(
options?: { // 可选覆盖
skipSocial?, // 跳过社保计算
overrideSocial?, // 手动覆盖社保值
prevDeferred?, // 上月递延扣款(次月补扣){ socialEmp, housingEmp, minWage }
}
)
```
@@ -131,6 +132,12 @@ socialEmp = Math.max(0, fullSocialEmp - deductedSocialEmp)
socialOrg = Math.max(0, fullSocialOrg - deductedSocialOrg)
housingEmp = Math.max(0, fullHousingEmp - deductedHousingEmp)
housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg)
// 叠加上月递延的社保/公积金(入职当月未扣完的部分,本月补扣)
if (options?.prevDeferred) {
socialEmp += options.prevDeferred.socialEmp || 0
housingEmp += options.prevDeferred.housingEmp || 0
}
```
### 关键设计:差额补扣机制
@@ -271,11 +278,18 @@ tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted)
---
## 六、实发工资
## 六、实发工资与最低工资保护
```typescript
netPay = totalPay - socialEmp - housingEmp - tax
// 应发 - 个人社保 - 个人公积金 - 个税
// 初始实发 = 应发 - 个人社保 - 个人公积金 - 个税 - 上月递延最低工资补扣
netPay = totalPay - socialEmp - housingEmp - tax - prevDeferredMinWage
// 最低工资保护(仅 REGULAR / TERMINATION 批次)
if (minWage > 0 && netPay < minWage) {
// 优先递延社保 → 递延公积金 → 递延最低工资补齐
// 详见第十五章「最低工资保护与递延扣款机制」
netPay = minWage
}
```
---
@@ -417,6 +431,7 @@ POST /batches/:batchId/unarchive
│SocialAccount│ │SpecialDeduction │ │ OvertimeRecord │
│+YearStandard│ │Record (按月) │ │ (加班费) │
│(社保比例) │ └──────────────────┘ └─────────────────┘
│+minWage │ │ │
└──────┬─────┘ │ │
│ │ │
▼ ▼ ▼
@@ -424,15 +439,23 @@ POST /batches/:batchId/unarchive
│ calcBatchEntry() │
│ │
│ 社保 = 应缴全额 - 已归档批次已扣(差额补扣) │
│ + 上月递延社保(次月补扣) │
│ 应发 = 基本工资 + 各项津贴 + 奖金 - 扣款 │
│ 个税 = 累计预扣法(从已归档批次取累计数据) │
│ 实发 = 应发 - 个人社保 - 个人公积金 - 个税 │
│ - 上月递延最低工资补扣 │
│ 最低工资保护:实发 < minWage 时 → 递延扣款补齐 │
│ 递延社保 → 递延公积金 → 递延最低工资补齐 │
└──────────────────────┬───────────────────────────────┘
┌────────────────┐
│ BatchEntry │
│ (批次条目) │
│ + minWage │
│ + minWageApplied│
│ + deferred* │
│ + prevDeferred*│
└───────┬────────┘
│ 归档
@@ -444,6 +467,11 @@ POST /batches/:batchId/unarchive
│ 发布
员工端可见
次月创建批次时:
getPrevDeferred() 读取上月 BatchEntry 的 deferred* 字段
→ 传入 calcBatchEntry 的 options.prevDeferred
→ 本月社保/公积金叠加补扣上月递延金额
```
---
@@ -454,6 +482,8 @@ POST /batches/:batchId/unarchive
|--------|--------|---------|------|
| 社保比例 | `SocialYearStandard` | accountId + 月份在生效区间 | 计算社保公积金 |
| 社保比例(回退) | `SocialInsuranceConfig` | orgId + city + 月份在生效区间 | 兼容旧数据 |
| 最低工资标准 | `SocialYearStandard.minWage` | accountId + 月份在生效区间 | 最低工资保护 |
| 最低工资标准(回退) | `SocialInsuranceConfig.minWage` | orgId + city + 月份在生效区间 | 兼容旧数据 |
| 社保基数 | `Employee.socialInsBase` | — | 优先于基本工资 |
| 公积金基数 | `Employee.housingFundBase` | — | 优先于基本工资 |
| 已扣社保 | `BatchEntry` | 当月已归档批次 | 差额补扣 |
@@ -467,6 +497,9 @@ POST /batches/:batchId/unarchive
| 基本工资 | `Payslip`(上月) | employeeId + 上月 | copy_last 模式 |
| 基本工资(回退) | `Employee.monthlySalary` | 解密 | 无上月工资条时 |
| 试用期工资 | `LaborContract.probationSalary` | 最新合同 | 试用期内的 baseSalary |
| 上月递延社保 | `BatchEntry.deferredSocialEmp` | 上月已归档批次 | 次月补扣 |
| 上月递延公积金 | `BatchEntry.deferredHousingEmp` | 上月已归档批次 | 次月补扣 |
| 上月递延最低工资补齐 | `BatchEntry.deferredMinWage` | 上月已归档批次 | 次月补扣 |
---
@@ -494,11 +527,16 @@ POST /batches/:batchId/unarchive
质量门禁(草稿状态自动检查):
- 实发为负的员工 → 红色警告
- 实发低于最低工资 → 红色警告
- 实发低于最低工资(未触发保护)→ 红色警告
- 最低工资保护已触发(递延扣款)→ 黄色警告
- 本月补扣上月递延 → 黄色警告
- 全零记录 → 黄色警告
表格可视化提示:
- **实发列**:最低工资保护触发时显示橙色 + ★标记,悬停显示补齐明细
- **递延扣款列**(新增):橙色"递延 ¥X"或蓝色"补扣 ¥X",悬停显示分项明细
- **风险列**:最低工资保护触发时显示橙色警告图标
### 步骤 2:归档锁定(ARCHIVED
**归档时后端自动执行**
@@ -514,7 +552,153 @@ POST /batches/:batchId/unarchive
---
## 十五、状态流转
## 十五、最低工资保护与递延扣款机制
### 背景
入职当月工资按天折算后,可能不足以扣除社保/公积金个人部分,导致实发为负。
《劳动法》第四十八条规定用人单位支付劳动者的工资不得低于当地最低工资标准。
### 最低工资标准配置
| 配置位置 | 表 | 字段 | 说明 |
|---------|---|------|------|
| 新账户体系 | `SocialYearStandard` | `minWage` | 按账户+年度设置,优先使用 |
| 旧配置表 | `SocialInsuranceConfig` | `minWage` | 兼容回退,按城市匹配 |
前端配置入口:社保配置页面 → 新建版本 → "最低工资标准"输入框(仅社保 Tab)
设为 0 表示不检查最低工资。
### 保护逻辑(calcBatchEntry 中执行)
```
初始实发 = 应发 - 社保个人 - 公积金个人 - 个税 - 上月递延最低工资补扣
if (minWage > 0 且 实发 < minWage) {
差额 = minWage - 实发
① 优先递延社保个人部分
if (差额 <= 社保个人) {
递延社保 = 差额
社保个人 -= 差额
实发 = minWage
}
② 社保不够,递延全部社保 + 部分公积金
else if (差额 <= 社保个人 + 公积金个人) {
递延社保 = 社保个人
递延公积金 = 差额 - 社保个人
社保个人 = 0
公积金个人 -= 递延公积金
实发 = minWage
}
③ 社保+公积金都不够,差额作为最低工资补齐递延
else {
递延社保 = 社保个人
递延公积金 = 公积金个人
递延最低工资补齐 = 差额 - 社保个人 - 公积金个人
社保个人 = 0
公积金个人 = 0
实发 = minWage
}
}
```
### minWageApplied 的含义
`minWageApplied` = 为让实发达到最低工资而减少的扣款总额,包含三部分:
| 组成 | 字段 | 含义 |
|------|------|------|
| 免扣社保 | `deferredSocialEmp` | 本该扣但没扣的社保个人部分 |
| 免扣公积金 | `deferredHousingEmp` | 本该扣但没扣的公积金个人部分 |
| 额外补齐 | `deferredMinWage` | 社保+公积金全免后仍不足的差额 |
| **合计** | `minWageApplied` | = 免扣社保 + 免扣公积金 + 额外补齐 |
**注意**`minWageApplied` 不是"额外补了这么多现金",而是"减少了这么多扣款"。
### 递延扣款的次月补扣
```
本月(入职当月):
BatchEntry 记录 deferredSocialEmp / deferredHousingEmp / deferredMinWage
实发 = minWage(保护后)
次月(创建批次时):
getPrevDeferred() 读取上月已归档批次的递延金额
calcBatchEntry 的 options.prevDeferred 传入
本月社保个人 = 当月应缴社保 + 上月递延社保
本月实发 = 应发 - (当月社保 + 上月递延社保) - (当月公积金 + 上月递延公积金) - 个税 - 上月递延最低工资补齐
BatchEntry 记录 prevDeferredSocialEmp / prevDeferredHousingEmp / prevDeferredMinWage
```
### 示例
**张三 8 月 20 日入职,月薪 12000,扣款 20000(测试数据)**
```
应发 = 12000 + 0 + 0 + (-500) - 20000 = -8500
社保个人(原)= 1575
公积金个人(原)= 1800
个税 = 0
初始实发 = -8500 - 1575 - 1800 - 0 = -11875
最低工资 = 2420
保护触发:
差额 = 2420 - (-11875) = 14295
递延社保 = 1575(全部免扣)
递延公积金 = 1800(全部免扣)
递延最低工资补齐 = 14295 - 1575 - 1800 = 10920
实发 = 2420
BatchEntry 记录:
minWage = 2420
minWageApplied = 14295
deferredSocialEmp = 1575
deferredHousingEmp = 1800
deferredMinWage = 10920
socialEmp = 0(当月不扣)
housingEmp = 0(当月不扣)
netPay = 2420
次月(9 月):
getPrevDeferred() 读取 → { socialEmp: 1575, housingEmp: 1800, minWage: 10920 }
9 月社保个人 = 当月应缴社保 + 1575
9 月实发 = 应发 - 9月社保(含补扣) - 9月公积金(含补扣) - 个税 - 10920
```
### 前端展示
| 展示位置 | 样式 | 内容 |
|---------|------|------|
| 实发列 | 橙色 + ★ | 触发最低工资保护时,悬停显示补齐明细 |
| 递延扣款列 | 橙色"递延 ¥X" | 当月递延,悬停显示分项(免扣社保/公积金/额外补齐) |
| 递延扣款列 | 蓝色"补扣 ¥X" | 次月补扣上月递延,悬停显示分项 |
| 风险列 | 橙色警告图标 | 最低工资保护触发 |
| 质量门禁 | 黄色提示 | "N 名员工实发已补齐到最低工资标准..." |
### prePayrollCheck 检查项(第 11/12 项)
```
// 11. 实发低于最低工资标准(剔除加班费后比较)
if (minWage > 0 且 可比收入 < minWage 且 实发 < minWage) {
if (minWageApplied > 0) {
→ WARNING: 最低工资保护已触发(递延扣款)
} else {
→ FAIL: 实发低于最低工资标准
}
}
// 12. 上月递延扣款待补扣
if (prevDeferred > 0) {
→ WARNING: 上月递延扣款已补扣
}
```
---
## 十六、状态流转
```
DRAFT(草稿,可编辑)
@@ -528,7 +712,7 @@ Payslip.publishStatus: PUBLISHED(员工端可见)
---
## 十、批次类型详解
## 十、批次类型详解
| 类型 | 说明 | 社保公积金 | 个税计算 | 员工来源 |
|------|------|-----------|---------|---------|
@@ -539,7 +723,7 @@ Payslip.publishStatus: PUBLISHED(员工端可见)
---
## 十、创建批次的 5 种数据初始化模式
## 十、创建批次的 5 种数据初始化模式
| 模式 | 说明 | 员工来源 | 数据来源 |
|------|------|---------|---------|