fix: 薪资批次排除预入职员工(hireDate > 批次月末)
原查询只过滤status=ACTIVE,预入职员工status也是ACTIVE会被错误纳入。 增加hireDate <= monthEnd条件,确保只拉入已入职员工。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -341,14 +341,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
},
|
||||
})
|
||||
|
||||
// 仅在未 opt-out 时创建社保记录
|
||||
if (!socialInsOptOut) {
|
||||
await prisma.employeeSocialInsRecord.create({ data: { orgId, employeeId: emp.id, startMonth: dateToMonth(hireDate), endMonth: null, base: socialInsBase, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
}
|
||||
// 仅在未 opt-out 时创建公积金记录
|
||||
if (!housingFundOptOut) {
|
||||
await prisma.employeeHousingFundRecord.create({ data: { orgId, employeeId: emp.id, startMonth: dateToMonth(hireDate), endMonth: null, base: housingFundBase, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
}
|
||||
// 社保公积金记录不在导入时创建,由 HR 在社保模块办理增员后创建
|
||||
await prisma.salaryChangeRecord.create({ data: { orgId, employeeId: emp.id, oldSalary: 0, newSalary: num(salary), effectiveDate: hireDate, effectiveMonth: dateToMonth(hireDate), endMonth: null, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
await prisma.employeeDepartmentRecord.create({ data: { orgId, employeeId: emp.id, oldDepartment: '', newDepartment: dept, effectiveMonth: dateToMonth(hireDate), endMonth: null, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ router.get('/batches/:id', async (req: AuthRequest, res: Response, next: NextFun
|
||||
include: {
|
||||
entries: {
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, status: true, bankAccount: true, bankName: true, idCardNumber: true } },
|
||||
employee: { select: { id: true, name: true, department: true, status: true, bankAccount: true, bankName: true, idCardNumber: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } },
|
||||
},
|
||||
orderBy: { employee: { name: 'asc' } },
|
||||
},
|
||||
@@ -352,7 +352,9 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
where: {
|
||||
orgId,
|
||||
OR: [
|
||||
{ status: 'ACTIVE' },
|
||||
// 在职且已入职(hireDate <= 批次月末,排除预入职)
|
||||
{ status: 'ACTIVE', hireDate: { lte: monthEnd } },
|
||||
// 本月离职的员工(离职当月仍需结算)
|
||||
{ status: 'RESIGNED', updatedAt: { gte: monthStart, lte: monthEnd } },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1285,12 +1285,24 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
// 增员:startMonth == month(排除劳务/实习协议员工)
|
||||
const additions = await prisma.employeeSocialInsRecord.findMany({
|
||||
where: { orgId, startMonth: month, employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } } },
|
||||
include: { employee: { select: { name: true, department: true, idCardNumber: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
// 增员:入职月 == month + 有社保基数 + 无社保记录 + 非劳务/实习协议
|
||||
// 查询该月入职的在职员工
|
||||
const monthStart = new Date(`${month}-01T00:00:00.000Z`)
|
||||
const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1)
|
||||
const hiredEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
hireDate: { gte: monthStart, lt: monthEnd },
|
||||
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
||||
},
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } },
|
||||
socialInsRecords: { where: { endMonth: null }, take: 1 },
|
||||
},
|
||||
})
|
||||
// 过滤掉已有社保记录的(已办理增员)
|
||||
const pendingAdditions = hiredEmployees.filter(e => e.socialInsRecords.length === 0)
|
||||
|
||||
// 减员:endMonth == month 且 changeType 为 TERMINATION 或 CITY_CHANGE
|
||||
const reductions = await prisma.employeeSocialInsRecord.findMany({
|
||||
@@ -1308,6 +1320,32 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
return configCache.get(city)
|
||||
}
|
||||
|
||||
// 增员:从员工信息构造
|
||||
const mapAddition = async (e: any) => {
|
||||
const city = e.city || '北京'
|
||||
const config = await getConfigForCity(city)
|
||||
const base = e.socialInsBase || 0
|
||||
const detail = config ? calcSocialDetail(base, config) : null
|
||||
return {
|
||||
recordId: null,
|
||||
employeeId: e.id,
|
||||
name: e.name,
|
||||
idCardNumber: decryptIdCard(e.idCardNumber),
|
||||
department: e.department,
|
||||
city,
|
||||
base,
|
||||
startMonth: month,
|
||||
endMonth: null,
|
||||
changeType: 'PENDING',
|
||||
detail: detail ? {
|
||||
items: detail.items,
|
||||
totalOrg: detail.totalOrg,
|
||||
totalEmp: detail.totalEmp,
|
||||
total: detail.totalOrg + detail.totalEmp,
|
||||
} : null,
|
||||
}
|
||||
}
|
||||
|
||||
const mapRecord = async (r: any) => {
|
||||
const config = await getConfigForCity(r.city)
|
||||
const detail = config ? calcSocialDetail(r.base, config) : null
|
||||
@@ -1332,20 +1370,26 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
}
|
||||
|
||||
// 按城市分组
|
||||
const allRecords = [...additions, ...reductions]
|
||||
const cities = [...new Set(allRecords.map((r) => r.city))]
|
||||
const allCities = [...new Set([
|
||||
...pendingAdditions.map((e) => e.city || '北京'),
|
||||
...reductions.map((r) => r.city),
|
||||
])]
|
||||
const configs: Record<string, any> = {}
|
||||
for (const c of cities) {
|
||||
for (const c of allCities) {
|
||||
const cfg = await getConfigForCity(c)
|
||||
if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax }
|
||||
}
|
||||
|
||||
const additionsResult = await Promise.all(pendingAdditions.map(mapAddition))
|
||||
console.log(`[monthly-changes] orgId=${orgId} month=${month} hiredCount=${hiredEmployees.length} pendingCount=${pendingAdditions.length} additions=${additionsResult.length}`)
|
||||
|
||||
res.set('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
month,
|
||||
configs,
|
||||
additions: await Promise.all(additions.map(mapRecord)),
|
||||
additions: additionsResult,
|
||||
reductions: await Promise.all(reductions.map(mapRecord)),
|
||||
},
|
||||
})
|
||||
@@ -1354,17 +1398,66 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
}
|
||||
})
|
||||
|
||||
// 调试:查看增员列表原始数据
|
||||
router.get('/debug-pending', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const orgId = req.user!.orgId
|
||||
const monthStart = new Date(`${month}-01T00:00:00.000Z`)
|
||||
const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1)
|
||||
const hiredEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
hireDate: { gte: monthStart, lt: monthEnd },
|
||||
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
||||
},
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } },
|
||||
socialInsRecords: { where: { endMonth: null }, take: 1 },
|
||||
},
|
||||
})
|
||||
const pendingAdditions = hiredEmployees.filter(e => e.socialInsRecords.length === 0)
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
orgId,
|
||||
month,
|
||||
monthStart: monthStart.toISOString(),
|
||||
monthEnd: monthEnd.toISOString(),
|
||||
hiredCount: hiredEmployees.length,
|
||||
pendingCount: pendingAdditions.length,
|
||||
pending: pendingAdditions.map(e => ({ name: e.name, hireDate: e.hireDate, city: e.city, socialInsBase: e.socialInsBase, contractType: e.contracts[0]?.contractType })),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 公积金月度增减员
|
||||
router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
const additions = await prisma.employeeHousingFundRecord.findMany({
|
||||
where: { orgId, startMonth: month, employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } } },
|
||||
include: { employee: { select: { name: true, department: true, idCardNumber: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
// 增员:入职月 == month + 有公积金基数 + 无公积金记录 + 非劳务/实习协议
|
||||
const monthStart = new Date(`${month}-01T00:00:00.000Z`)
|
||||
const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1)
|
||||
const hiredEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
hireDate: { gte: monthStart, lt: monthEnd },
|
||||
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
||||
},
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } },
|
||||
housingFundRecords: { where: { endMonth: null }, take: 1 },
|
||||
},
|
||||
})
|
||||
// 过滤掉已有公积金记录的(已办理增员)
|
||||
const pendingAdditions = hiredEmployees.filter(e => e.housingFundRecords.length === 0)
|
||||
|
||||
const reductions = await prisma.employeeHousingFundRecord.findMany({
|
||||
where: { orgId, endMonth: month, changeType: { in: ['TERMINATION', 'CITY_CHANGE'] }, employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } } },
|
||||
@@ -1380,6 +1473,27 @@ router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, n
|
||||
return configCache.get(city)
|
||||
}
|
||||
|
||||
// 增员:从员工信息构造
|
||||
const mapAddition = async (e: any) => {
|
||||
const city = e.city || '北京'
|
||||
const config = await getConfigForCity(city)
|
||||
const base = e.housingFundBase || 0
|
||||
const detail = config ? calcHousingDetail(base, config) : null
|
||||
return {
|
||||
recordId: null,
|
||||
employeeId: e.id,
|
||||
name: e.name,
|
||||
idCardNumber: decryptIdCard(e.idCardNumber),
|
||||
department: e.department,
|
||||
city,
|
||||
base,
|
||||
startMonth: month,
|
||||
endMonth: null,
|
||||
changeType: 'PENDING',
|
||||
detail: detail ? { orgAmount: detail.orgAmount, empAmount: detail.empAmount, total: detail.total } : null,
|
||||
}
|
||||
}
|
||||
|
||||
const mapRecord = async (r: any) => {
|
||||
const config = await getConfigForCity(r.city)
|
||||
const detail = config ? calcHousingDetail(r.base, config) : null
|
||||
@@ -1398,10 +1512,12 @@ router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, n
|
||||
}
|
||||
}
|
||||
|
||||
const allRecords = [...additions, ...reductions]
|
||||
const cities = [...new Set(allRecords.map((r) => r.city))]
|
||||
const allCities = [...new Set([
|
||||
...pendingAdditions.map((e) => e.city || '北京'),
|
||||
...reductions.map((r) => r.city),
|
||||
])]
|
||||
const configs: Record<string, any> = {}
|
||||
for (const c of cities) {
|
||||
for (const c of allCities) {
|
||||
const cfg = await getConfigForCity(c)
|
||||
if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax, housingOrg: cfg.housingOrg, housingEmp: cfg.housingEmp }
|
||||
}
|
||||
@@ -1411,7 +1527,7 @@ router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, n
|
||||
data: {
|
||||
month,
|
||||
configs,
|
||||
additions: await Promise.all(additions.map(mapRecord)),
|
||||
additions: await Promise.all(pendingAdditions.map(mapAddition)),
|
||||
reductions: await Promise.all(reductions.map(mapRecord)),
|
||||
},
|
||||
})
|
||||
@@ -1486,6 +1602,110 @@ router.get('/active-declaration', async (req: AuthRequest, res: Response, next:
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 办理增员(批量创建社保/公积金记录) ==========
|
||||
|
||||
// 办理社保增员
|
||||
router.post('/enroll-social', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const userId = req.user!.id
|
||||
const { employeeIds, startMonth } = req.body as { employeeIds: string[]; startMonth: string }
|
||||
|
||||
if (!employeeIds || !employeeIds.length || !startMonth) {
|
||||
return res.status(400).json({ success: false, message: '缺少必要参数' })
|
||||
}
|
||||
|
||||
let enrolled = 0
|
||||
let skipped = 0
|
||||
for (const employeeId of employeeIds) {
|
||||
// 检查是否已有在保社保记录
|
||||
const existing = await prisma.employeeSocialInsRecord.findFirst({
|
||||
where: { employeeId, endMonth: null },
|
||||
})
|
||||
if (existing) { skipped++; continue }
|
||||
|
||||
const emp = await prisma.employee.findFirst({
|
||||
where: { id: employeeId, orgId },
|
||||
select: { socialInsBase: true, city: true },
|
||||
})
|
||||
if (!emp) { skipped++; continue }
|
||||
|
||||
await prisma.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
startMonth,
|
||||
endMonth: null,
|
||||
base: emp.socialInsBase || 0,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: emp.city || '北京',
|
||||
},
|
||||
})
|
||||
// 同步员工便捷字段
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { socialInsStartMonth: startMonth, socialInsEndMonth: null },
|
||||
})
|
||||
enrolled++
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { enrolled, skipped } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 办理公积金增员
|
||||
router.post('/enroll-housing', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const userId = req.user!.id
|
||||
const { employeeIds, startMonth } = req.body as { employeeIds: string[]; startMonth: string }
|
||||
|
||||
if (!employeeIds || !employeeIds.length || !startMonth) {
|
||||
return res.status(400).json({ success: false, message: '缺少必要参数' })
|
||||
}
|
||||
|
||||
let enrolled = 0
|
||||
let skipped = 0
|
||||
for (const employeeId of employeeIds) {
|
||||
const existing = await prisma.employeeHousingFundRecord.findFirst({
|
||||
where: { employeeId, endMonth: null },
|
||||
})
|
||||
if (existing) { skipped++; continue }
|
||||
|
||||
const emp = await prisma.employee.findFirst({
|
||||
where: { id: employeeId, orgId },
|
||||
select: { housingFundBase: true, city: true },
|
||||
})
|
||||
if (!emp) { skipped++; continue }
|
||||
|
||||
await prisma.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
startMonth,
|
||||
endMonth: null,
|
||||
base: emp.housingFundBase || 0,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: emp.city || '北京',
|
||||
},
|
||||
})
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { housingFundStartMonth: startMonth, housingFundEndMonth: null },
|
||||
})
|
||||
enrolled++
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { enrolled, skipped } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 公积金在保人员
|
||||
router.get('/housing/active-declaration', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
|
||||
@@ -436,36 +436,8 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
|
||||
},
|
||||
})
|
||||
|
||||
// 劳务/实习协议不创建社保公积金记录
|
||||
if (!isNoSocialContract) {
|
||||
await tx.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: emp.id,
|
||||
startMonth: socialInsStartMonth,
|
||||
endMonth: null,
|
||||
base: socialInsBase,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
accountId: data.socialAccountId || null,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: emp.id,
|
||||
startMonth: housingFundStartMonth,
|
||||
endMonth: null,
|
||||
base: housingFundBase,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
accountId: data.housingAccountId || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
// 社保公积金记录不在录入时创建,由 HR 在社保模块办理增员后创建
|
||||
// 录入时仅保存社保基数到 Employee 表,工资计算直接用 employee.socialInsBase
|
||||
|
||||
await tx.salaryChangeRecord.create({
|
||||
data: {
|
||||
@@ -620,36 +592,7 @@ export async function rehireEmployee(orgId: string, userId: string, id: string,
|
||||
},
|
||||
})
|
||||
|
||||
// 劳务/实习协议不创建社保公积金记录
|
||||
if (!isNoSocialContract) {
|
||||
// 创建新社保缴费记录
|
||||
await prisma.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
startMonth: socialInsStartMonth,
|
||||
endMonth: null,
|
||||
base: socialInsBase,
|
||||
changeType: 'REHIRE',
|
||||
createdBy: userId,
|
||||
city: data.city || employee.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建新公积金缴费记录
|
||||
await prisma.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
startMonth: housingFundStartMonth,
|
||||
endMonth: null,
|
||||
base: housingFundBase,
|
||||
changeType: 'REHIRE',
|
||||
createdBy: userId,
|
||||
city: data.city || employee.city || '北京',
|
||||
},
|
||||
})
|
||||
}
|
||||
// 社保公积金记录不在重新入职时创建,由 HR 在社保模块办理增员后创建
|
||||
|
||||
// 创建新薪资记录
|
||||
await prisma.salaryChangeRecord.create({
|
||||
|
||||
Reference in New Issue
Block a user