From 64ae633857c8198309a216d586cc3e1cfe0edc1c Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Wed, 19 Aug 2026 08:57:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20P4=20dataSource=20=E5=AD=97=E6=AE=B5=20?= =?UTF-8?q?+=20overtimePay=20=E4=BF=AE=E5=A4=8D=20+=20=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=8A=A8=E6=80=81=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - schema: PayslipItem 增加 dataSource 字段标记数据来源 - DEFAULT_ITEMS: overtimePay 从 CALCULATED 改为 INPUT(修复被重算覆盖为0的bug) - DEFAULT_ITEMS: performanceSalary/bonus/deduction 标记 dataSource - 迁移逻辑: 已有组织自动补充 dataSource + 修正 overtimePay 类型 - 后端模板 CRUD 支持 dataSource 字段 - 前端模板编辑器增加数据来源下拉选择 - 前端模板表格增加数据来源列 - 前端批次详情'获取'按钮根据模板 dataSource 动态显示 --- backend/prisma/schema.prisma | 1 + backend/src/routes/payroll2.routes.ts | 3 + backend/src/services/payroll.service.ts | 63 ++++++++----- frontend/src/pages/money/BatchTab.tsx | 109 +++++++++++++---------- frontend/src/pages/money/TemplateTab.tsx | 39 +++++++- 5 files changed, 143 insertions(+), 72 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index b053b21..9425986 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -969,6 +969,7 @@ model PayslipItem { formula String? // 计算公式(CALCULATED 类型),如 "baseSalary + overtimePay + allowance - deduction";SYSTEM 类型存系统函数标识如 SOCIAL_EMP calcType String @default("FORMULA") // FORMULA: 公式解析; SYSTEM: 系统函数计算(仅 CALCULATED 类型有效) dependencies String @default("[]") // JSON数组:该字段依赖哪些其他字段代码 + dataSource String? // 数据来源标识:null=手动填写, overtime=加班记录, performance=绩效记录, bonus=奖金记录, deduction=扣款记录 order Int @default(0) isDefault Boolean @default(true) // 系统预置项不可删除 isEditable Boolean @default(true) diff --git a/backend/src/routes/payroll2.routes.ts b/backend/src/routes/payroll2.routes.ts index 661abc0..ec09c7d 100644 --- a/backend/src/routes/payroll2.routes.ts +++ b/backend/src/routes/payroll2.routes.ts @@ -164,6 +164,7 @@ const updateTemplateItemSchema = z.object({ formula: z.string().nullable().optional(), calcType: z.enum(['FORMULA', 'SYSTEM']).optional(), dependencies: z.string().optional(), + dataSource: z.string().nullable().optional(), order: z.number().int().optional(), isEditable: z.boolean().optional(), }) @@ -183,6 +184,7 @@ router.put('/template/:id', async (req: AuthRequest, res: Response, next: NextFu if (data.isEditable !== undefined) updateData.isEditable = data.isEditable if (data.calcType !== undefined) updateData.calcType = data.calcType if (data.dependencies !== undefined) updateData.dependencies = data.dependencies + if (data.dataSource !== undefined) updateData.dataSource = data.dataSource const updated = await prisma.payslipItem.update({ where: { id: req.params.id }, data: updateData }) res.json({ success: true, data: updated }) @@ -199,6 +201,7 @@ const createTemplateItemSchema = z.object({ formula: z.string().nullable().optional(), calcType: z.enum(['FORMULA', 'SYSTEM']).default('FORMULA'), dependencies: z.string().default('[]'), + dataSource: z.string().nullable().default(null), order: z.number().int().default(99), isEditable: z.boolean().default(true), }) diff --git a/backend/src/services/payroll.service.ts b/backend/src/services/payroll.service.ts index f829586..517c9db 100644 --- a/backend/src/services/payroll.service.ts +++ b/backend/src/services/payroll.service.ts @@ -67,25 +67,25 @@ async function getStandardByAccountAndMonth(accountId: string, month: string) { // ========== 薪酬模版 ========== -const DEFAULT_ITEMS: { name: string; code: string; type: 'INPUT' | 'CALCULATED'; formula: string | null; calcType: string; dependencies: string; order: number; isDefault: boolean; isEditable: boolean }[] = [ - { name: '基本工资', code: 'baseSalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 1, isDefault: true, isEditable: true }, - { name: '岗位工资', code: 'positionSalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 2, isDefault: true, isEditable: true }, - { name: '绩效工资', code: 'performanceSalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 3, isDefault: true, isEditable: true }, - { name: '工龄工资', code: 'senioritySalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 4, isDefault: true, isEditable: true }, - { name: '加班费', code: 'overtimePay', type: 'CALCULATED', formula: 'weekdayOvertimePay + weekendOvertimePay + holidayOvertimePay', calcType: 'FORMULA', dependencies: '["weekdayOvertimePay","weekendOvertimePay","holidayOvertimePay"]', order: 5, isDefault: true, isEditable: false }, - { name: '交通补贴', code: 'transportAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 6, isDefault: true, isEditable: true }, - { name: '餐补', code: 'mealAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 7, isDefault: true, isEditable: true }, - { name: '住房补贴', code: 'housingAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 8, isDefault: true, isEditable: true }, - { name: '通讯补贴', code: 'communicationAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 9, isDefault: true, isEditable: true }, - { name: '津贴补贴', code: 'allowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 10, isDefault: true, isEditable: true }, - { name: '奖金', code: 'bonus', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 11, isDefault: true, isEditable: true }, - { name: '扣款', code: 'deduction', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 12, isDefault: true, isEditable: true }, - { name: '其他扣款', code: 'otherDeduction', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', order: 13, isDefault: true, isEditable: true }, - { name: '应发合计', code: 'totalPay', type: 'CALCULATED', formula: 'baseSalary + positionSalary + performanceSalary + senioritySalary + overtimePay + transportAllowance + mealAllowance + housingAllowance + communicationAllowance + allowance + bonus - deduction - otherDeduction', calcType: 'FORMULA', dependencies: '["baseSalary","positionSalary","performanceSalary","senioritySalary","overtimePay","transportAllowance","mealAllowance","housingAllowance","communicationAllowance","allowance","bonus","deduction","otherDeduction"]', order: 14, isDefault: true, isEditable: false }, - { name: '个人社保', code: 'socialEmp', type: 'CALCULATED', formula: 'SOCIAL_EMP', calcType: 'SYSTEM', dependencies: '[]', order: 15, isDefault: true, isEditable: false }, - { name: '个人公积金', code: 'housingEmp', type: 'CALCULATED', formula: 'HOUSING_EMP', calcType: 'SYSTEM', dependencies: '[]', order: 16, isDefault: true, isEditable: false }, - { name: '个人所得税', code: 'tax', type: 'CALCULATED', formula: 'TAX', calcType: 'SYSTEM', dependencies: '["totalPay","socialEmp","housingEmp"]', order: 17, isDefault: true, isEditable: false }, - { name: '实发工资', code: 'netPay', type: 'CALCULATED', formula: 'totalPay - socialEmp - housingEmp - tax', calcType: 'FORMULA', dependencies: '["totalPay","socialEmp","housingEmp","tax"]', order: 18, isDefault: true, isEditable: false }, +const DEFAULT_ITEMS: { name: string; code: string; type: 'INPUT' | 'CALCULATED'; formula: string | null; calcType: string; dependencies: string; dataSource: string | null; order: number; isDefault: boolean; isEditable: boolean }[] = [ + { name: '基本工资', code: 'baseSalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 1, isDefault: true, isEditable: true }, + { name: '岗位工资', code: 'positionSalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 2, isDefault: true, isEditable: true }, + { name: '绩效工资', code: 'performanceSalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: 'performance', order: 3, isDefault: true, isEditable: true }, + { name: '工龄工资', code: 'senioritySalary', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 4, isDefault: true, isEditable: true }, + { name: '加班费', code: 'overtimePay', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: 'overtime', order: 5, isDefault: true, isEditable: true }, + { name: '交通补贴', code: 'transportAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 6, isDefault: true, isEditable: true }, + { name: '餐补', code: 'mealAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 7, isDefault: true, isEditable: true }, + { name: '住房补贴', code: 'housingAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 8, isDefault: true, isEditable: true }, + { name: '通讯补贴', code: 'communicationAllowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 9, isDefault: true, isEditable: true }, + { name: '津贴补贴', code: 'allowance', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 10, isDefault: true, isEditable: true }, + { name: '奖金', code: 'bonus', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: 'bonus', order: 11, isDefault: true, isEditable: true }, + { name: '扣款', code: 'deduction', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: 'deduction', order: 12, isDefault: true, isEditable: true }, + { name: '其他扣款', code: 'otherDeduction', type: 'INPUT', formula: null, calcType: 'FORMULA', dependencies: '[]', dataSource: null, order: 13, isDefault: true, isEditable: true }, + { name: '应发合计', code: 'totalPay', type: 'CALCULATED', formula: 'baseSalary + positionSalary + performanceSalary + senioritySalary + overtimePay + transportAllowance + mealAllowance + housingAllowance + communicationAllowance + allowance + bonus - deduction - otherDeduction', calcType: 'FORMULA', dependencies: '["baseSalary","positionSalary","performanceSalary","senioritySalary","overtimePay","transportAllowance","mealAllowance","housingAllowance","communicationAllowance","allowance","bonus","deduction","otherDeduction"]', dataSource: null, order: 14, isDefault: true, isEditable: false }, + { name: '个人社保', code: 'socialEmp', type: 'CALCULATED', formula: 'SOCIAL_EMP', calcType: 'SYSTEM', dependencies: '[]', dataSource: null, order: 15, isDefault: true, isEditable: false }, + { name: '个人公积金', code: 'housingEmp', type: 'CALCULATED', formula: 'HOUSING_EMP', calcType: 'SYSTEM', dependencies: '[]', dataSource: null, order: 16, isDefault: true, isEditable: false }, + { name: '个人所得税', code: 'tax', type: 'CALCULATED', formula: 'TAX', calcType: 'SYSTEM', dependencies: '["totalPay","socialEmp","housingEmp"]', dataSource: null, order: 17, isDefault: true, isEditable: false }, + { name: '实发工资', code: 'netPay', type: 'CALCULATED', formula: 'totalPay - socialEmp - housingEmp - tax', calcType: 'FORMULA', dependencies: '["totalPay","socialEmp","housingEmp","tax"]', dataSource: null, order: 18, isDefault: true, isEditable: false }, ] export async function ensureDefaultTemplate(orgId: string) { @@ -95,11 +95,30 @@ export async function ensureDefaultTemplate(orgId: string) { data: DEFAULT_ITEMS.map(item => ({ ...item, orgId })), }) } else { - // 迁移:为已有记录补充 calcType 和 dependencies 字段(仅对缺失的预置项) + // 迁移:为已有记录补充 calcType、dependencies、dataSource 字段,并修正 overtimePay 类型 for (const item of DEFAULT_ITEMS) { const existing = await prisma.payslipItem.findFirst({ where: { orgId, code: item.code } }) - if (existing && (!existing.calcType || existing.calcType === 'FORMULA') && item.calcType === 'SYSTEM') { - await prisma.payslipItem.update({ where: { id: existing.id }, data: { calcType: 'SYSTEM', dependencies: item.dependencies } }) + if (!existing) continue + const updates: any = {} + // 补充 SYSTEM calcType + if ((!existing.calcType || existing.calcType === 'FORMULA') && item.calcType === 'SYSTEM') { + updates.calcType = 'SYSTEM' + updates.dependencies = item.dependencies + } + // 补充 dataSource + if (!existing.dataSource && item.dataSource) { + updates.dataSource = item.dataSource + } + // 修正 overtimePay:从 CALCULATED 改为 INPUT + if (item.code === 'overtimePay' && existing.type === 'CALCULATED') { + updates.type = 'INPUT' + updates.formula = null + updates.calcType = 'FORMULA' + updates.dependencies = '[]' + updates.isEditable = true + } + if (Object.keys(updates).length > 0) { + await prisma.payslipItem.update({ where: { id: existing.id }, data: updates }) } } } diff --git a/frontend/src/pages/money/BatchTab.tsx b/frontend/src/pages/money/BatchTab.tsx index d3fa294..7e806a3 100644 --- a/frontend/src/pages/money/BatchTab.tsx +++ b/frontend/src/pages/money/BatchTab.tsx @@ -897,55 +897,66 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void > 下载模板 - - - - - + {/* 根据模板 dataSource 动态显示获取按钮 */} + {(templateItems || []).some((t: any) => t.dataSource === 'overtime') && ( + + )} + {(templateItems || []).some((t: any) => t.dataSource === 'bonus') && ( + + )} + {(templateItems || []).some((t: any) => t.dataSource === 'performance') && ( + + )} + {(templateItems || []).some((t: any) => t.dataSource === 'deduction') && ( + <> + + + + )}