Files
TurboHR/20260816-薪税管理逻辑.md
T
selfrelease 014c94e482 feat: 账户关联根部门 + 员工新增自动带出账户
1. 账户管理:新建/编辑时可勾选关联 level=0 根部门(公司/分公司/子公司)
2. 后端新增 API:
   - PUT /social/accounts/:id/departments 批量关联根部门
   - GET /social/accounts/:id/departments 查询已关联部门
   - GET /social/department-account/:departmentId 按部门带出适用账户+标准
3. 部门更新 API 支持 socialAccountId/housingAccountId 字段
4. 员工新增表单:选定部门后自动带出社保公积金账户,可手动调整
5. 参保记录创建时写入 accountId

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-16 13:59:06 +08:00

20 KiB
Raw Blame History

薪税管理完整处理逻辑和取数逻辑

生成时间:2026-08-16 涉及模块:薪资发放批次、社保公积金、个税累计预扣、工资条生成与发布 核心文件:backend/src/services/payroll.service.tsbackend/src/routes/payroll2.routes.tsbackend/src/routes/social.routes.ts


一、整体架构

┌─────────────────────────────────────────────────────────────┐
│                    薪税管理三大模块                          │
├──────────────┬──────────────────┬───────────────────────────┤
│  社保公积金   │    个税计算       │    工资条生成/发布        │
│  (缴纳/扣除)  │  (累计预扣法)     │    (汇总/发布/定时)       │
└──────────────┴──────────────────┴───────────────────────────┘
         ↓ 取数 ↓                ↓ 取数 ↓
┌──────────────┬──────────────────┬───────────────────────────┐
│ SocialAccount│ Employee 表      │ PayrollBatch (已归档)     │
│ + YearStandard│ SpecialDeduction │ + BatchEntry             │
│ + 旧Config表  │ + Record表       │ → Payslip                │
└──────────────┴──────────────────┴───────────────────────────┘

二、核心计算函数 calcBatchEntry

这是所有薪税计算的入口,在三个时机被调用:

  1. 创建批次时:为每个员工初始化计算
  2. 编辑条目时:修改任意金额后实时重算
  3. 归档时:最终锁定前再重算一遍

输入参数

calcBatchEntry(
  orgId,           // 组织ID
  employeeId,      // 员工ID
  month,           // 批次月份 YYYY-MM
  inputs: {        // HR 可编辑的输入项
    baseSalary,          // 基本工资
    overtimePay,         // 加班费
    allowance,           // 其他津贴
    deduction,           // 扣款
    bonus,               // 奖金
    positionSalary?,     // 岗位工资
    performanceSalary?,  // 绩效工资
    senioritySalary?,    // 工龄工资
    transportAllowance?, // 交通补贴
    mealAllowance?,      // 餐补
    housingAllowance?,   // 住房补贴
    communicationAllowance?, // 通讯补贴
    otherDeduction?,     // 其他扣款
  },
  batchType,       // REGULAR | TERMINATION | BONUS | SEVERANCE
  options?: {      // 可选覆盖
    skipSocial?,        // 跳过社保计算
    overrideSocial?,    // 手动覆盖社保值
  }
)

三、社保公积金取数逻辑

取数优先级(三层回退)

① 员工 → 根部门(level=0) → socialAccount / housingAccount
   ↓ 找不到
② 公司默认账户 (isDefault=true)
   ↓ 找不到
③ 旧配置表 SocialInsuranceConfig / HousingFundConfig(按城市匹配)

新逻辑(账户体系)

getEmployeeAccounts(orgId, employeeId)
  ├─ 查员工所属部门
  ├─ 向上遍历到 level=0 的根部门
  ├─ 读取根部门的 socialAccountId / housingAccountId
  ├─ 若根部门未关联 → 查公司默认账户 (isDefault=true)
  └─ 返回 { socialAccount, housingAccount }

