4f125d309b
- Schema: 拆分社保/公积金配置,新增EmployeeSocialInsRecord/EmployeeHousingFundRecord/DepartmentRecord模型,扩展SalaryChangeRecord,增加SEVERANCE批次类型 - 后端: createEmployee/rehireEmployee接收社保公积金字段并创建缴费记录版本;createTermination/createResignation接收截止年月并关闭缴费记录;调薪/调部门API+版本记录;月度增减员API;公积金独立CRUD/计算/调基;SEVERANCE批次calcBatchEntry - 前端: AddEmployeeModal/RehireModal增加社保公积金输入;ResignModal/Termination增加截止年月+日期不一致提醒;花名册增加调薪/调部门弹窗;SocialInsurance.tsx Tab拆分(社保/公积金/月度增减员)+CSV导出;Money.tsx增加补偿金批次类型 - 修复: seed.ts移除housingOrg/housingEmp;risk.service.ts从HousingFundConfig获取公积金费率
284 lines
14 KiB
TypeScript
284 lines
14 KiB
TypeScript
import { PrismaClient } from '@prisma/client'
|
|
import bcrypt from 'bcryptjs'
|
|
import { encrypt } from '../src/lib/crypto'
|
|
|
|
const prisma = new PrismaClient()
|
|
|
|
// 社保计算(与 payroll.service.ts 一致)
|
|
function calcSocial(base: number, config: any) {
|
|
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
|
|
const socialEmp = actualBase * (config.pensionEmp + config.medicalEmp + config.unemploymentEmp) / 100
|
|
const socialOrg = actualBase * (config.pensionOrg + config.medicalOrg + config.unemploymentOrg + config.injuryOrg + config.maternityOrg) / 100
|
|
return { socialEmp: Math.round(socialEmp * 100) / 100, socialOrg: Math.round(socialOrg * 100) / 100 }
|
|
}
|
|
function calcHousing(base: number, config: any) {
|
|
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
|
|
const housingEmp = actualBase * config.housingEmp / 100
|
|
const housingOrg = actualBase * config.housingOrg / 100
|
|
return { housingEmp: Math.round(housingEmp * 100) / 100, housingOrg: Math.round(housingOrg * 100) / 100 }
|
|
}
|
|
function calcTax(taxableIncome: number): number {
|
|
if (taxableIncome <= 0) return 0
|
|
let tax = 0
|
|
if (taxableIncome <= 36000) tax = taxableIncome * 0.03
|
|
else if (taxableIncome <= 144000) tax = taxableIncome * 0.10 - 2520
|
|
else if (taxableIncome <= 300000) tax = taxableIncome * 0.20 - 16920
|
|
else if (taxableIncome <= 420000) tax = taxableIncome * 0.25 - 31920
|
|
else if (taxableIncome <= 660000) tax = taxableIncome * 0.30 - 52920
|
|
else if (taxableIncome <= 960000) tax = taxableIncome * 0.35 - 85920
|
|
else tax = taxableIncome * 0.45 - 181920
|
|
return Math.max(0, Math.round(tax * 100) / 100)
|
|
}
|
|
|
|
// 9名员工完整数据
|
|
const EMPLOYEES = [
|
|
{ name: '张伟', gender: '男', dept: '技术部', phone: '13900000001', idCard: '310101199001011234', salary: 18000, hireDate: '2023-03-01', socialBase: 18000, housingBase: 18000, specialDeduction: 2000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 14400, bank: '工商银行', account: '6222021234567890001', emergency: '张父', emergencyPhone: '13800001001', address: '上海市浦东新区张江路100号' },
|
|
{ name: '李娜', gender: '女', dept: '技术部', phone: '13900000002', idCard: '310102199203052345', salary: 15000, hireDate: '2023-06-15', socialBase: 15000, housingBase: 15000, specialDeduction: 1000, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 12000, bank: '建设银行', account: '6227001234567890002', emergency: '李母', emergencyPhone: '13800001002', address: '上海市徐汇区漕河泾50号', pregnant: true },
|
|
{ name: '王强', gender: '男', dept: '销售部', phone: '13900000003', idCard: '310103198812103456', salary: 12000, hireDate: '2024-01-10', socialBase: 12000, housingBase: 12000, specialDeduction: 3000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 9600, bank: '招商银行', account: '6225881234567890003', emergency: '王妻', emergencyPhone: '13800001003', address: '上海市闵行区莘庄路200号' },
|
|
{ name: '赵敏', gender: '女', dept: '人事部', phone: '13900000004', idCard: '310104199506154567', salary: 10000, hireDate: '2022-09-01', socialBase: 10000, housingBase: 10000, specialDeduction: 1500, contractType: 'UNFIXED', years: 0, probation: 0, probationSalary: 0, bank: '农业银行', account: '6228481234567890004', emergency: '赵父', emergencyPhone: '13800001004', address: '上海市黄浦区南京东路300号' },
|
|
{ name: '陈刚', gender: '男', dept: '销售部', phone: '13900000005', idCard: '310105199907205678', salary: 8000, hireDate: '2024-07-01', socialBase: 8000, housingBase: 8000, specialDeduction: 0, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 6400, bank: '中国银行', account: '6217001234567890005', emergency: '陈母', emergencyPhone: '13800001005', address: '上海市杨浦区五角场400号' },
|
|
{ name: '刘洋', gender: '男', dept: '技术部', phone: '13900000006', idCard: '310106198504016789', salary: 22000, hireDate: '2021-04-01', socialBase: 33891, housingBase: 33891, specialDeduction: 4000, contractType: 'UNFIXED', years: 0, probation: 0, probationSalary: 0, bank: '交通银行', account: '6222601234567890006', emergency: '刘妻', emergencyPhone: '13800001006', address: '上海市长宁区中山公园500号' },
|
|
{ name: '周婷', gender: '女', dept: '财务部', phone: '13900000007', idCard: '310107199311157890', salary: 13000, hireDate: '2023-11-15', socialBase: 13000, housingBase: 13000, specialDeduction: 2500, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 10400, bank: '浦发银行', account: '6225161234567890007', emergency: '周父', emergencyPhone: '13800001007', address: '上海市静安区南京西路600号' },
|
|
{ name: '孙磊', gender: '男', dept: '技术部', phone: '13900000008', idCard: '310108199008018901', salary: 16000, hireDate: '2022-06-01', socialBase: 16000, housingBase: 16000, specialDeduction: 1000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 12800, bank: '民生银行', account: '6226161234567890008', emergency: '孙母', emergencyPhone: '13800001008', address: '上海市虹口区四川北路700号' },
|
|
{ name: '吴芳', gender: '女', dept: '销售部', phone: '13900000009', idCard: '310109199702159012', salary: 9000, hireDate: '2025-02-15', socialBase: 9000, housingBase: 9000, specialDeduction: 500, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 7200, bank: '光大银行', account: '6226621234567890009', emergency: '吴夫', emergencyPhone: '13800001009', address: '上海市宝山区牡丹江路800号' },
|
|
]
|
|
|
|
async function main() {
|
|
// 1. 清空所有数据(按依赖顺序删除)
|
|
console.log('清空现有数据...')
|
|
await prisma.notificationLog.deleteMany()
|
|
await prisma.auditLog.deleteMany()
|
|
await prisma.batchEntry.deleteMany()
|
|
await prisma.payrollBatch.deleteMany()
|
|
await prisma.payslipItem.deleteMany()
|
|
await prisma.salaryChangeRecord.deleteMany()
|
|
await prisma.payslip.deleteMany()
|
|
await prisma.overtimeRecord.deleteMany()
|
|
await prisma.terminationRecord.deleteMany()
|
|
await prisma.riskItem.deleteMany()
|
|
await prisma.employeeAttachment.deleteMany()
|
|
await prisma.disciplinaryRecord.deleteMany()
|
|
await prisma.attendanceRecord.deleteMany()
|
|
await prisma.trainingRecord.deleteMany()
|
|
await prisma.performanceRecord.deleteMany()
|
|
await prisma.laborContract.deleteMany()
|
|
await prisma.contractConfirmLink.deleteMany()
|
|
await prisma.onboardingLink.deleteMany()
|
|
await prisma.employee.deleteMany()
|
|
await prisma.socialInsuranceConfig.deleteMany()
|
|
await prisma.notificationSetting.deleteMany()
|
|
await prisma.user.deleteMany()
|
|
await prisma.organization.deleteMany()
|
|
console.log('数据已清空')
|
|
|
|
// 2. 创建企业
|
|
const org = await prisma.organization.create({
|
|
data: {
|
|
name: '智云科技有限公司',
|
|
plan: 'PRO',
|
|
maxEmployees: 50,
|
|
city: '上海',
|
|
payrollFrequency: 1,
|
|
},
|
|
})
|
|
console.log('企业已创建:', org.name)
|
|
|
|
// 3. 创建管理员
|
|
const passwordHash = await bcrypt.hash('12345678', 10)
|
|
const admin = await prisma.user.create({
|
|
data: {
|
|
orgId: org.id,
|
|
phone: '13800000001',
|
|
name: '管理员',
|
|
passwordHash,
|
|
role: 'ADMIN',
|
|
},
|
|
})
|
|
console.log('管理员已创建:', admin.phone)
|
|
|
|
// 4. 创建社保配置(上海标准)
|
|
await prisma.socialInsuranceConfig.create({
|
|
data: {
|
|
orgId: org.id,
|
|
city: '上海',
|
|
pensionOrg: 16,
|
|
pensionEmp: 8,
|
|
medicalOrg: 9.8,
|
|
medicalEmp: 2,
|
|
unemploymentOrg: 0.5,
|
|
unemploymentEmp: 0.5,
|
|
injuryOrg: 0.2,
|
|
maternityOrg: 0.8,
|
|
baseMin: 7384,
|
|
baseMax: 36921,
|
|
effectiveFrom: '2025-07',
|
|
createdBy: admin.id,
|
|
},
|
|
})
|
|
console.log('社保配置已创建')
|
|
|
|
// 5. 创建通知设置
|
|
await prisma.notificationSetting.create({
|
|
data: {
|
|
orgId: org.id,
|
|
contractExpiry: true,
|
|
expiryDays: 30,
|
|
contractUnsigned: true,
|
|
overtimeAlert: true,
|
|
payslipReady: true,
|
|
payrollDay: 10,
|
|
socialInsDay: 15,
|
|
housingFundDay: 15,
|
|
taxDay: 15,
|
|
},
|
|
})
|
|
|
|
// 6. 创建薪酬模版(预置项)
|
|
const defaultItems: { name: string; code: string; type: 'INPUT' | 'CALCULATED'; formula: string | null; order: number; isDefault: boolean; isEditable: boolean }[] = [
|
|
{ name: '基本工资', code: 'baseSalary', type: 'INPUT', formula: null, order: 1, isDefault: true, isEditable: true },
|
|
{ name: '加班费', code: 'overtimePay', type: 'CALCULATED', formula: 'weekdayOvertimePay + weekendOvertimePay + holidayOvertimePay', order: 2, isDefault: true, isEditable: false },
|
|
{ name: '津贴补贴', code: 'allowance', type: 'INPUT', formula: null, order: 3, isDefault: true, isEditable: true },
|
|
{ name: '奖金', code: 'bonus', type: 'INPUT', formula: null, order: 4, isDefault: true, isEditable: true },
|
|
{ name: '扣款', code: 'deduction', type: 'INPUT', formula: null, order: 5, isDefault: true, isEditable: true },
|
|
{ name: '应发合计', code: 'totalPay', type: 'CALCULATED', formula: 'baseSalary + overtimePay + allowance + bonus - deduction', order: 6, isDefault: true, isEditable: false },
|
|
{ name: '个人社保', code: 'socialEmp', type: 'CALCULATED', formula: 'SOCIAL_EMP', order: 7, isDefault: true, isEditable: false },
|
|
{ name: '个人公积金', code: 'housingEmp', type: 'CALCULATED', formula: 'HOUSING_EMP', order: 8, isDefault: true, isEditable: false },
|
|
{ name: '个人所得税', code: 'tax', type: 'CALCULATED', formula: 'TAX', order: 9, isDefault: true, isEditable: false },
|
|
{ name: '实发工资', code: 'netPay', type: 'CALCULATED', formula: 'totalPay - socialEmp - housingEmp - tax', order: 10, isDefault: true, isEditable: false },
|
|
]
|
|
for (const item of defaultItems) {
|
|
await prisma.payslipItem.create({
|
|
data: { orgId: org.id, ...item },
|
|
})
|
|
}
|
|
console.log('薪酬模版已创建')
|
|
|
|
// 7. 创建9名员工 + 合同
|
|
for (let i = 0; i < EMPLOYEES.length; i++) {
|
|
const e = EMPLOYEES[i]
|
|
const emp = await prisma.employee.create({
|
|
data: {
|
|
orgId: org.id,
|
|
name: e.name,
|
|
department: e.dept,
|
|
hireDate: new Date(e.hireDate),
|
|
monthlySalary: encrypt(String(e.salary)),
|
|
phone: e.phone,
|
|
idCardNumber: encrypt(e.idCard),
|
|
gender: e.gender,
|
|
socialInsBase: e.socialBase,
|
|
housingFundBase: e.housingBase,
|
|
specialDeduction: e.specialDeduction,
|
|
bankName: e.bank,
|
|
bankAccount: encrypt(e.account),
|
|
emergencyContact: e.emergency,
|
|
emergencyPhone: e.emergencyPhone,
|
|
address: e.address,
|
|
isPregnant: e.pregnant || false,
|
|
createdBy: admin.id,
|
|
},
|
|
})
|
|
|
|
// 创建合同
|
|
const startDate = new Date(e.hireDate)
|
|
const endDate = e.contractType === 'FIXED'
|
|
? new Date(startDate.getFullYear() + e.years, startDate.getMonth(), startDate.getDate() - 1)
|
|
: null
|
|
|
|
await prisma.laborContract.create({
|
|
data: {
|
|
orgId: org.id,
|
|
employeeId: emp.id,
|
|
signDate: new Date(e.hireDate),
|
|
startDate,
|
|
endDate,
|
|
contractType: e.contractType as any,
|
|
signMethod: 'PAPER',
|
|
contractYears: e.years,
|
|
probationMonths: e.probation,
|
|
probationSalary: e.probationSalary,
|
|
createdBy: admin.id,
|
|
},
|
|
})
|
|
console.log(`员工 ${i + 1}/9 已创建: ${e.name} - ${e.dept} - ¥${e.salary}/月`)
|
|
}
|
|
|
|
// 8. 生成 1-6 月历史工资条(已发布),使 7 月累计预扣个税有 YTD 数据
|
|
console.log('\n生成 1-6 月历史工资条...')
|
|
const socialConfig = await prisma.socialInsuranceConfig.findFirst({ where: { orgId: org.id, isCurrent: true } })
|
|
const allEmployees = await prisma.employee.findMany({ where: { orgId: org.id } })
|
|
const months = ['2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06']
|
|
|
|
for (const emp of allEmployees) {
|
|
// 跳过 2026 年之后入职的员工
|
|
const hireYear = emp.hireDate.getFullYear()
|
|
if (hireYear > 2026) continue
|
|
const hireMonth = hireYear === 2026 ? emp.hireDate.getMonth() + 1 : 1
|
|
|
|
let ytdIncome = 0, ytdSocialEmp = 0, ytdHousingEmp = 0, ytdTaxDeducted = 0
|
|
|
|
for (let m = 1; m <= 6; m++) {
|
|
if (m < hireMonth) continue
|
|
const monthStr = `2026-${String(m).padStart(2, '0')}`
|
|
const baseSalary = emp.socialInsBase || 0 // 用社保基数作为基本工资(简化)
|
|
const social = calcSocial(emp.socialInsBase || baseSalary, socialConfig)
|
|
const housing = calcHousing(emp.housingFundBase || baseSalary, socialConfig)
|
|
const totalPay = baseSalary
|
|
const specialDeduction = emp.specialDeduction * m
|
|
|
|
ytdIncome += totalPay
|
|
ytdSocialEmp += social.socialEmp
|
|
ytdHousingEmp += housing.housingEmp
|
|
|
|
const ytdTaxableIncome = Math.max(0, ytdIncome - 5000 * m - ytdSocialEmp - ytdHousingEmp - specialDeduction)
|
|
const ytdTax = calcTax(ytdTaxableIncome)
|
|
const monthTax = Math.max(0, Math.round((ytdTax - ytdTaxDeducted) * 100) / 100)
|
|
ytdTaxDeducted += monthTax
|
|
|
|
const netPay = Math.round((totalPay - social.socialEmp - housing.housingEmp - monthTax) * 100) / 100
|
|
|
|
await prisma.payslip.create({
|
|
data: {
|
|
orgId: org.id,
|
|
employeeId: emp.id,
|
|
month: monthStr,
|
|
baseSalary,
|
|
overtimePay: 0,
|
|
allowance: 0,
|
|
deduction: 0,
|
|
bonus: 0,
|
|
totalPay,
|
|
socialEmp: social.socialEmp,
|
|
housingEmp: housing.housingEmp,
|
|
tax: monthTax,
|
|
netPay,
|
|
ytdIncome,
|
|
ytdTaxDeducted,
|
|
ytdSocialEmp,
|
|
ytdHousingEmp,
|
|
status: 'PUBLISHED',
|
|
publishedAt: new Date(`${monthStr}-10T10:00:00Z`),
|
|
confirmedAt: new Date(`${monthStr}-12T10:00:00Z`),
|
|
},
|
|
})
|
|
}
|
|
console.log(` ${emp.name}: 1-6月工资条已生成`)
|
|
}
|
|
|
|
console.log('\n===== 示例数据创建完成 =====')
|
|
console.log(`企业: ${org.name}`)
|
|
console.log(`管理员: 13800000001 / 密码: 12345678`)
|
|
console.log(`员工: ${EMPLOYEES.length} 人`)
|
|
console.log('社保配置: 上海标准')
|
|
console.log('薪酬模版: 10项预置')
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error(e)
|
|
process.exit(1)
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect()
|
|
})
|