feat: Phase 1-3 优化全部完成
Phase 1 紧急修复(8项): - 社保城市选择改为可输入 - 社保上下限拆分(三险/医保独立基数) - 公积金试算结果展示修复 - 花名册合同保存修复(日期ISO格式) - 薪酬批次创建失败修复(城市过滤+错误处理) - 证据链查看修复 - 个税计算修复(blank_employees读取基本工资) - 加班费倍率读取配置 Phase 2 功能完善(3项): - 批量导入per-row异常捕获+导入按钮 - 单人发薪UI入口优化 - 解除协议模板补充(员工提出离职版) Phase 3 后期规划(4项): - 工资表导入功能(POST /import/payroll + 前端入口) - 大病险/长护险附加险种(extraInsurances JSON + 计算适配) - 专项附加扣除按月录入(SpecialDeductionRecord模型 + 前端Tab) - 预置河北省社保政策(seed数据)
This commit is contained in:
@@ -165,6 +165,7 @@ model Organization {
|
||||
policyReadRecords PolicyReadRecord[]
|
||||
healthCheckReports HealthCheckReport[]
|
||||
annualValueReports AnnualValueReport[]
|
||||
specialDeductionRecords SpecialDeductionRecord[]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -240,6 +241,7 @@ model Employee {
|
||||
evidenceChains EvidenceChain[]
|
||||
attendanceConfirmations AttendanceConfirmation[]
|
||||
policyReadRecords PolicyReadRecord[]
|
||||
specialDeductionRecords SpecialDeductionRecord[]
|
||||
|
||||
@@unique([orgId, idCardHash])
|
||||
}
|
||||
@@ -386,8 +388,11 @@ model SocialInsuranceConfig {
|
||||
unemploymentEmp Float @default(0.5) // 失业保险 个人比例 %
|
||||
injuryOrg Float @default(0.2) // 工伤保险 企业比例 %
|
||||
maternityOrg Float @default(0.8) // 生育保险 企业比例 %
|
||||
baseMin Float @default(6326) // 社保缴费基数下限
|
||||
baseMax Float @default(33891) // 社保缴费基数上限
|
||||
baseMin Float @default(6326) // 社保缴费基数下限(养老/失业/工伤)
|
||||
baseMax Float @default(33891) // 社保缴费基数上限(养老/失业/工伤)
|
||||
medicalBaseMin Float @default(0) // 医疗/生育保险基数下限(0 时 fallback 到 baseMin)
|
||||
medicalBaseMax Float @default(0) // 医疗/生育保险基数上限(0 时 fallback 到 baseMax)
|
||||
extraInsurances Json? // 附加险种配置 JSON: [{ name, orgRate, empRate, baseType: 'pension'|'medical'|'fixed', fixedAmount }]
|
||||
effectiveFrom String // 生效月份 YYYY-MM
|
||||
effectiveTo String? // 失效月份 YYYY-MM(null=当前有效)
|
||||
isCurrent Boolean @default(true) // 是否当前生效版本
|
||||
@@ -1017,3 +1022,26 @@ model AnnualValueReport {
|
||||
@@index([orgId, year])
|
||||
@@index([orgId, createdAt])
|
||||
}
|
||||
|
||||
// 专项附加扣除按月记录
|
||||
model SpecialDeductionRecord {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
month String // YYYY-MM 月份
|
||||
amount Float @default(0) // 专项附加扣除总额
|
||||
children Float @default(0) // 子女教育
|
||||
elderly Float @default(0) // 赡养老人
|
||||
housing Float @default(0) // 住房贷款利息/住房租金
|
||||
education Float @default(0) // 继续教育
|
||||
infant Float @default(0) // 3岁以下婴幼儿照护
|
||||
remark String?
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([employeeId, month])
|
||||
@@index([orgId, month])
|
||||
}
|
||||
|
||||
@@ -132,6 +132,48 @@ async function main() {
|
||||
})
|
||||
console.log('公积金配置已创建')
|
||||
|
||||
// 4.6 创建河北省社保配置(2025年度标准)
|
||||
await prisma.socialInsuranceConfig.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
city: '石家庄',
|
||||
pensionOrg: 16,
|
||||
pensionEmp: 8,
|
||||
medicalOrg: 8,
|
||||
medicalEmp: 2,
|
||||
unemploymentOrg: 0.7,
|
||||
unemploymentEmp: 0.3,
|
||||
injuryOrg: 0.3,
|
||||
maternityOrg: 0.5,
|
||||
baseMin: 3920,
|
||||
baseMax: 19602,
|
||||
medicalBaseMin: 3920,
|
||||
medicalBaseMax: 19602,
|
||||
extraInsurances: [
|
||||
{ name: '大病医疗', baseType: 'fixed', fixedAmount: 5, empFixedAmount: 0, orgRate: 0, empRate: 0 },
|
||||
{ name: '长期护理险', baseType: 'pension', orgRate: 0.1, empRate: 0.1 },
|
||||
],
|
||||
effectiveFrom: '2025-07',
|
||||
createdBy: admin.id,
|
||||
},
|
||||
})
|
||||
console.log('河北省社保配置已创建')
|
||||
|
||||
// 4.7 创建河北省公积金配置
|
||||
await prisma.housingFundConfig.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
city: '石家庄',
|
||||
housingOrg: 12,
|
||||
housingEmp: 12,
|
||||
baseMin: 3920,
|
||||
baseMax: 19602,
|
||||
effectiveFrom: '2025-07',
|
||||
createdBy: admin.id,
|
||||
},
|
||||
})
|
||||
console.log('河北省公积金配置已创建')
|
||||
|
||||
// 5. 创建通知设置
|
||||
await prisma.notificationSetting.create({
|
||||
data: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router, Response } from 'express'
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import multer from 'multer'
|
||||
import * as XLSX from 'xlsx'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
@@ -6,6 +6,7 @@ import { requireAdmin } from '../middleware/rbac'
|
||||
import { encrypt, decrypt, sha256 } from '../lib/crypto'
|
||||
import prisma from '../lib/prisma'
|
||||
import { extractBirthDateFromIdCard, extractGenderFromIdCard } from '../services/retirement.service'
|
||||
import { calcBatchEntry } from '../services/payroll.service'
|
||||
|
||||
const router = Router()
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } })
|
||||
@@ -327,19 +328,21 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
const empByName = new Map(employees.map(e => [e.name, e.id]))
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const idCard = val(r['身份证号'])
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(r['姓名']))
|
||||
if (!empId) { result.errors.push(`加班第${i + 2}行:找不到员工「${val(r['姓名'])}」`); continue }
|
||||
const date = parseDate(r['日期'])
|
||||
if (!date) continue
|
||||
const month = dateToMonth(date)
|
||||
const otType = val(r['加班类型']) || '工作日加班'
|
||||
const hours = num(r['加班时长'])
|
||||
const weekdayHours = num(r['工作日加班时长']) || (otType.includes('工作日') ? hours : 0)
|
||||
const weekendHours = num(r['休息日加班时长']) || (otType.includes('休息日') ? hours : 0)
|
||||
const holidayHours = num(r['法定节假日加班时长']) || (otType.includes('法定') ? hours : 0)
|
||||
await prisma.overtimeRecord.create({ data: { orgId, employeeId: empId, month, weekdayHours, weekendHours, holidayHours, createdBy: userId } as any })
|
||||
result.overtime++
|
||||
try {
|
||||
const idCard = val(r['身份证号'])
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(r['姓名']))
|
||||
if (!empId) { result.errors.push(`加班第${i + 2}行:找不到员工「${val(r['姓名'])}」`); continue }
|
||||
const date = parseDate(r['日期'])
|
||||
if (!date) { result.errors.push(`加班第${i + 2}行:日期格式错误`); continue }
|
||||
const month = dateToMonth(date)
|
||||
const otType = val(r['加班类型']) || '工作日加班'
|
||||
const hours = num(r['加班时长'])
|
||||
const weekdayHours = num(r['工作日加班时长']) || (otType.includes('工作日') ? hours : 0)
|
||||
const weekendHours = num(r['休息日加班时长']) || (otType.includes('休息日') ? hours : 0)
|
||||
const holidayHours = num(r['法定节假日加班时长']) || (otType.includes('法定') ? hours : 0)
|
||||
await prisma.overtimeRecord.create({ data: { orgId, employeeId: empId, month, weekdayHours, weekendHours, holidayHours, createdBy: userId } as any })
|
||||
result.overtime++
|
||||
} catch (e: any) { result.errors.push(`加班第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,16 +354,18 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
const empByName = new Map(employees.map(e => [e.name, e.id]))
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const idCard = val(r['身份证号'])
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(r['姓名']))
|
||||
if (!empId) { result.errors.push(`违纪第${i + 2}行:找不到员工「${val(r['姓名'])}」`); continue }
|
||||
const date = parseDate(r['日期'])
|
||||
if (!date) continue
|
||||
const typeMap: any = { '迟到': 'LATE', '旷工': 'ABSENT', '不服从': 'INSUBORDINATION', '违纪': 'MISCONDUCT', '违规': 'VIOLATE_POLICY', '其他': 'OTHER' }
|
||||
const sevMap: any = { '警告': 'WARNING', '严重': 'SERIOUS', '重度': 'SEVERE' }
|
||||
const actMap: any = { '口头警告': 'ORAL_WARNING', '书面警告': 'WRITTEN_WARNING', '扣款': 'DEDUCTION', '降级': 'DEMOTION', '辞退': 'TERMINATION' }
|
||||
await prisma.disciplinaryRecord.create({ data: { orgId, employeeId: empId, violationDate: date, violationType: typeMap[val(r['违纪类型'])] || 'OTHER', description: val(r['描述']), severity: sevMap[val(r['严重程度'])] || 'WARNING', action: actMap[val(r['处罚'])] || 'ORAL_WARNING', createdBy: userId } })
|
||||
result.disciplinary++
|
||||
try {
|
||||
const idCard = val(r['身份证号'])
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(r['姓名']))
|
||||
if (!empId) { result.errors.push(`违纪第${i + 2}行:找不到员工「${val(r['姓名'])}」`); continue }
|
||||
const date = parseDate(r['日期'])
|
||||
if (!date) { result.errors.push(`违纪第${i + 2}行:日期格式错误`); continue }
|
||||
const typeMap: any = { '迟到': 'LATE', '旷工': 'ABSENT', '不服从': 'INSUBORDINATION', '违纪': 'MISCONDUCT', '违规': 'VIOLATE_POLICY', '其他': 'OTHER' }
|
||||
const sevMap: any = { '警告': 'WARNING', '严重': 'SERIOUS', '重度': 'SEVERE' }
|
||||
const actMap: any = { '口头警告': 'ORAL_WARNING', '书面警告': 'WRITTEN_WARNING', '扣款': 'DEDUCTION', '降级': 'DEMOTION', '辞退': 'TERMINATION' }
|
||||
await prisma.disciplinaryRecord.create({ data: { orgId, employeeId: empId, violationDate: date, violationType: typeMap[val(r['违纪类型'])] || 'OTHER', description: val(r['描述']), severity: sevMap[val(r['严重程度'])] || 'WARNING', action: actMap[val(r['处罚'])] || 'ORAL_WARNING', createdBy: userId } })
|
||||
result.disciplinary++
|
||||
} catch (e: any) { result.errors.push(`违纪第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,14 +377,16 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
const empByName = new Map(employees.map(e => [e.name, e.id]))
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
const idCard = val(r['身份证号'])
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(r['姓名']))
|
||||
if (!empId) { result.errors.push(`考勤第${i + 2}行:找不到员工「${val(r['姓名'])}」`); continue }
|
||||
const date = parseDate(r['日期'])
|
||||
if (!date) continue
|
||||
const statusMap: any = { '正常': 'NORMAL', '迟到': 'LATE', '早退': 'EARLY_LEAVE', '缺勤': 'ABSENT', '请假': 'LEAVE', '出差': 'BUSINESS_TRIP' }
|
||||
await prisma.attendanceRecord.create({ data: { orgId, employeeId: empId, date, status: statusMap[val(r['考勤状态'])] || 'NORMAL', checkInTime: val(r['上班时间']) || null, checkOutTime: val(r['下班时间']) || null, remark: val(r['备注']) || null, createdBy: userId } })
|
||||
result.attendance++
|
||||
try {
|
||||
const idCard = val(r['身份证号'])
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(r['姓名']))
|
||||
if (!empId) { result.errors.push(`考勤第${i + 2}行:找不到员工「${val(r['姓名'])}」`); continue }
|
||||
const date = parseDate(r['日期'])
|
||||
if (!date) { result.errors.push(`考勤第${i + 2}行:日期格式错误`); continue }
|
||||
const statusMap: any = { '正常': 'NORMAL', '迟到': 'LATE', '早退': 'EARLY_LEAVE', '缺勤': 'ABSENT', '请假': 'LEAVE', '出差': 'BUSINESS_TRIP' }
|
||||
await prisma.attendanceRecord.create({ data: { orgId, employeeId: empId, date, status: statusMap[val(r['考勤状态'])] || 'NORMAL', checkInTime: val(r['上班时间']) || null, checkOutTime: val(r['下班时间']) || null, remark: val(r['备注']) || null, createdBy: userId } })
|
||||
result.attendance++
|
||||
} catch (e: any) { result.errors.push(`考勤第${i + 2}行:${e?.message || '导入失败'}`) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,4 +617,104 @@ router.get('/monthly-template', authMiddleware, async (_req: AuthRequest, res: R
|
||||
res.send(buf)
|
||||
})
|
||||
|
||||
// 工资表导入 — 批量更新批次条目的薪酬输入项
|
||||
router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ success: false, message: '请上传文件' })
|
||||
const orgId = req.user!.orgId
|
||||
const userId = req.user!.id
|
||||
const batchId = req.body.batchId as string
|
||||
if (!batchId) return res.status(400).json({ success: false, message: '缺少批次ID' })
|
||||
|
||||
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
|
||||
if (!batch) return res.status(404).json({ success: false, message: '批次不存在' })
|
||||
if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, message: '已归档批次不可导入' })
|
||||
|
||||
const wb = XLSX.read(req.file.buffer, { type: 'buffer' })
|
||||
const sheet = wb.Sheets[wb.SheetNames[0]]
|
||||
if (!sheet) return res.status(400).json({ success: false, message: 'Excel 文件无有效 Sheet' })
|
||||
|
||||
const rows = XLSX.utils.sheet_to_json(sheet)
|
||||
const result = { total: rows.length, updated: 0, errors: [] as string[] }
|
||||
|
||||
// 构建员工查找索引
|
||||
const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, idCardHash: true } })
|
||||
const empByName = new Map(employees.map(e => [e.name, e.id]))
|
||||
const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash!, e.id]))
|
||||
|
||||
// 查询现有批次条目
|
||||
const entries = await prisma.batchEntry.findMany({ where: { batchId }, select: { id: true, employeeId: true } })
|
||||
const entryByEmp = new Map(entries.map(e => [e.employeeId, e.id]))
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i] as any
|
||||
try {
|
||||
const idCard = val(r['身份证号'])
|
||||
const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(r['姓名']))
|
||||
if (!empId) { result.errors.push(`第${i + 2}行:找不到员工「${val(r['姓名'])}」`); continue }
|
||||
const entryId = entryByEmp.get(empId)
|
||||
if (!entryId) { result.errors.push(`第${i + 2}行:员工「${val(r['姓名'])}」不在本批次中`); continue }
|
||||
|
||||
const inputs = {
|
||||
baseSalary: num(r['基本工资']) || 0,
|
||||
overtimePay: num(r['加班费']) || 0,
|
||||
allowance: num(r['津贴']) || 0,
|
||||
deduction: num(r['扣款']) || 0,
|
||||
bonus: num(r['奖金']) || 0,
|
||||
}
|
||||
|
||||
// 重新计算税费
|
||||
const calcResult = await calcBatchEntry(orgId, empId, batch.month, inputs, batch.type)
|
||||
await prisma.batchEntry.update({ where: { id: entryId }, data: { ...inputs, ...calcResult } })
|
||||
result.updated++
|
||||
} catch (e: any) {
|
||||
result.errors.push(`第${i + 2}行:${e?.message || '导入失败'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新批次汇总
|
||||
const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
|
||||
const totals = allEntries.reduce((acc, e) => ({
|
||||
totalPay: acc.totalPay + e.totalPay,
|
||||
totalNetPay: acc.totalNetPay + e.netPay,
|
||||
totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
|
||||
totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
|
||||
totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
|
||||
totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
|
||||
totalTax: acc.totalTax + e.tax,
|
||||
}), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })
|
||||
|
||||
await prisma.payrollBatch.update({
|
||||
where: { id: batchId },
|
||||
data: {
|
||||
totalPay: Math.round(totals.totalPay * 100) / 100,
|
||||
totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
|
||||
totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
|
||||
totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
|
||||
totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
|
||||
totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
|
||||
totalTax: Math.round(totals.totalTax * 100) / 100,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 工资表导入模板下载
|
||||
router.get('/payroll-template', authMiddleware, (_req: AuthRequest, res: Response) => {
|
||||
const wb = XLSX.utils.book_new()
|
||||
const data = [
|
||||
{ '姓名': '张三', '身份证号': '110101199001011234', '基本工资': 10000, '加班费': 500, '津贴': 800, '扣款': 0, '奖金': 2000 },
|
||||
{ '姓名': '李四', '身份证号': '110101199002021234', '基本工资': 12000, '加班费': 0, '津贴': 600, '扣款': 100, '奖金': 0 },
|
||||
]
|
||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data), '工资表')
|
||||
const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="payroll-import-template.xlsx"')
|
||||
res.send(buf)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -41,10 +41,11 @@ router.get('/overtime', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
router.post('/overtime', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = overtimeSchema.parse(req.body)
|
||||
const hourlyWage = data.monthlyWage / 21.75 / 8
|
||||
const weekdayPay = hourlyWage * 1.5 * data.weekdayHours
|
||||
const weekendPay = hourlyWage * 2.0 * data.weekendHours
|
||||
const holidayPay = hourlyWage * 3.0 * data.holidayHours
|
||||
const otConfig = await prisma.overtimeConfig.findUnique({ where: { orgId: req.user!.orgId } }) ?? { weekdayRate: 1.5, weekendRate: 2.0, holidayRate: 3.0, monthlyDays: 21.75, dailyHours: 8 }
|
||||
const hourlyWage = data.monthlyWage / otConfig.monthlyDays / otConfig.dailyHours
|
||||
const weekdayPay = hourlyWage * otConfig.weekdayRate * data.weekdayHours
|
||||
const weekendPay = hourlyWage * otConfig.weekendRate * data.weekendHours
|
||||
const holidayPay = hourlyWage * otConfig.holidayRate * data.holidayHours
|
||||
const totalPay = weekdayPay + weekendPay + holidayPay
|
||||
|
||||
const record = await prisma.overtimeRecord.upsert({
|
||||
@@ -103,10 +104,11 @@ router.put('/overtime/:id', async (req: AuthRequest, res: Response, next: NextFu
|
||||
const weekendHours = data.weekendHours ?? existing.weekendHours
|
||||
const holidayHours = data.holidayHours ?? existing.holidayHours
|
||||
|
||||
const hourlyWage = monthlyWage / 21.75 / 8
|
||||
const weekdayPay = hourlyWage * 1.5 * weekdayHours
|
||||
const weekendPay = hourlyWage * 2.0 * weekendHours
|
||||
const holidayPay = hourlyWage * 3.0 * holidayHours
|
||||
const otConfig = await prisma.overtimeConfig.findUnique({ where: { orgId: req.user!.orgId } }) ?? { weekdayRate: 1.5, weekendRate: 2.0, holidayRate: 3.0, monthlyDays: 21.75, dailyHours: 8 }
|
||||
const hourlyWage = monthlyWage / otConfig.monthlyDays / otConfig.dailyHours
|
||||
const weekdayPay = hourlyWage * otConfig.weekdayRate * weekdayHours
|
||||
const weekendPay = hourlyWage * otConfig.weekendRate * weekendHours
|
||||
const holidayPay = hourlyWage * otConfig.holidayRate * holidayHours
|
||||
const totalPay = weekdayPay + weekendPay + holidayPay
|
||||
|
||||
const record = await prisma.overtimeRecord.update({
|
||||
|
||||
@@ -297,6 +297,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
|
||||
// 创建批次条目
|
||||
const entries: any[] = []
|
||||
const failedEmployees: { employeeId: string; name: string; error: string }[] = []
|
||||
for (const emp of employees) {
|
||||
let baseSalary = 0
|
||||
let overtimePay = 0
|
||||
@@ -334,6 +335,10 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
deduction = prevPayslip?.deduction || 0
|
||||
}
|
||||
// blank_employees 和 blank_all: 所有金额默认 0
|
||||
// blank_employees 模式下尝试从员工记录获取基本工资
|
||||
if (mode === 'blank_employees' && emp.monthlySalary) {
|
||||
try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
|
||||
}
|
||||
|
||||
// 判断同月是否已有归档的常规批次(用于决定是否跳过社保)
|
||||
const hasArchivedRegularBatch = await prisma.payrollBatch.count({
|
||||
@@ -343,7 +348,15 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
// 计算社保、个税等
|
||||
// 同月已有归档常规批次时,新批次跳过社保(避免重复扣缴),但用户可手动编辑覆盖
|
||||
const skipSocial = type !== 'BONUS' && type !== 'SEVERANCE' && hasArchivedRegularBatch > 0
|
||||
const calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, overtimePay, allowance, deduction, bonus }, type, { skipSocial })
|
||||
|
||||
let calcResult: any
|
||||
try {
|
||||
calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, overtimePay, allowance, deduction, bonus }, type, { skipSocial })
|
||||
} catch (calcErr: any) {
|
||||
// 单个员工计算失败不阻塞整个批次,记录错误并使用零值
|
||||
failedEmployees.push({ employeeId: emp.id, name: emp.name, error: calcErr?.message || '计算失败' })
|
||||
calcResult = { socialEmp: 0, socialOrg: 0, housingEmp: 0, housingOrg: 0, tax: 0, totalPay: baseSalary + overtimePay + allowance + bonus - deduction, netPay: baseSalary + overtimePay + allowance + bonus - deduction }
|
||||
}
|
||||
|
||||
// 风险提示
|
||||
const riskWarnings = await getPayrollRiskWarnings(orgId, emp.id)
|
||||
@@ -396,7 +409,7 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
include: { entries: { include: { employee: { select: { id: true, name: true, department: true, status: true } } } } },
|
||||
})
|
||||
|
||||
res.json({ success: true, data: updatedBatch })
|
||||
res.json({ success: true, data: updatedBatch, failedEmployees: failedEmployees.length > 0 ? failedEmployees : undefined })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ const socialConfigFields = {
|
||||
maternityOrg: z.number().optional(),
|
||||
baseMin: z.number().optional(),
|
||||
baseMax: z.number().optional(),
|
||||
medicalBaseMin: z.number().optional(),
|
||||
medicalBaseMax: z.number().optional(),
|
||||
extraInsurances: z.any().optional(),
|
||||
}
|
||||
|
||||
const housingConfigFields = {
|
||||
@@ -408,23 +411,49 @@ router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunc
|
||||
}
|
||||
|
||||
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
|
||||
const medMin = config.medicalBaseMin && config.medicalBaseMin > 0 ? config.medicalBaseMin : config.baseMin
|
||||
const medMax = config.medicalBaseMax && config.medicalBaseMax > 0 ? config.medicalBaseMax : config.baseMax
|
||||
const medicalBase = Math.min(Math.max(base, medMin), medMax)
|
||||
|
||||
const pensionOrg = actualBase * config.pensionOrg / 100
|
||||
const pensionEmp = actualBase * config.pensionEmp / 100
|
||||
const medicalOrg = actualBase * config.medicalOrg / 100
|
||||
const medicalEmp = actualBase * config.medicalEmp / 100
|
||||
const medicalOrg = medicalBase * config.medicalOrg / 100
|
||||
const medicalEmp = medicalBase * config.medicalEmp / 100
|
||||
const unemploymentOrg = actualBase * config.unemploymentOrg / 100
|
||||
const unemploymentEmp = actualBase * config.unemploymentEmp / 100
|
||||
const injuryOrg = actualBase * config.injuryOrg / 100
|
||||
const maternityOrg = actualBase * config.maternityOrg / 100
|
||||
const totalOrg = pensionOrg + medicalOrg + unemploymentOrg + injuryOrg + maternityOrg
|
||||
const totalEmp = pensionEmp + medicalEmp + unemploymentEmp
|
||||
const maternityOrg = medicalBase * config.maternityOrg / 100
|
||||
let totalOrg = pensionOrg + medicalOrg + unemploymentOrg + injuryOrg + maternityOrg
|
||||
let totalEmp = pensionEmp + medicalEmp + unemploymentEmp
|
||||
|
||||
// 附加险种
|
||||
const extraItems: any[] = []
|
||||
if (config.extraInsurances && Array.isArray(config.extraInsurances)) {
|
||||
for (const ins of config.extraInsurances) {
|
||||
const insBase = ins.baseType === 'medical' ? medicalBase : ins.baseType === 'fixed' ? 1 : actualBase
|
||||
if (ins.baseType === 'fixed' && ins.fixedAmount) {
|
||||
const orgAmt = ins.fixedAmount
|
||||
const empAmt = ins.empFixedAmount || 0
|
||||
totalOrg += orgAmt
|
||||
totalEmp += empAmt
|
||||
extraItems.push({ name: ins.name, orgRate: 0, empRate: 0, orgAmount: orgAmt, empAmount: empAmt })
|
||||
} else {
|
||||
const orgAmt = insBase * (ins.orgRate || 0) / 100
|
||||
const empAmt = insBase * (ins.empRate || 0) / 100
|
||||
totalOrg += orgAmt
|
||||
totalEmp += empAmt
|
||||
extraItems.push({ name: ins.name, orgRate: ins.orgRate || 0, empRate: ins.empRate || 0, orgAmount: orgAmt, empAmount: empAmt })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = totalOrg + totalEmp
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
actualBase,
|
||||
medicalBase,
|
||||
originalBase: base,
|
||||
capped: base > config.baseMax,
|
||||
floored: base < config.baseMin,
|
||||
@@ -435,6 +464,7 @@ router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunc
|
||||
{ name: '失业保险', orgRate: config.unemploymentOrg, empRate: config.unemploymentEmp, orgAmount: unemploymentOrg, empAmount: unemploymentEmp },
|
||||
{ name: '工伤保险', orgRate: config.injuryOrg, empRate: 0, orgAmount: injuryOrg, empAmount: 0 },
|
||||
{ name: '生育保险', orgRate: config.maternityOrg, empRate: 0, orgAmount: maternityOrg, empAmount: 0 },
|
||||
...extraItems,
|
||||
],
|
||||
totalOrg,
|
||||
totalEmp,
|
||||
@@ -790,16 +820,30 @@ router.post('/housing-config/:id/reset-adjustment', async (req: AuthRequest, res
|
||||
/** 根据基数和社保配置计算各项企业/个人缴费明细 */
|
||||
function calcSocialDetail(base: number, config: any) {
|
||||
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
|
||||
const medMin = config.medicalBaseMin && config.medicalBaseMin > 0 ? config.medicalBaseMin : config.baseMin
|
||||
const medMax = config.medicalBaseMax && config.medicalBaseMax > 0 ? config.medicalBaseMax : config.baseMax
|
||||
const medicalBase = Math.min(Math.max(base, medMin), medMax)
|
||||
const items = [
|
||||
{ name: '养老', orgRate: config.pensionOrg, empRate: config.pensionEmp, orgAmount: actualBase * config.pensionOrg / 100, empAmount: actualBase * config.pensionEmp / 100 },
|
||||
{ name: '医疗', orgRate: config.medicalOrg, empRate: config.medicalEmp, orgAmount: actualBase * config.medicalOrg / 100, empAmount: actualBase * config.medicalEmp / 100 },
|
||||
{ name: '医疗', orgRate: config.medicalOrg, empRate: config.medicalEmp, orgAmount: medicalBase * config.medicalOrg / 100, empAmount: medicalBase * config.medicalEmp / 100 },
|
||||
{ name: '失业', orgRate: config.unemploymentOrg, empRate: config.unemploymentEmp, orgAmount: actualBase * config.unemploymentOrg / 100, empAmount: actualBase * config.unemploymentEmp / 100 },
|
||||
{ name: '工伤', orgRate: config.injuryOrg, empRate: 0, orgAmount: actualBase * config.injuryOrg / 100, empAmount: 0 },
|
||||
{ name: '生育', orgRate: config.maternityOrg, empRate: 0, orgAmount: actualBase * config.maternityOrg / 100, empAmount: 0 },
|
||||
{ name: '生育', orgRate: config.maternityOrg, empRate: 0, orgAmount: medicalBase * config.maternityOrg / 100, empAmount: 0 },
|
||||
]
|
||||
// 附加险种
|
||||
if (config.extraInsurances && Array.isArray(config.extraInsurances)) {
|
||||
for (const ins of config.extraInsurances) {
|
||||
const insBase = ins.baseType === 'medical' ? medicalBase : ins.baseType === 'fixed' ? 1 : actualBase
|
||||
if (ins.baseType === 'fixed' && ins.fixedAmount) {
|
||||
items.push({ name: ins.name, orgRate: 0, empRate: 0, orgAmount: ins.fixedAmount, empAmount: ins.empFixedAmount || 0 })
|
||||
} else {
|
||||
items.push({ name: ins.name, orgRate: ins.orgRate || 0, empRate: ins.empRate || 0, orgAmount: insBase * (ins.orgRate || 0) / 100, empAmount: insBase * (ins.empRate || 0) / 100 })
|
||||
}
|
||||
}
|
||||
}
|
||||
const totalOrg = items.reduce((s, i) => s + i.orgAmount, 0)
|
||||
const totalEmp = items.reduce((s, i) => s + i.empAmount, 0)
|
||||
return { actualBase, items, totalOrg, totalEmp }
|
||||
return { actualBase, medicalBase, items, totalOrg, totalEmp }
|
||||
}
|
||||
|
||||
/** 根据基数和公积金配置计算企业/个人缴费明细 */
|
||||
@@ -1274,4 +1318,139 @@ router.put('/records/housing/:id/correct', async (req: AuthRequest, res: Respons
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 专项附加扣除按月录入 ==========
|
||||
|
||||
// 查询员工某月专项附加扣除
|
||||
router.get('/special-deduction', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { employeeId, month } = req.query
|
||||
if (!employeeId || !month) return res.status(400).json({ success: false, message: '缺少 employeeId 或 month' })
|
||||
const record = await prisma.specialDeductionRecord.findUnique({
|
||||
where: { employeeId_month: { employeeId: employeeId as string, month: month as string } },
|
||||
})
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 批量查询员工某月专项附加扣除
|
||||
router.get('/special-deduction/batch', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { month } = req.query
|
||||
if (!month) return res.status(400).json({ success: false, message: '缺少 month' })
|
||||
const records = await prisma.specialDeductionRecord.findMany({
|
||||
where: { orgId: req.user!.orgId, month: month as string },
|
||||
include: { employee: { select: { name: true, department: true } } },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建/更新专项附加扣除
|
||||
const upsertDeductionSchema = z.object({
|
||||
employeeId: z.string().min(1),
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
amount: z.number().min(0).optional(),
|
||||
children: z.number().min(0).optional(),
|
||||
elderly: z.number().min(0).optional(),
|
||||
housing: z.number().min(0).optional(),
|
||||
education: z.number().min(0).optional(),
|
||||
infant: z.number().min(0).optional(),
|
||||
remark: z.string().optional(),
|
||||
})
|
||||
|
||||
router.post('/special-deduction', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = upsertDeductionSchema.parse(req.body)
|
||||
const orgId = req.user!.orgId
|
||||
const amount = data.amount ?? (data.children || 0) + (data.elderly || 0) + (data.housing || 0) + (data.education || 0) + (data.infant || 0)
|
||||
|
||||
const record = await prisma.specialDeductionRecord.upsert({
|
||||
where: { employeeId_month: { employeeId: data.employeeId, month: data.month } },
|
||||
create: {
|
||||
orgId,
|
||||
employeeId: data.employeeId,
|
||||
month: data.month,
|
||||
amount,
|
||||
children: data.children || 0,
|
||||
elderly: data.elderly || 0,
|
||||
housing: data.housing || 0,
|
||||
education: data.education || 0,
|
||||
infant: data.infant || 0,
|
||||
remark: data.remark,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
update: {
|
||||
amount,
|
||||
children: data.children || 0,
|
||||
elderly: data.elderly || 0,
|
||||
housing: data.housing || 0,
|
||||
education: data.education || 0,
|
||||
infant: data.infant || 0,
|
||||
remark: data.remark,
|
||||
},
|
||||
})
|
||||
|
||||
// 同步员工便捷字段
|
||||
await prisma.employee.update({ where: { id: data.employeeId }, data: { specialDeduction: amount } })
|
||||
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 批量录入专项附加扣除
|
||||
router.post('/special-deduction/batch', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { month, items } = req.body as { month: string; items: any[] }
|
||||
if (!month || !items || !Array.isArray(items)) return res.status(400).json({ success: false, message: '缺少 month 或 items' })
|
||||
const orgId = req.user!.orgId
|
||||
const result = { total: items.length, updated: 0, errors: [] as string[] }
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
try {
|
||||
const amount = item.amount ?? (item.children || 0) + (item.elderly || 0) + (item.housing || 0) + (item.education || 0) + (item.infant || 0)
|
||||
await prisma.specialDeductionRecord.upsert({
|
||||
where: { employeeId_month: { employeeId: item.employeeId, month } },
|
||||
create: {
|
||||
orgId,
|
||||
employeeId: item.employeeId,
|
||||
month,
|
||||
amount,
|
||||
children: item.children || 0,
|
||||
elderly: item.elderly || 0,
|
||||
housing: item.housing || 0,
|
||||
education: item.education || 0,
|
||||
infant: item.infant || 0,
|
||||
remark: item.remark,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
update: {
|
||||
amount,
|
||||
children: item.children || 0,
|
||||
elderly: item.elderly || 0,
|
||||
housing: item.housing || 0,
|
||||
education: item.education || 0,
|
||||
infant: item.infant || 0,
|
||||
remark: item.remark,
|
||||
},
|
||||
})
|
||||
await prisma.employee.update({ where: { id: item.employeeId }, data: { specialDeduction: amount } })
|
||||
result.updated++
|
||||
} catch (e: any) {
|
||||
result.errors.push(`第${i + 1}行:${e?.message || '录入失败'}`)
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -35,10 +35,35 @@ export async function getTemplate(orgId: string) {
|
||||
// ========== 社保计算 ==========
|
||||
|
||||
export function calcSocialInsurance(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 { actualBase, socialEmp, socialOrg }
|
||||
// 医疗/生育保险基数(独立上下限,为 0 时 fallback 到统一基数)
|
||||
const medMin = config.medicalBaseMin && config.medicalBaseMin > 0 ? config.medicalBaseMin : config.baseMin
|
||||
const medMax = config.medicalBaseMax && config.medicalBaseMax > 0 ? config.medicalBaseMax : config.baseMax
|
||||
const medicalBase = Math.min(Math.max(base, medMin), medMax)
|
||||
let socialEmp = actualBase * (config.pensionEmp + config.unemploymentEmp) / 100 + medicalBase * config.medicalEmp / 100
|
||||
let socialOrg = actualBase * (config.pensionOrg + config.unemploymentOrg + config.injuryOrg) / 100 + medicalBase * (config.medicalOrg + config.maternityOrg) / 100
|
||||
// 附加险种(大病险/长护险等)
|
||||
const extraItems: any[] = []
|
||||
if (config.extraInsurances && Array.isArray(config.extraInsurances)) {
|
||||
for (const ins of config.extraInsurances) {
|
||||
const insBase = ins.baseType === 'medical' ? medicalBase : ins.baseType === 'fixed' ? 1 : actualBase
|
||||
if (ins.baseType === 'fixed' && ins.fixedAmount) {
|
||||
const orgAmt = ins.fixedAmount
|
||||
const empAmt = ins.empFixedAmount || 0
|
||||
socialOrg += orgAmt
|
||||
socialEmp += empAmt
|
||||
extraItems.push({ name: ins.name, orgAmount: orgAmt, empAmount: empAmt })
|
||||
} else {
|
||||
const orgAmt = insBase * (ins.orgRate || 0) / 100
|
||||
const empAmt = insBase * (ins.empRate || 0) / 100
|
||||
socialOrg += orgAmt
|
||||
socialEmp += empAmt
|
||||
extraItems.push({ name: ins.name, orgAmount: orgAmt, empAmount: empAmt })
|
||||
}
|
||||
}
|
||||
}
|
||||
return { actualBase, medicalBase, socialEmp, socialOrg, extraItems }
|
||||
}
|
||||
|
||||
export function calcHousingFund(base: number, config: any) {
|
||||
@@ -106,11 +131,14 @@ export async function calcBatchEntry(
|
||||
batchType: string = 'REGULAR',
|
||||
options?: { skipSocial?: boolean; overrideSocial?: { socialEmp?: number; socialOrg?: number; housingEmp?: number; housingOrg?: number } },
|
||||
) {
|
||||
const [employee, socialConfig, housingConfig] = await Promise.all([
|
||||
prisma.employee.findFirst({ where: { id: employeeId, orgId } }),
|
||||
const employee = await prisma.employee.findFirst({ where: { id: employeeId, orgId } })
|
||||
if (!employee) throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
|
||||
const cityWhere = employee.city ? { orgId, city: employee.city } : { orgId }
|
||||
const [socialConfig, housingConfig] = await Promise.all([
|
||||
prisma.socialInsuranceConfig.findFirst({
|
||||
where: {
|
||||
orgId,
|
||||
...cityWhere,
|
||||
effectiveFrom: { lte: month },
|
||||
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
|
||||
},
|
||||
@@ -118,14 +146,13 @@ export async function calcBatchEntry(
|
||||
}),
|
||||
prisma.housingFundConfig.findFirst({
|
||||
where: {
|
||||
orgId,
|
||||
...cityWhere,
|
||||
effectiveFrom: { lte: month },
|
||||
OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }],
|
||||
},
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
}),
|
||||
])
|
||||
if (!employee) throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||
|
||||
// 社保基数:优先用员工核定基数,否则用基本工资
|
||||
const socialBase = employee.socialInsBase || inputs.baseSalary
|
||||
|
||||
@@ -90,7 +90,7 @@ export const documentTemplates: DocumentTemplate[] = [
|
||||
id: 'tpl_termination_agreement',
|
||||
name: '解除劳动合同协议书',
|
||||
category: 'AGREEMENT',
|
||||
description: '协商一致解除劳动合同协议书模板',
|
||||
description: '协商一致解除劳动合同协议书模板(用人单位提出)',
|
||||
variables: ['companyName', 'employeeName', 'idCard', 'terminationDate', 'compensation', 'lastWorkDay', 'socialInsEndMonth', 'housingFundEndMonth'],
|
||||
content: `解除劳动合同协议书
|
||||
|
||||
@@ -123,6 +123,53 @@ export const documentTemplates: DocumentTemplate[] = [
|
||||
八、其他
|
||||
本协议一式两份,甲乙双方各执一份,自双方签字盖章之日起生效。
|
||||
|
||||
甲方(盖章):____________ 乙方(签字):____________
|
||||
日期:____年__月__日 日期:____年__月__日`,
|
||||
},
|
||||
{
|
||||
id: 'tpl_termination_agreement_employee',
|
||||
name: '解除劳动合同协议书(员工提出离职)',
|
||||
category: 'AGREEMENT',
|
||||
description: '员工主动提出离职的解除劳动合同协议书模板',
|
||||
variables: ['companyName', 'employeeName', 'idCard', 'terminationDate', 'lastWorkDay', 'socialInsEndMonth', 'housingFundEndMonth', 'resignationReason'],
|
||||
content: `解除劳动合同协议书
|
||||
|
||||
甲方(用人单位):{{companyName}}
|
||||
乙方(劳动者):{{employeeName}},身份证号:{{idCard}}
|
||||
|
||||
乙方因个人原因主动提出离职,经甲乙双方友好协商,就解除劳动合同事宜达成如下协议:
|
||||
|
||||
一、离职原因
|
||||
乙方因{{resignationReason}},自愿提出解除劳动合同。
|
||||
|
||||
二、解除日期
|
||||
双方同意于{{terminationDate}}解除劳动合同,乙方最后工作日为{{lastWorkDay}}。
|
||||
|
||||
三、工资结算
|
||||
甲方结清乙方截至解除日的所有工资、加班费等劳动报酬,于乙方办理完工作交接手续后__个工作日内一次性支付。
|
||||
|
||||
四、社会保险和公积金
|
||||
甲方为乙方缴纳社会保险至{{socialInsEndMonth}}月,住房公积金缴存至{{housingFundEndMonth}}月。
|
||||
|
||||
五、工作交接
|
||||
乙方应在最后工作日前完成工作交接,归还甲方所有财物和资料。
|
||||
|
||||
六、经济补偿
|
||||
因乙方主动提出离职,甲方无需向乙方支付经济补偿金。乙方确认对此无异议。
|
||||
|
||||
七、保密义务
|
||||
乙方解除劳动合同后,仍应遵守保密义务,不得泄露甲方商业秘密。
|
||||
|
||||
八、竞业限制
|
||||
如双方另行签有竞业限制协议,乙方应继续履行竞业限制义务。
|
||||
|
||||
九、争议解决
|
||||
本协议履行过程中如发生争议,双方应协商解决;协商不成的,可向劳动争议仲裁委员会申请仲裁。
|
||||
|
||||
十、其他
|
||||
1. 本协议签订后,双方劳动关系即告终止,双方不再存在任何劳动争议。
|
||||
2. 本协议一式两份,甲乙双方各执一份,自双方签字盖章之日起生效。
|
||||
|
||||
甲方(盖章):____________ 乙方(签字):____________
|
||||
日期:____年__月__日 日期:____年__月__日`,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user