getStandardByAccountAndMonth(accountId, month)
  ├─ 查 SocialYearStandard: accountId + effectiveFrom ≤ month ≤ effectiveTo
  ├─ 找不到 → 回退到 isCurrent=true 的当前标准
  └─ 返回年度标准(比例 + 基数上下限)

旧逻辑(兼容回退)

SocialInsuranceConfig.findFirst({
  orgId, city: employee.city,
  effectiveFrom ≤ month, effectiveTo ≥ month 或 null
})

社保基数确定

socialBase = employee.socialInsBase || inputs.baseSalary
housingBase = employee.housingFundBase || inputs.baseSalary

优先级:员工核定基数 > 基本工资

社保金额计算

// 当月应缴全额
fullSocialEmp = calcSocialInsurance(socialBase, socialConfig).socialEmp
fullSocialOrg = calcSocialInsurance(socialBase, socialConfig).socialOrg
fullHousingEmp = calcHousingFund(housingBase, housingConfig).housingEmp
fullHousingOrg = calcHousingFund(housingBase, housingConfig).housingOrg

// 已归档批次已扣金额(避免多批次重复扣社保)
deductedSocialEmp = SUM(archivedEntries.socialEmp)
deductedSocialOrg = SUM(archivedEntries.socialOrg)
deductedHousingEmp = SUM(archivedEntries.housingEmp)
deductedHousingOrg = SUM(archivedEntries.housingOrg)

// 本批次应扣 = 应缴全额 - 已扣金额(差额补扣,足额为0)
socialEmp = Math.max(0, fullSocialEmp - deductedSocialEmp)
socialOrg = Math.max(0, fullSocialOrg - deductedSocialOrg)
housingEmp = Math.max(0, fullHousingEmp - deductedHousingEmp)
housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg)

关键设计:差额补扣机制

  • 同一员工同一月份可能有多个批次(如常规发薪 + 离职结算)
  • 第一个批次扣全额,后续批次扣差额(应缴全额 - 已扣)
  • 避免重复扣除

批次类型对社保的影响

批次类型 社保公积金 说明
REGULAR 计算 常规发薪,差额补扣
TERMINATION 计算 离职结算,差额补扣
BONUS 不扣 年终奖单独计税,无社保
SEVERANCE 不扣 补偿金不走社保个税

手动覆盖

// HR 可在编辑界面手动修改社保值
if (options?.overrideSocial) {
  socialEmp = overrideSocial.socialEmp   // 覆盖个人社保
  socialOrg = overrideSocial.socialOrg   // 覆盖单位社保
  housingEmp = overrideSocial.housingEmp // 覆盖个人公积金
  housingOrg = overrideSocial.housingOrg // 覆盖单位公积金
}
// 同时保存 systemSocialEmp 等系统计算值,用于对比展示

四、应发合计计算

totalPay =
  baseSalary              // 基本工资
  + positionSalary        // 岗位工资
  + performanceSalary     // 绩效工资
  + senioritySalary       // 工龄工资
  + overtimePay           // 加班费
  + transportAllowance    // 交通补贴
  + mealAllowance         // 餐补
  + housingAllowance      // 住房补贴
  + communicationAllowance // 通讯补贴
  + allowance             // 其他津贴
  + bonus                 // 奖金
  - deduction             // 扣款
  - otherDeduction        // 其他扣款

五、个税计算逻辑

两种计税方式

1. 年终奖单独计税(BONUS 批次)

tax = calcBonusTax(inputs.bonus)
// 年终奖 ÷ 12 → 找税率区间 → 年终奖 × 税率 - 速算扣除数

