feat: 补充公积金支持(员工级别可选)

上海等城市存在补充公积金账户,同一企业内高管可能有但普通员工
没有。新增员工级别补充公积金账户关联:

- Employee 表加 supplementaryHousingAccountId 字段
- BatchEntry 表加 supplementaryHousingEmp/Org 字段
- 薪资计算 calcBatchEntry 支持补充公积金(额外算一笔,纳入
  个税扣除、最低工资保护递延逻辑)
- 新增员工弹窗加补充公积金账户选择器(可选,仅显示
  accountType=SUPPLEMENTARY 的公积金账户)
- 员工档案编辑支持补充公积金账户

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-17 10:20:59 +08:00
parent 1e7932c36c
commit 16f38f3c36
7 changed files with 82 additions and 14 deletions
+2
View File
@@ -407,6 +407,7 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
education: data.education || null,
position: data.position || null,
status: data.status || 'ACTIVE',
supplementaryHousingAccountId: data.supplementaryHousingAccountId || null,
},
})
@@ -759,6 +760,7 @@ export async function updateEmployee(orgId: string, id: string, data: any) {
if (data.education !== undefined) updateData.education = data.education
if (data.position !== undefined) updateData.position = data.position
if (data.status !== undefined) updateData.status = data.status
if (data.supplementaryHousingAccountId !== undefined) updateData.supplementaryHousingAccountId = data.supplementaryHousingAccountId || null
// 参保城市变更:关闭旧城市在保记录,创建新城市记录
if (data.city !== undefined && data.city !== employee.city) {
+44 -11
View File
@@ -220,6 +220,7 @@ export async function calcBatchEntry(
const { socialAccount, housingAccount } = await getEmployeeAccounts(orgId, employeeId)
let socialConfig: any = null
let housingConfig: any = null
let supplementaryHousingConfig: any = null
if (socialAccount) {
socialConfig = await getStandardByAccountAndMonth(socialAccount.id, month)
@@ -227,6 +228,10 @@ export async function calcBatchEntry(
if (housingAccount) {
housingConfig = await getStandardByAccountAndMonth(housingAccount.id, month)
}
// 补充公积金:员工单独关联的补充公积金账户
if (employee.supplementaryHousingAccountId) {
supplementaryHousingConfig = await getStandardByAccountAndMonth(employee.supplementaryHousingAccountId, month)
}
// 回退到旧配置表(兼容未迁移数据)
const cityWhere = employee.city ? { orgId, city: employee.city } : { orgId }
@@ -248,12 +253,14 @@ export async function calcBatchEntry(
const housingBase = isNoSocialContract ? 0 : (employee.housingFundBase != null ? employee.housingFundBase : inputs.baseSalary)
let socialEmp = 0, socialOrg = 0, housingEmp = 0, housingOrg = 0
let suppHousingEmp = 0, suppHousingOrg = 0
// 年终奖/奖金批次、补偿金批次、劳务协议/实习协议人员:不扣社保公积金
// 其他批次:计算当月应缴全额,减去已归档批次已扣金额,差额为本批次应扣
if (batchType !== 'BONUS' && batchType !== 'SEVERANCE' && !options?.skipSocial && !isNoSocialContract) {
// 1. 计算当月应缴社保公积金全额
let fullSocialEmp = 0, fullSocialOrg = 0, fullHousingEmp = 0, fullHousingOrg = 0
let fullSuppHousingEmp = 0, fullSuppHousingOrg = 0
if (socialConfig) {
const social = calcSocialInsurance(socialBase, socialConfig)
fullSocialEmp = social.socialEmp
@@ -264,6 +271,12 @@ export async function calcBatchEntry(
fullHousingEmp = housing.housingEmp
fullHousingOrg = housing.housingOrg
}
// 补充公积金(用同样的公积金基数)
if (supplementaryHousingConfig) {
const suppHousing = calcHousingFund(housingBase, supplementaryHousingConfig)
fullSuppHousingEmp = suppHousing.housingEmp
fullSuppHousingOrg = suppHousing.housingOrg
}
// 2. 查询该员工当月已归档批次中已扣的社保公积金金额
const archivedEntries = await prisma.batchEntry.findMany({
@@ -272,18 +285,22 @@ export async function calcBatchEntry(
employeeId,
batch: { month, status: 'ARCHIVED', type: { in: ['REGULAR', 'TERMINATION'] } },
},
select: { socialEmp: true, socialOrg: true, housingEmp: true, housingOrg: true },
select: { socialEmp: true, socialOrg: true, housingEmp: true, housingOrg: true, supplementaryHousingEmp: true, supplementaryHousingOrg: true },
})
const deductedSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0)
const deductedSocialOrg = archivedEntries.reduce((s, e) => s + e.socialOrg, 0)
const deductedHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0)
const deductedHousingOrg = archivedEntries.reduce((s, e) => s + e.housingOrg, 0)
const deductedSuppHousingEmp = archivedEntries.reduce((s, e) => s + (e.supplementaryHousingEmp || 0), 0)
const deductedSuppHousingOrg = archivedEntries.reduce((s, e) => s + (e.supplementaryHousingOrg || 0), 0)
// 3. 本批次应扣 = 应缴全额 - 已扣金额(不足额补扣,足额为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)
suppHousingEmp = Math.max(0, fullSuppHousingEmp - deductedSuppHousingEmp)
suppHousingOrg = Math.max(0, fullSuppHousingOrg - deductedSuppHousingOrg)
// 4. 叠加上月递延的社保/公积金(入职当月未扣完的部分,本月补扣)
if (options?.prevDeferred) {
@@ -297,6 +314,8 @@ export async function calcBatchEntry(
const systemSocialOrg = socialOrg
const systemHousingEmp = housingEmp
const systemHousingOrg = housingOrg
const systemSupplementaryHousingEmp = suppHousingEmp
const systemSupplementaryHousingOrg = suppHousingOrg
// 手动覆盖社保值
if (options?.overrideSocial) {
@@ -340,11 +359,11 @@ export async function calcBatchEntry(
status: 'ARCHIVED',
},
},
select: { totalPay: true, socialEmp: true, housingEmp: true, tax: true },
select: { totalPay: true, socialEmp: true, housingEmp: true, supplementaryHousingEmp: true, tax: true },
})
const ytdIncome = archivedEntries.reduce((s, e) => s + e.totalPay, 0) + totalPay
const ytdSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0) + socialEmp
const ytdHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0) + housingEmp
const ytdHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp + (e.supplementaryHousingEmp || 0), 0) + housingEmp + suppHousingEmp
// 专项附加扣除:按月读取 SpecialDeductionRecord 实际填报金额累加
// 优先使用按月记录;若无按月记录,回退到员工便捷字段 × 月数(兼容旧数据)
@@ -392,8 +411,8 @@ export async function calcBatchEntry(
// 上月递延的最低工资补齐差额(本批次需扣回)
const prevDeferredMinWage = options?.prevDeferred?.minWage || 0
// 初始实发 = 应发 - 社保 - 公积金 - 个税 - 上月递延最低工资补扣
let netPay = totalPay - socialEmp - housingEmp - tax - prevDeferredMinWage
// 初始实发 = 应发 - 社保 - 公积金 - 补充公积金 - 个税 - 上月递延最低工资补扣
let netPay = totalPay - socialEmp - housingEmp - suppHousingEmp - tax - prevDeferredMinWage
// 递延金额(本批次扣不动、递延到次月的部分)
let deferredSocialEmp = 0
@@ -433,21 +452,31 @@ export async function calcBatchEntry(
socialEmp -= shortfall
netPay = targetNetPay
minWageApplied = shortfall
} else if (shortfall <= socialEmp + housingEmp) {
// 递延全部社保 + 部分公积金
} else if (shortfall <= socialEmp + housingEmp + suppHousingEmp) {
// 递延全部社保 + 部分公积金(含补充)
deferredSocialEmp = socialEmp
deferredHousingEmp = shortfall - socialEmp
socialEmp = 0
housingEmp -= deferredHousingEmp
const housingShortfall = shortfall - deferredSocialEmp
// 先递延基本公积金,再递延补充公积金
if (housingShortfall <= housingEmp) {
deferredHousingEmp = housingShortfall
housingEmp -= housingShortfall
} else {
deferredHousingEmp = housingEmp
const suppShortfall = housingShortfall - housingEmp
housingEmp = 0
suppHousingEmp -= suppShortfall
}
netPay = targetNetPay
minWageApplied = shortfall
} else {
// 递延全部社保 + 全部公积金,仍不足 → 差额作为最低工资补齐递延
// 递延全部社保 + 全部公积金(含补充),仍不足 → 差额作为最低工资补齐递延
deferredSocialEmp = socialEmp
deferredHousingEmp = housingEmp
const remainingShortfall = shortfall - socialEmp - housingEmp
const remainingShortfall = shortfall - socialEmp - housingEmp - suppHousingEmp
socialEmp = 0
housingEmp = 0
suppHousingEmp = 0
deferredMinWage = remainingShortfall
netPay = targetNetPay
minWageApplied = shortfall
@@ -461,10 +490,14 @@ export async function calcBatchEntry(
socialOrg: Math.round(socialOrg * 100) / 100,
housingEmp: Math.round(housingEmp * 100) / 100,
housingOrg: Math.round(housingOrg * 100) / 100,
supplementaryHousingEmp: Math.round(suppHousingEmp * 100) / 100,
supplementaryHousingOrg: Math.round(suppHousingOrg * 100) / 100,
systemSocialEmp: Math.round(systemSocialEmp * 100) / 100,
systemSocialOrg: Math.round(systemSocialOrg * 100) / 100,
systemHousingEmp: Math.round(systemHousingEmp * 100) / 100,
systemHousingOrg: Math.round(systemHousingOrg * 100) / 100,
systemSupplementaryHousingEmp: Math.round(systemSupplementaryHousingEmp * 100) / 100,
systemSupplementaryHousingOrg: Math.round(systemSupplementaryHousingOrg * 100) / 100,
tax,
taxBreakdown,
totalPay: Math.round(totalPay * 100) / 100,