250 lines
9.4 KiB
Markdown
250 lines
9.4 KiB
Markdown
# 多批次工资发放算法
|
||
|
||
## 一、整体流程
|
||
|
||
```
|
||
创建批次 → 拉入员工/复制数据 → 计算每个员工薪资 → 编辑调整 → 归档批次 → 生成工资条
|
||
```
|
||
|
||
一个月份可以有多个批次(如第1批常规发薪、第2批离职结算、第3批奖金),每个批次独立计算,归档后汇总生成一张工资条。
|
||
|
||
**核心约束**:当月存在未归档(DRAFT)的批次时,不允许创建新批次,确保个税按批次累计计算。
|
||
|
||
## 二、批次类型
|
||
|
||
| 类型 | code | 社保公积金 | 个税方式 |
|
||
|------|------|-----------|---------|
|
||
| 常规发薪 | `REGULAR` | 正常扣缴 | 累计预扣法 |
|
||
| 离职结算 | `TERMINATION` | 正常扣缴 | 累计预扣法 |
|
||
| 年终奖/奖金 | `BONUS` | 不扣 | 单独计税 |
|
||
| 补偿金 | `SEVERANCE` | 不扣 | 累计预扣法 |
|
||
|
||
## 三、创建批次(POST /batches)
|
||
|
||
### 3.1 前置校验
|
||
|
||
- 当月存在 `DRAFT` 状态的批次时,拒绝创建,返回错误"当月存在未归档的批次,请先归档后再创建新批次"
|
||
- 确保新批次创建时,之前所有批次均已归档(已生成工资条),保证个税累计计算正确
|
||
|
||
### 3.2 确定批次号
|
||
|
||
取当月最大 `batchNo + 1`,避免删除批次后 `count` 不准导致唯一键冲突(`@@unique([orgId, month, batchNo])`)。
|
||
|
||
### 3.3 四种初始化模式
|
||
|
||
| 模式 | 说明 |
|
||
|------|------|
|
||
| `copy_last` | 拉入在职员工,从上月工资条复制基本工资/津贴/扣款,自动取当月加班费 |
|
||
| `blank_employees` | 拉入在职员工,所有金额为0,仅从员工档案取基本工资 |
|
||
| `blank_all` | 空批次,不拉入员工,后续手动添加 |
|
||
| `copy_batch` | 从指定已归档批次复制员工和薪资数据 |
|
||
|
||
### 3.4 员工范围
|
||
|
||
- **常规发薪(REGULAR)**:`status = ACTIVE` + 当月离职员工(`status = RESIGNED` 且 `updatedAt` 在当月)
|
||
- **离职结算/补偿金(TERMINATION/SEVERANCE)**:当月有离职记录(`terminationDate` 在当月)的员工
|
||
|
||
### 3.5 社保公积金扣缴逻辑(按员工个人、按实际金额)
|
||
|
||
```
|
||
对每个员工独立判断:
|
||
1. 计算当月应缴社保公积金全额(基于社保基数和配置)
|
||
2. 查询该员工当月已归档的常规/离职批次中已扣的社保公积金累计金额
|
||
3. 本批次应扣 = max(0, 应缴全额 - 已扣金额)
|
||
- 已扣金额 = 0 → 本批次扣全额
|
||
- 已扣金额 < 应缴全额 → 本批次补扣差额
|
||
- 已扣金额 ≥ 应缴全额 → 本批次扣0(足额)
|
||
```
|
||
|
||
**规则**:
|
||
- 按员工个人维度、按实际金额计算,而非全局判断是否扣过
|
||
- 第1批:员工A扣全额社保 → 归档后 → 第2批中员工A已扣金额=全额,应扣=0;员工B未在第1批中,已扣=0,应扣=全额
|
||
- BONUS 和 SEVERANCE 批次本身不扣社保
|
||
- 用户可手动编辑覆盖社保值
|
||
|
||
## 四、单条目计算(calcBatchEntry)
|
||
|
||
### 4.1 应发合计
|
||
|
||
```
|
||
totalPay = baseSalary + overtimePay + allowance + bonus - deduction
|
||
```
|
||
|
||
### 4.2 社保公积金计算
|
||
|
||
**基数**:优先用员工核定基数(`employee.socialInsBase` / `employee.housingFundBase`),否则用基本工资
|
||
|
||
**社保(calcSocialInsurance)**:
|
||
- 养老/失业/工伤:基数 × 各自比例
|
||
- 医疗/生育:独立基数(`medicalBaseMin/Max`,fallback 到统一基数)
|
||
- 附加险种:支持固定金额或按比例
|
||
|
||
**公积金(calcHousingFund)**:
|
||
- 基数 × `housingEmp%`(个人)/ `housingOrg%`(单位)
|
||
|
||
**跳过条件**:
|
||
- `batchType = BONUS 或 SEVERANCE` → 不扣
|
||
- `skipSocial = true`(同月已有归档常规批次)→ 不扣
|
||
- `overrideSocial` 手动覆盖 → 用用户输入值
|
||
|
||
### 4.3 个税计算
|
||
|
||
**年终奖批次(BONUS)— 单独计税**:
|
||
|
||
```
|
||
monthlyBonus = bonus / 12
|
||
→ 查税率表确定 rate 和 quickDeduction
|
||
→ tax = bonus × rate - quickDeduction
|
||
```
|
||
|
||
| 月均奖金 | 税率 | 速算扣除数 |
|
||
|---------|------|-----------|
|
||
| ≤3000 | 3% | 0 |
|
||
| ≤12000 | 10% | 210 |
|
||
| ≤25000 | 20% | 1410 |
|
||
| ≤35000 | 25% | 2660 |
|
||
| ≤55000 | 30% | 4410 |
|
||
| ≤80000 | 35% | 7160 |
|
||
| >80000 | 45% | 15160 |
|
||
|
||
**其他批次(REGULAR/TERMINATION/SEVERANCE)— 累计预扣法**:
|
||
|
||
```
|
||
1. 查询该员工当年所有已归档批次的 BatchEntry(不依赖工资条是否已生成)
|
||
2. 累计收入 = Σ 已归档批次.totalPay + 本批次.totalPay
|
||
3. 累计社保 = Σ 已归档批次.socialEmp + 本批次.socialEmp
|
||
4. 累计公积金 = Σ 已归档批次.housingEmp + 本批次.housingEmp
|
||
5. 累计专项扣除 = employee.specialDeduction × 月份序号
|
||
6. 累计应纳税所得额 = max(0, 累计收入 - 5000×月份 - 累计社保 - 累计公积金 - 累计专项扣除)
|
||
7. 累计税额 = calcTax(累计应纳税所得额) // 查7级超额累进税率表
|
||
8. 当月应预扣 = max(0, 累计税额 - Σ 已归档批次.tax)
|
||
```
|
||
|
||
**7级超额累进税率表**:
|
||
|
||
| 累计应纳税所得额 | 税率 | 速算扣除数 |
|
||
|----------------|------|-----------|
|
||
| ≤36000 | 3% | 0 |
|
||
| ≤144000 | 10% | 2520 |
|
||
| ≤300000 | 20% | 16920 |
|
||
| ≤420000 | 25% | 31920 |
|
||
| ≤660000 | 30% | 52920 |
|
||
| ≤960000 | 35% | 85920 |
|
||
| >960000 | 45% | 181920 |
|
||
|
||
### 4.4 实发工资
|
||
|
||
```
|
||
netPay = totalPay - socialEmp - housingEmp - tax
|
||
```
|
||
|
||
## 五、添加/删除员工后更新汇总
|
||
|
||
添加或删除员工后,重新查询所有条目并汇总更新批次:
|
||
|
||
```
|
||
employeeCount = 条目数
|
||
totalPay = Σ entry.totalPay
|
||
totalNetPay = Σ entry.netPay
|
||
totalTax = Σ entry.tax
|
||
totalSocialEmp = Σ entry.socialEmp
|
||
totalSocialOrg = Σ entry.socialOrg
|
||
totalHousingEmp = Σ entry.housingEmp
|
||
totalHousingOrg = Σ entry.housingOrg
|
||
```
|
||
|
||
## 六、编辑条目后重算
|
||
|
||
- 合并用户修改的输入项(baseSalary/overtimePay/allowance/deduction/bonus)
|
||
- 重新调用 `calcBatchEntry`(含 `skipSocial` 逻辑)
|
||
- 如果用户手动输入了社保值,用 `overrideSocial` 优先
|
||
- 更新条目后重新汇总批次
|
||
|
||
## 七、归档批次(POST /batches/:batchId/archive)
|
||
|
||
- 将批次状态改为 `ARCHIVED`,记录 `archivedAt`
|
||
- 归档后批次锁定不可编辑、不可删除
|
||
- 当月存在未归档批次时,不允许创建新批次
|
||
|
||
## 八、取消归档(POST /batches/:batchId/unarchive)
|
||
|
||
- 将批次状态从 `ARCHIVED` 恢复为 `DRAFT`,清除 `archivedAt`
|
||
- **只能依次取消**:只能取消最后一个归档批次(按 `batchNo` 倒序),确保个税累计链不断裂
|
||
- 取消归档后处理工资条:
|
||
- 当月已无归档批次 → 删除该月所有相关员工的工资条(完全撤销个税累计)
|
||
- 当月仍有归档批次 → 基于剩余归档批次重新生成工资条(更新累计数据)
|
||
|
||
## 九、生成工资条(POST /payslips/generate)
|
||
|
||
### 8.1 汇总归档批次
|
||
|
||
```
|
||
1. 获取当月所有 ARCHIVED 批次
|
||
2. 按员工ID分组,累加所有批次的各项金额:
|
||
baseSalary, overtimePay, allowance, deduction, bonus,
|
||
socialEmp, socialOrg, housingEmp, housingOrg, tax, totalPay, netPay
|
||
```
|
||
|
||
### 8.2 生成/更新工资条
|
||
|
||
```
|
||
upsert(employeeId + month):
|
||
- 各项金额 = 多批次汇总值
|
||
- ytdIncome = Σ 历史工资条.totalPay + 当月汇总.totalPay
|
||
- ytdTaxDeducted = Σ 历史工资条.tax + 当月汇总.tax
|
||
- ytdSocialEmp, ytdHousingEmp 同理
|
||
- status = PUBLISHED
|
||
```
|
||
|
||
### 8.3 唯一约束
|
||
|
||
`Payslip` 有 `@@unique([employeeId, month])`,同一员工同月只有一条工资条,多次生成会覆盖更新。
|
||
|
||
## 九、多批次场景示例
|
||
|
||
### 场景1:常规工资 + 奖金
|
||
|
||
| 步骤 | 批次1(REGULAR) | 批次2(BONUS) |
|
||
|------|-----------------|---------------|
|
||
| 创建 | 拉入员工,复制上月数据 | 空白批次,手动添加人员 |
|
||
| 社保 | 正常扣缴 | 不扣 |
|
||
| 个税 | 累计预扣(含历史工资条) | 单独计税 |
|
||
| 归档 | 归档 | 归档 |
|
||
| 生成工资条 | 汇总批次1+2 → 一张工资条 |
|
||
|
||
### 场景2:常规工资 + 补发常规工资
|
||
|
||
| 步骤 | 批次1(REGULAR) | 批次2(REGULAR) |
|
||
|------|-----------------|-----------------|
|
||
| 创建 | 拉入员工,正常计算 | 拉入员工,skipSocial=true |
|
||
| 社保 | 正常扣缴 | 跳过(同月已有归档常规批次) |
|
||
| 个税 | 累计预扣(含历史) | 累计预扣(含历史+批次1的工资条) |
|
||
| 归档 | 归档 | 归档 |
|
||
| 生成工资条 | 汇总批次1+2 → 一张工资条,社保只算一次,个税累计 |
|
||
|
||
### 场景3:常规工资 + 离职结算
|
||
|
||
| 步骤 | 批次1(REGULAR) | 批次2(TERMINATION) |
|
||
|------|-----------------|---------------------|
|
||
| 创建 | 拉入在职员工 | 拉入当月离职员工 |
|
||
| 社保 | 正常扣缴 | 跳过(同月已有归档常规批次) |
|
||
| 个税 | 累计预扣 | 累计预扣(含批次1的工资条) |
|
||
| 归档 | 归档 | 归档 |
|
||
| 生成工资条 | 汇总批次1+2 → 一张工资条 |
|
||
|
||
## 十、关键代码位置
|
||
|
||
| 模块 | 文件 | 行号 |
|
||
|------|------|------|
|
||
| 创建批次 | `backend/src/routes/payroll2.routes.ts` | 220 |
|
||
| 单条目计算 | `backend/src/services/payroll.service.ts` | 126 |
|
||
| 社保计算 | `backend/src/services/payroll.service.ts` | 37 |
|
||
| 公积金计算 | `backend/src/services/payroll.service.ts` | 69 |
|
||
| 累计预扣个税 | `backend/src/services/payroll.service.ts` | 97 |
|
||
| 年终奖计税 | `backend/src/services/payroll.service.ts` | 108 |
|
||
| 编辑条目 | `backend/src/routes/payroll2.routes.ts` | 435 |
|
||
| 添加员工 | `backend/src/routes/payroll2.routes.ts` | 504 |
|
||
| 删除员工 | `backend/src/routes/payroll2.routes.ts` | 585 |
|
||
| 归档批次 | `backend/src/routes/payroll2.routes.ts` | 662 |
|
||
| 生成工资条 | `backend/src/services/payroll.service.ts` | 275 |
|