feat: 最低工资保护 + 社保/最低工资递延补扣机制
新增功能: 1. SocialYearStandard / SocialInsuranceConfig 增加 minWage 字段 2. BatchEntry 增加递延扣款字段(deferredSocialEmp/HousingEmp/MinWage) 3. calcBatchEntry 增加最低工资保护逻辑: - 实发 < 最低工资时,优先递延社保 → 递延公积金 → 递延最低工资补齐 - 递延金额记录到 BatchEntry,次月创建批次时自动补扣 4. prePayrollCheck 增加最低工资检查(第 11/12 项) 5. 前端社保配置增加最低工资输入框 6. 前端 BatchTab 质量门禁增加最低工资/递延扣款提示 处理场景: - 入职当月工资不足扣社保个人部分 → 递延到次月补扣 - 实发低于最低工资 → 补齐到最低工资,差额递延次月扣 - 次月创建批次时自动读取上月递延金额并补扣 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -201,7 +201,7 @@ export async function calcBatchEntry(
|
||||
month: string,
|
||||
inputs: { baseSalary: number; overtimePay: number; allowance: number; deduction: number; bonus: number; positionSalary?: number; performanceSalary?: number; senioritySalary?: number; transportAllowance?: number; mealAllowance?: number; housingAllowance?: number; communicationAllowance?: number; otherDeduction?: number },
|
||||
batchType: string = 'REGULAR',
|
||||
options?: { skipSocial?: boolean; overrideSocial?: { socialEmp?: number; socialOrg?: number; housingEmp?: number; housingOrg?: number } },
|
||||
options?: { skipSocial?: boolean; overrideSocial?: { socialEmp?: number; socialOrg?: number; housingEmp?: number; housingOrg?: number }; prevDeferred?: { socialEmp?: number; housingEmp?: number; minWage?: number } },
|
||||
) {
|
||||
const employee = await prisma.employee.findFirst({ where: { id: employeeId, orgId } })
|
||||
if (!employee) throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
@@ -274,9 +274,15 @@ export async function calcBatchEntry(
|
||||
socialOrg = Math.max(0, fullSocialOrg - deductedSocialOrg)
|
||||
housingEmp = Math.max(0, fullHousingEmp - deductedHousingEmp)
|
||||
housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg)
|
||||
|
||||
// 4. 叠加上月递延的社保/公积金(入职当月未扣完的部分,本月补扣)
|
||||
if (options?.prevDeferred) {
|
||||
socialEmp += options.prevDeferred.socialEmp || 0
|
||||
housingEmp += options.prevDeferred.housingEmp || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 保存系统计算值(覆盖前)
|
||||
// 保存系统计算值(覆盖前,含递延)
|
||||
const systemSocialEmp = socialEmp
|
||||
const systemSocialOrg = socialOrg
|
||||
const systemHousingEmp = housingEmp
|
||||
@@ -366,7 +372,58 @@ export async function calcBatchEntry(
|
||||
}
|
||||
}
|
||||
|
||||
const netPay = totalPay - socialEmp - housingEmp - tax
|
||||
// ── 最低工资保护 + 递延扣款逻辑 ──
|
||||
// 读取最低工资标准(从年度标准或旧配置表)
|
||||
let minWage = 0
|
||||
if (socialConfig && (socialConfig as any).minWage) {
|
||||
minWage = (socialConfig as any).minWage
|
||||
}
|
||||
|
||||
// 上月递延的最低工资补齐差额(本批次需扣回)
|
||||
const prevDeferredMinWage = options?.prevDeferred?.minWage || 0
|
||||
|
||||
// 初始实发 = 应发 - 社保 - 公积金 - 个税 - 上月递延最低工资补扣
|
||||
let netPay = totalPay - socialEmp - housingEmp - tax - prevDeferredMinWage
|
||||
|
||||
// 递延金额(本批次扣不动、递延到次月的部分)
|
||||
let deferredSocialEmp = 0
|
||||
let deferredHousingEmp = 0
|
||||
let deferredMinWage = 0
|
||||
let minWageApplied = 0 // 当月实际补齐到最低工资的金额
|
||||
|
||||
// 最低工资保护:实发不得低于最低工资标准(仅 REGULAR / TERMINATION 批次)
|
||||
if (minWage > 0 && batchType !== 'BONUS' && batchType !== 'SEVERANCE' && netPay < minWage) {
|
||||
const shortfall = minWage - netPay // 需要补齐的金额
|
||||
|
||||
// 优先递延社保个人部分(减少当月社保扣款)
|
||||
if (shortfall <= socialEmp) {
|
||||
// 只递延社保就够了
|
||||
deferredSocialEmp = shortfall
|
||||
socialEmp -= shortfall
|
||||
netPay = minWage
|
||||
minWageApplied = shortfall
|
||||
} else if (shortfall <= socialEmp + housingEmp) {
|
||||
// 递延全部社保 + 部分公积金
|
||||
deferredSocialEmp = socialEmp
|
||||
deferredHousingEmp = shortfall - socialEmp
|
||||
socialEmp = 0
|
||||
housingEmp -= deferredHousingEmp
|
||||
netPay = minWage
|
||||
minWageApplied = shortfall
|
||||
} else {
|
||||
// 递延全部社保 + 全部公积金,仍不足 → 差额作为最低工资补齐递延
|
||||
deferredSocialEmp = socialEmp
|
||||
deferredHousingEmp = housingEmp
|
||||
const remainingShortfall = shortfall - socialEmp - housingEmp
|
||||
socialEmp = 0
|
||||
housingEmp = 0
|
||||
// 此时 netPay = totalPay - tax - prevDeferredMinWage
|
||||
// 差额 = minWage - (totalPay - tax - prevDeferredMinWage)
|
||||
deferredMinWage = remainingShortfall
|
||||
netPay = minWage
|
||||
minWageApplied = shortfall
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
socialEmp: Math.round(socialEmp * 100) / 100,
|
||||
@@ -381,6 +438,15 @@ export async function calcBatchEntry(
|
||||
taxBreakdown,
|
||||
totalPay: Math.round(totalPay * 100) / 100,
|
||||
netPay: Math.round(netPay * 100) / 100,
|
||||
// 最低工资保护 + 递延信息
|
||||
minWage,
|
||||
minWageApplied: Math.round(minWageApplied * 100) / 100,
|
||||
deferredSocialEmp: Math.round(deferredSocialEmp * 100) / 100,
|
||||
deferredHousingEmp: Math.round(deferredHousingEmp * 100) / 100,
|
||||
deferredMinWage: Math.round(deferredMinWage * 100) / 100,
|
||||
prevDeferredSocialEmp: options?.prevDeferred?.socialEmp || 0,
|
||||
prevDeferredHousingEmp: options?.prevDeferred?.housingEmp || 0,
|
||||
prevDeferredMinWage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,6 +866,57 @@ export async function prePayrollCheck(orgId: string, batchId: string): Promise<P
|
||||
}
|
||||
}
|
||||
|
||||
// 11. 实发工资低于最低工资标准(剔除加班费后比较)
|
||||
for (const entry of entries) {
|
||||
const minWage = (entry as any).minWage || 0
|
||||
if (minWage > 0 && batch.type !== 'BONUS' && batch.type !== 'SEVERANCE') {
|
||||
// 最低工资剔除项:加班费、高温/夜班津贴等不计入
|
||||
const comparablePay = entry.totalPay - entry.overtimePay
|
||||
if (comparablePay < minWage && entry.netPay < minWage) {
|
||||
const applied = (entry as any).minWageApplied || 0
|
||||
if (applied > 0) {
|
||||
// 已触发最低工资保护,检查递延情况
|
||||
const deferredTotal = ((entry as any).deferredSocialEmp || 0) + ((entry as any).deferredHousingEmp || 0) + ((entry as any).deferredMinWage || 0)
|
||||
checks.push({
|
||||
code: 'MIN_WAGE_DEFERRED',
|
||||
name: '最低工资保护已触发(递延扣款)',
|
||||
status: 'WARNING',
|
||||
message: `${entry.employee.name} 应发 ¥${comparablePay}(剔除加班费)低于最低工资 ¥${minWage},已补齐 ¥${applied},递延扣款 ¥${deferredTotal} 将在次月补扣`,
|
||||
employeeId: entry.employeeId,
|
||||
employeeName: entry.employee.name,
|
||||
detail: { comparablePay, minWage, applied, deferredTotal },
|
||||
})
|
||||
} else {
|
||||
checks.push({
|
||||
code: 'BELOW_MIN_WAGE',
|
||||
name: '实发低于最低工资标准',
|
||||
status: 'FAIL',
|
||||
message: `${entry.employee.name} 实发 ¥${entry.netPay} 低于当地最低工资标准 ¥${minWage},请检查社保基数或启用最低工资保护`,
|
||||
employeeId: entry.employeeId,
|
||||
employeeName: entry.employee.name,
|
||||
detail: { netPay: entry.netPay, minWage, comparablePay },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 12. 上月递延扣款待补扣
|
||||
for (const entry of entries) {
|
||||
const prevDeferred = ((entry as any).prevDeferredSocialEmp || 0) + ((entry as any).prevDeferredHousingEmp || 0) + ((entry as any).prevDeferredMinWage || 0)
|
||||
if (prevDeferred > 0) {
|
||||
checks.push({
|
||||
code: 'PREV_DEFERRED_RECOVERED',
|
||||
name: '上月递延扣款已补扣',
|
||||
status: 'WARNING',
|
||||
message: `${entry.employee.name} 本月补扣上月递延 ¥${prevDeferred}(社保 ¥${(entry as any).prevDeferredSocialEmp || 0} + 公积金 ¥${(entry as any).prevDeferredHousingEmp || 0} + 最低工资补齐 ¥${(entry as any).prevDeferredMinWage || 0})`,
|
||||
employeeId: entry.employeeId,
|
||||
employeeName: entry.employee.name,
|
||||
detail: { prevDeferred },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 汇总
|
||||
const passedCount = entries.length > 0 ? Math.max(0, entries.length - checks.filter(c => c.employeeId).length) : 0
|
||||
const failedCount = checks.filter(c => c.status === 'FAIL').length
|
||||
|
||||
Reference in New Issue
Block a user