fix: 个税累计预扣中专项附加扣除改为按月读取实际填报金额
原逻辑用 employee.specialDeduction(便捷字段)× 月数计算累计 专项附加扣除,未按月读取 SpecialDeductionRecord 实际金额。 若员工某月填报/取消专项附加扣除,累计值会不准。 修复: - 优先按月查询 SpecialDeductionRecord(当年至当月)累加实际金额 - 无按月记录时回退到便捷字段 × 月数(兼容旧数据) - taxBreakdown 增加 specialDeductionSource 字段标识数据来源 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 社保公积金账户化迁移脚本
|
||||
* 将旧 SocialInsuranceConfig / HousingFundConfig 迁移到 SocialAccount + SocialYearStandard
|
||||
* 将员工参保记录的 city 关联到 accountId
|
||||
*
|
||||
* 用法: npx tsx scripts/migrate-social-accounts.ts
|
||||
*/
|
||||
import prisma from '../src/lib/prisma'
|
||||
|
||||
async function main() {
|
||||
console.log('========== 社保公积金账户化迁移 ==========')
|
||||
|
||||
// ========== 1. 迁移社保配置 → 社保账户 + 年度标准 ==========
|
||||
console.log('[1/6] 迁移社保配置...')
|
||||
|
||||
// 按 (orgId, city) 分组创建社保账户
|
||||
const socialConfigs = await prisma.socialInsuranceConfig.findMany()
|
||||
const socialAccountMap = new Map<string, string>() // key: orgId:city → accountId
|
||||
|
||||
for (const config of socialConfigs) {
|
||||
const key = `${config.orgId}:${config.city}`
|
||||
if (socialAccountMap.has(key)) continue
|
||||
|
||||
const account = await prisma.socialAccount.create({
|
||||
data: {
|
||||
orgId: config.orgId,
|
||||
type: 'SOCIAL',
|
||||
name: `${config.city}社保账户`,
|
||||
city: config.city,
|
||||
isDefault: true,
|
||||
status: 'ACTIVE',
|
||||
createdBy: 'migration',
|
||||
},
|
||||
})
|
||||
socialAccountMap.set(key, account.id)
|
||||
console.log(` 创建社保账户: ${config.city} → ${account.id}`)
|
||||
}
|
||||
|
||||
// 迁移每条 Config → YearStandard
|
||||
for (const config of socialConfigs) {
|
||||
const key = `${config.orgId}:${config.city}`
|
||||
const accountId = socialAccountMap.get(key)!
|
||||
await prisma.socialYearStandard.create({
|
||||
data: {
|
||||
orgId: config.orgId,
|
||||
accountId,
|
||||
pensionOrg: config.pensionOrg,
|
||||
pensionEmp: config.pensionEmp,
|
||||
medicalOrg: config.medicalOrg,
|
||||
medicalEmp: config.medicalEmp,
|
||||
unemploymentOrg: config.unemploymentOrg,
|
||||
unemploymentEmp: config.unemploymentEmp,
|
||||
injuryOrg: config.injuryOrg,
|
||||
maternityOrg: config.maternityOrg,
|
||||
baseMin: config.baseMin,
|
||||
baseMax: config.baseMax,
|
||||
medicalBaseMin: config.medicalBaseMin,
|
||||
medicalBaseMax: config.medicalBaseMax,
|
||||
extraInsurances: config.extraInsurances as any,
|
||||
effectiveFrom: config.effectiveFrom,
|
||||
effectiveTo: config.effectiveTo,
|
||||
isCurrent: config.isCurrent,
|
||||
adjustmentDone: config.adjustmentDone,
|
||||
createdBy: config.createdBy,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` 迁移 ${socialConfigs.length} 条社保年度标准`)
|
||||
|
||||
// ========== 2. 迁移公积金配置 → 公积金账户 + 年度标准 ==========
|
||||
console.log('[2/6] 迁移公积金配置...')
|
||||
|
||||
const housingConfigs = await prisma.housingFundConfig.findMany()
|
||||
const housingAccountMap = new Map<string, string>() // key: orgId:city:accountType → accountId
|
||||
|
||||
for (const config of housingConfigs) {
|
||||
const accountType = config.accountType || 'BASIC'
|
||||
const key = `${config.orgId}:${config.city}:${accountType}`
|
||||
if (housingAccountMap.has(key)) continue
|
||||
|
||||
const account = await prisma.socialAccount.create({
|
||||
data: {
|
||||
orgId: config.orgId,
|
||||
type: 'HOUSING',
|
||||
name: `${config.city}${accountType === 'SUPPLEMENTARY' ? '补充' : ''}公积金账户`,
|
||||
city: config.city,
|
||||
accountType,
|
||||
isDefault: accountType === 'BASIC',
|
||||
status: 'ACTIVE',
|
||||
createdBy: 'migration',
|
||||
},
|
||||
})
|
||||
housingAccountMap.set(key, account.id)
|
||||
console.log(` 创建公积金账户: ${config.city} ${accountType} → ${account.id}`)
|
||||
}
|
||||
|
||||
for (const config of housingConfigs) {
|
||||
const accountType = config.accountType || 'BASIC'
|
||||
const key = `${config.orgId}:${config.city}:${accountType}`
|
||||
const accountId = housingAccountMap.get(key)!
|
||||
await prisma.socialYearStandard.create({
|
||||
data: {
|
||||
orgId: config.orgId,
|
||||
accountId,
|
||||
housingOrg: config.housingOrg,
|
||||
housingEmp: config.housingEmp,
|
||||
baseMin: config.baseMin,
|
||||
baseMax: config.baseMax,
|
||||
effectiveFrom: config.effectiveFrom,
|
||||
effectiveTo: config.effectiveTo,
|
||||
isCurrent: config.isCurrent,
|
||||
adjustmentDone: config.adjustmentDone,
|
||||
createdBy: config.createdBy,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` 迁移 ${housingConfigs.length} 条公积金年度标准`)
|
||||
|
||||
// ========== 3. 迁移员工社保参保记录 accountId ==========
|
||||
console.log('[3/6] 迁移员工社保参保记录 accountId...')
|
||||
|
||||
const socialRecords = await prisma.employeeSocialInsRecord.findMany({ where: { accountId: null } })
|
||||
let socialUpdated = 0
|
||||
for (const record of socialRecords) {
|
||||
const key = `${record.orgId}:${record.city}`
|
||||
const accountId = socialAccountMap.get(key)
|
||||
if (accountId) {
|
||||
await prisma.employeeSocialInsRecord.update({ where: { id: record.id }, data: { accountId } })
|
||||
socialUpdated++
|
||||
}
|
||||
}
|
||||
console.log(` 更新 ${socialUpdated}/${socialRecords.length} 条社保参保记录`)
|
||||
|
||||
// ========== 4. 迁移员工公积金参保记录 accountId ==========
|
||||
console.log('[4/6] 迁移员工公积金参保记录 accountId...')
|
||||
|
||||
const housingRecords = await prisma.employeeHousingFundRecord.findMany({ where: { accountId: null } })
|
||||
let housingUpdated = 0
|
||||
for (const record of housingRecords) {
|
||||
// 公积金默认用 BASIC 类型账户
|
||||
const key = `${record.orgId}:${record.city}:BASIC`
|
||||
const accountId = housingAccountMap.get(key)
|
||||
if (accountId) {
|
||||
await prisma.employeeHousingFundRecord.update({ where: { id: record.id }, data: { accountId } })
|
||||
housingUpdated++
|
||||
}
|
||||
}
|
||||
console.log(` 更新 ${housingUpdated}/${housingRecords.length} 条公积金参保记录`)
|
||||
|
||||
// ========== 5. 迁移月度办理记录 accountId ==========
|
||||
console.log('[5/6] 迁移月度办理记录 accountId...')
|
||||
|
||||
const monthlyProcesses = await prisma.socialMonthlyProcess.findMany({ where: { accountId: null } })
|
||||
let monthlyUpdated = 0
|
||||
for (const proc of monthlyProcesses) {
|
||||
const snapshot = proc.snapshot as any
|
||||
const city = snapshot?.city || snapshot?.configCity
|
||||
if (city) {
|
||||
const accountMap = proc.type === 'SOCIAL' ? socialAccountMap : housingAccountMap
|
||||
// 社保用 orgId:city,公积金用 orgId:city:BASIC
|
||||
const key = proc.type === 'SOCIAL' ? `${proc.orgId}:${city}` : `${proc.orgId}:${city}:BASIC`
|
||||
const accountId = accountMap.get(key)
|
||||
if (accountId) {
|
||||
await prisma.socialMonthlyProcess.update({ where: { id: proc.id }, data: { accountId } })
|
||||
monthlyUpdated++
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` 更新 ${monthlyUpdated}/${monthlyProcesses.length} 条月度办理记录`)
|
||||
|
||||
// ========== 6. 验证数据完整性 ==========
|
||||
console.log('[6/6] 验证数据完整性...')
|
||||
|
||||
const accounts = await prisma.socialAccount.count()
|
||||
const standards = await prisma.socialYearStandard.count()
|
||||
const socialWithoutAccount = await prisma.employeeSocialInsRecord.count({ where: { accountId: null } })
|
||||
const housingWithoutAccount = await prisma.employeeHousingFundRecord.count({ where: { accountId: null } })
|
||||
|
||||
console.log(` 账户总数: ${accounts}`)
|
||||
console.log(` 年度标准总数: ${standards}`)
|
||||
console.log(` 社保参保记录无 accountId: ${socialWithoutAccount}`)
|
||||
console.log(` 公积金参保记录无 accountId: ${housingWithoutAccount}`)
|
||||
|
||||
if (socialWithoutAccount > 0 || housingWithoutAccount > 0) {
|
||||
console.log(' ⚠️ 部分参保记录未关联账户(可能城市无对应配置),需手动处理')
|
||||
}
|
||||
|
||||
console.log('')
|
||||
console.log('========== 迁移完成 ==========')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('迁移失败:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user