2. 累计预扣法(REGULAR / TERMINATION / SEVERANCE

// ── 取数:当年已归档批次的历史数据 ──
archivedEntries = BatchEntry.findMany({
  orgId, employeeId,
  batch: { month: startsWith(year), status: 'ARCHIVED' }
})
// 注意:不依赖 Payslip 是否已生成,直接从已归档批次取数

// ── 累计计算 ──
ytdIncome         = SUM(archivedEntries.totalPay) + totalPay
ytdSocialEmp      = SUM(archivedEntries.socialEmp) + socialEmp
ytdHousingEmp     = SUM(archivedEntries.housingEmp) + housingEmp
ytdTaxDeducted    = SUM(archivedEntries.tax)

// ── 专项附加扣除取数(三层回退)──
deductionRecords = SpecialDeductionRecord.findMany({
  orgId, employeeId,
  month: startsWith(year) AND lte(month)   // 当年至当月
})

if (deductionRecords.length > 0) {
  // ✅ 优先:按月实际填报金额累加
  ytdSpecialDeduction = SUM(deductionRecords.amount)
} else {
  // ⚠️ 回退:员工便捷字段 × 月数(兼容旧数据)
  ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7))
}

// ── 累计应纳税所得额 ──
deductionAmount    = 5000 × 月数                    // 基本减除费用
ytdTaxableIncome   = max(0,
  ytdIncome
  - deductionAmount       // 累计减除费用(5000/月)
  - ytdSocialEmp          // 累计个人社保
  - ytdHousingEmp         // 累计个人公积金
  - ytdSpecialDeduction   // 累计专项附加扣除
)

// ── 当月应扣个税 ──
tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted)
// = 累计应纳税额 - 已预扣个税

专项附加扣除数据来源

来源 说明
按月记录(优先) SpecialDeductionRecord HR/员工按月填报,含子女教育、赡养老人、住房贷款、继续教育、婴幼儿照护
便捷字段(回退) Employee.specialDeduction 员工表上的当前值,× 月数估算累计

按月记录的优势:员工某月取消专项附加扣除,该月金额为 0,累计值准确反映实际。

taxBreakdown 返回的明细

{
  "method": "累计预扣法",
  "month": 8,
  "ytdIncome": 80000,           // 累计收入
  "deductionAmount": 40000,      // 累计减除费用 (5000×8)
  "ytdSocialEmp": 6720,          // 累计个人社保
  "ytdHousingEmp": 3840,         // 累计个人公积金
  "ytdSpecialDeduction": 12000,  // 累计专项附加扣除
  "specialDeductionSource": "按月记录",  // 数据来源标识
  "specialDeductionRecords": 8,  // 按月记录条数
  "ytdTaxableIncome": 17440,     // 累计应纳税所得额
  "ytdTaxDeducted": 480,         // 已预扣个税
  "currentMonthTax": 360,        // 当月应扣个税
  "archivedCount": 7             // 已归档批次数
}

六、实发工资

netPay = totalPay - socialEmp - housingEmp - tax
// 应发 - 个人社保 - 个人公积金 - 个税

七、创建批次时的初始化取数

员工来源

批次类型 员工来源
REGULAR / BONUS status=ACTIVE + 本月离职(status=RESIGNED, updatedAt 在本月
TERMINATION 本月 TerminationRecord 关联的员工
SEVERANCE 本月离职记录中已审批(APPROVED/EXECUTING/COMPLETED)且有补偿金的

数据初始化(5 种模式)

模式 baseSalary 取数 overtimePay allowance/deduction
copy_last 试用期→probationSalary;转正→上月工资条baseSalary;无→monthlySalary 当月 OvertimeRecord.totalPay 上月工资条复制
blank_employees employee.monthlySalary(解密) 0 0
blank_all 0(无员工) 0 0
copy_batch 源批次条目复制 源批次复制 源批次复制
custom 0(手动填写) 0 0

试用期判定

isInProbation(contract, batchMonthEnd)
// 试用期结束日 = contract.startDate + contract.probationMonths
// 若 batchMonthEnd < 试用期结束日 → 在试用期内

统一规则(所有模式适用,SEVERANCE/TERMINATION 除外):

  • 在试用期内且 probationSalary > 0baseSalary = probationSalary

八、编辑条目时的重算

HR 修改任意金额字段
  ↓
PUT /batches/:batchId/entries/:employeeId
  ↓
合并新旧 inputs(未修改的保留原值)
  ↓
重新调用 calcBatchEntry() → 重算社保/个税/实发
  ↓
更新 BatchEntry + 重算批次汇总

社保手动覆盖:编辑社保字段时,通过 overrideSocial 传入,覆盖系统计算值,同时保存 systemSocial* 用于对比。


九、归档时的最终重算

POST /batches/:batchId/archive
  ↓
① 遍历所有条目,重新调用 calcBatchEntry()
   - 此时已归档批次的累计数据是最新的
   - 社保差额补扣准确
   - 个税累计预扣准确
② 更新批次汇总(totalPay/totalNetPay/各项合计)
③ 标记 status=ARCHIVED, archivedAt=now
④ 批次锁定,不可再编辑

归档时不自动生成工资条,需单独调用 POST /payslips/generate


十、工资条生成与发布

生成工资条

POST /payslips/generate { month }
  ↓
generatePayslipFromBatches(orgId, month)
  ↓
① 查当月所有已归档批次(status=ARCHIVED
② 按员工汇总所有批次的 BatchEntry(多批次合并)
③ 查当年历史工资条计算累计数据
④ upsert PayslipemployeeId+month 唯一键)

发布工资条

POST /batches/:batchId/publish
  ↓
更新 Payslip.publishStatus = 'PUBLISHED', publishedAt = now
  ↓
员工端可见

定时发送

POST /batches/:batchId/schedule { scheduledAt }
  ↓
更新 Payslip.publishStatus = 'SCHEDULED', scheduledAt = 指定时间
  ↓
(定时任务到点后发布)

十一、取消归档

POST /batches/:batchId/unarchive
  ↓
① 只能取消最后一个归档批次
② 批次恢复为 DRAFT
③ 如果还有其他归档批次 → 重新生成工资条(基于剩余批次)
④ 如果没有归档批次了 → 删除该月工资条

十二、完整数据流图

                    ┌─────────────────┐
                    │  Employee 表     │
                    │  socialInsBase   │
                    │  housingFundBase │
                    │  monthlySalary   │
                    │  specialDeduction│
                    └────────┬────────┘
                             │
    ┌────────────────────────┼────────────────────┐
    │                        │                    │
    ▼                        ▼                    ▼
┌──────────┐    ┌──────────────────┐    ┌─────────────────┐
│SocialAccount│  │SpecialDeduction  │    │ OvertimeRecord  │
│+YearStandard│  │Record (按月)     │    │ (加班费)        │
│(社保比例)   │  └──────────────────┘    └─────────────────┘
└──────┬─────┘           │                    │
       │                 │                    │
       ▼                 ▼                    ▼
┌──────────────────────────────────────────────────────┐
│              calcBatchEntry()                         │
│                                                       │
│  社保 = 应缴全额 - 已归档批次已扣(差额补扣)          │
│  应发 = 基本工资 + 各项津贴 + 奖金 - 扣款             │
│  个税 = 累计预扣法(从已归档批次取累计数据)           │
│  实发 = 应发 - 个人社保 - 个人公积金 - 个税            │
└──────────────────────┬───────────────────────────────┘
                       │
                       ▼
              ┌────────────────┐
              │  BatchEntry    │
              │  (批次条目)     │
              └───────┬────────┘
                      │ 归档
                      ▼
              ┌────────────────┐      汇总生成
              │ PayrollBatch   │ ──────────────→ ┌──────────┐
              │ status=ARCHIVED│                 │ Payslip  │
              └────────────────┘                 │ (工资条) │
                                                 └────┬─────┘
                                                      │ 发布
                                                      ▼
                                               员工端可见

十三、关键取数表汇总

数据项 取数表 取数条件 用途
社保比例 SocialYearStandard accountId + 月份在生效区间 计算社保公积金
社保比例(回退) SocialInsuranceConfig orgId + city + 月份在生效区间 兼容旧数据
社保基数 Employee.socialInsBase 优先于基本工资
公积金基数 Employee.housingFundBase 优先于基本工资
已扣社保 BatchEntry 当月已归档批次 差额补扣
累计收入 BatchEntry 当年已归档批次 个税累计预扣
累计社保 BatchEntry 当年已归档批次 个税累计预扣
累计个税 BatchEntry 当年已归档批次 个税累计预扣
专项附加扣除 SpecialDeductionRecord 当年至当月 个税累计预扣
专项附加扣除(回退) Employee.specialDeduction × 月数 兼容旧数据
基本减除费用 固定 5000 × 月数 个税累计预扣
加班费 OvertimeRecord employeeId + month 创建批次时自动拉取
基本工资 Payslip(上月) employeeId + 上月 copy_last 模式
基本工资(回退) Employee.monthlySalary 解密 无上月工资条时
试用期工资 LaborContract.probationSalary 最新合同 试用期内的 baseSalary

十四、4 步工作流

① 编辑薪资 → ② 核对汇总 → ③ 归档锁定 → ④ 发布工资条

步骤 1:编辑薪资(DRAFT 状态)

可操作:

  • 点击单元格编辑:基本工资、加班费、津贴、奖金、扣款、社保个人/单位、公积金个人/单位
  • 导入工资表 Excel:批量填充薪资数据
  • 下载导入模板
  • 导入加班费:从加班记录按月自动填充 overtimePay
  • 获取提成奖金:从提成奖金模块按月填充 bonus
  • 添加/删除人员:动态调整批次人员
  • 查看个税明细:点击个税金额查看累计预扣计算过程

质量门禁(草稿状态自动检查):

  • 实发为负的员工 → 红色警告
  • 全零记录 → 黄色警告

步骤 2:核对汇总

批次列表展示汇总数据:

  • 应发合计、社保合计、公积金合计、个税合计、实发合计
  • 可导出 薪资汇总表 / 薪资明细表CSV

步骤 3:归档锁定(ARCHIVED

归档时后端自动执行

  1. 重算所有条目:调用 calcBatchEntry 重新计算社保公积金和个税
  2. 更新批次汇总:重算 totalPay/totalNetPay/各项合计
  3. 标记为 ARCHIVED:设置 archivedAt,不可再编辑

步骤 4:发布工资条

  • 调用 POST /batches/:id/publish → 更新 Payslip.publishStatus = PUBLISHED
  • 或调用 POST /batches/:id/schedule → 设置 publishStatus = SCHEDULED + scheduledAt
  • 员工在员工端查看已发布的工资条

十五、状态流转

DRAFT(草稿,可编辑)
  ↓ 归档
ARCHIVED(已归档,锁定不可编辑)
  ↓ 取消归档
DRAFT(恢复草稿)
  ↓ 发布工资条
Payslip.publishStatus: PUBLISHED(员工端可见)

十六、批次类型详解

类型 说明 社保公积金 个税计算 员工来源
REGULAR 常规发薪 月度工资 差额补扣 累计预扣法 在职 + 本月离职
TERMINATION 离职结算 离职员工当月工资 差额补扣 累计预扣法 本月离职记录
BONUS 年终奖/奖金 单独计税 不扣 单独计税 在职 + 本月离职
SEVERANCE 补偿金 离职补偿金 不扣 累计预扣法(无社保扣除) 已审批且有补偿金的离职记录

十七、创建批次的 5 种数据初始化模式

模式 说明 员工来源 数据来源
copy_last 复制上月 默认模式 在职 + 本月离职 上月工资条复制基本工资/津贴/扣款 + 当月加班费
blank_employees 本月空白 拉入员工 在职 + 本月离职 所有金额为 0,手动填写
blank_all 全空白 不拉入员工 后续手动添加人员
copy_batch 复制指定批次 从源批次 源批次的员工 复制源批次薪资数据
custom 自定义选择 按部门/姓名筛选勾选 指定员工 金额为 0,手动填写