# 社保公积金优化方案 ## 核心原则 - 社保和公积金**完全分离**:独立配置、独立调基、独立增减员、独立申报 - 社保公积金开始/截止年月**必填**,增减变以此为准 - 基数缺省等于工资,可修改 - 调薪后社保公积金基数**不自动调整**(社保基数通常每年7月统一调基,调薪仅影响发薪基数) - 发薪列表和社保/公积金申报列表中,入离职日期与社保公积金年月不一致时**提醒** - 所有变更(入职/重新入职/调基/调薪/调部门/离职/解聘)都按**版本记录**保存,算薪和月度处理时按月份获取当前有效版本 --- ## 一、Schema 改动 ### 1.1 拆分配置模型 现有 `SocialInsuranceConfig`(含社保+公积金比例)拆为: - **`SocialInsuranceConfig`**(保留,移除公积金字段):养老/医疗/失业/工伤/生育比例 + 社保基数上下限 + 生效月份 + 版本管理 + `adjustmentDone` 标记 - **`HousingFundConfig`**(新增):公积金企业/个人比例 + 公积金基数上下限 + 生效月份 + 版本管理 + `adjustmentDone` 标记(字段结构同社保配置) > Organization 和 Employee 需增加反向关联字段: > - Organization: `housingFundConfigs HousingFundConfig[]`、`socialInsRecords EmployeeSocialInsRecord[]`、`housingFundRecords EmployeeHousingFundRecord[]`、`departmentRecords EmployeeDepartmentRecord[]`(`salaryChangeRecords` 已存在) > - Employee: `socialInsRecords EmployeeSocialInsRecord[]`、`housingFundRecords EmployeeHousingFundRecord[]`、`departmentRecords EmployeeDepartmentRecord[]`(`salaryChanges` 已存在) ### 1.2 新增模型:社保/公积金缴费记录(按版本保存) 社保和公积金的基数、起止年月不是 Employee 上的简单字段,而是按**版本记录**保存。每次入职/重新入职/调基/离职/解聘都生成新版本,形成完整变更历史。 #### EmployeeSocialInsRecord(社保缴费记录) ```prisma model EmployeeSocialInsRecord { 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) startMonth String // 开始缴费年月 YYYY-MM endMonth String? // 截止缴费年月 YYYY-MM(null=至今有效) base Float // 缴费基数 // 变更来源 changeType String // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘 changeRefId String? // 关联的 TerminationRecord ID(离职/解聘时) remark String? createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) @@index([employeeId, startMonth, endMonth]) // 复合索引:按员工+月份查询有效版本 } ``` #### EmployeeHousingFundRecord(公积金缴费记录) ```prisma model EmployeeHousingFundRecord { 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) startMonth String // 开始缴费年月 YYYY-MM endMonth String? // 截止缴费年月 YYYY-MM(null=至今有效) base Float // 缴费基数 // 变更来源 changeType String // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘 changeRefId String? // 关联的 TerminationRecord ID(离职/解聘时) remark String? createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) @@index([employeeId, startMonth, endMonth]) // 复合索引:按员工+月份查询有效版本 } ``` #### Employee 保留便捷字段(当前生效值,由后端同步维护) ``` socialInsStartMonth String? // 当前社保开始年月(=最新记录的startMonth) socialInsBase Float? // 当前社保基数(=最新记录的base) socialInsEndMonth String? // 当前社保截止年月(=最新记录的endMonth,null=在保) housingFundStartMonth String? // 当前公积金开始年月 housingFundBase Float? // 当前公积金基数 housingFundEndMonth String? // 当前公积金截止年月 ``` > 这些字段是冗余的便捷查询字段,由后端在创建/更新缴费记录时自动同步。增减员和在职申报查询主要使用 Record 表,发薪计算使用 Employee 便捷字段。 ### 1.3 TerminationRecord 增加字段 ``` socialInsEndMonth String // 社保截止缴费年月 YYYY-MM(必填) housingFundEndMonth String // 公积金截止缴费年月 YYYY-MM(必填) ``` > TerminationRecord 保存截止年月的同时,后端自动创建一条 EmployeeSocialInsRecord / EmployeeHousingFundRecord,将上一条有效记录的 endMonth 设为此值,并同步 Employee 便捷字段。 ### 1.4 扩展模型:调薪/调部门按版本保存 调薪和调部门也按**版本记录**保存,与社保公积金缴费记录同理。每次变更生成新版本,算薪和社保公积金月度处理时获取当前最新版。 #### 扩展现有 SalaryChangeRecord(增加版本字段) 现有 `SalaryChangeRecord` 已有 `oldSalary`/`newSalary`/`effectiveDate`/`reason`,与其新建模型,直接扩展: ```prisma // 在现有 SalaryChangeRecord 增加字段: effectiveMonth String // 生效年月 YYYY-MM(从 effectiveDate 转换) endMonth String? // 失效年月 YYYY-MM(null=至今有效,被新版本覆盖时设置) changeType String @default("SALARY_CHANGE") // ONBOARDING=入职, REHIRE=重新入职, SALARY_CHANGE=调薪 @@index([employeeId, effectiveMonth, endMonth]) // 复合索引 ``` > 不新建 `EmployeeSalaryRecord`,直接复用 `SalaryChangeRecord`,避免数据分散。入职时也创建一条(oldSalary=0, newSalary=月薪, changeType=ONBOARDING)。 #### EmployeeDepartmentRecord(部门变更记录,新增模型) ```prisma model EmployeeDepartmentRecord { 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) oldDepartment String // 调整前部门 newDepartment String // 调整后部门 effectiveMonth String // 生效年月 YYYY-MM endMonth String? // 失效年月 YYYY-MM(null=至今有效) reason String? // 调部门原因 changeType String // ONBOARDING=入职, REHIRE=重新入职, TRANSFER=调部门 createdBy String createdAt DateTime @default(now()) @@index([orgId, employeeId]) @@index([employeeId, effectiveMonth, endMonth]) // 复合索引 } ``` > Employee 上的 `monthlySalary` 和 `department` 作为便捷字段由后端同步维护。 ### 1.5 PayrollBatchType 增加枚举 ``` SEVERANCE // 补偿金按月发放(无社保,个税按政策处理) ``` ### 1.6 数据迁移策略 Schema 改动后,需要为现有员工创建初始 Record: - **EmployeeSocialInsRecord**:为每个现有员工创建一条,`startMonth` = 入职日期年月,`endMonth` = 已离职员工的离职日期年月(如有),`base` = 现有 `socialInsBase` 或月薪,`changeType` = 'ONBOARDING' - **EmployeeHousingFundRecord**:同上,`base` = 现有 `housingFundBase` 或月薪 - **SalaryChangeRecord**:为每个现有员工创建一条初始记录,`oldSalary` = 0, `newSalary` = 当前月薪, `effectiveMonth` = 入职日期年月, `endMonth` = null - **EmployeeDepartmentRecord**:为每个现有员工创建一条,`oldDepartment` = '', `newDepartment` = 当前部门, `effectiveMonth` = 入职日期年月, `endMonth` = null - **迁移脚本**:`npx prisma db push` 后执行一次性迁移脚本 `scripts/migrate-records.ts` --- ## 二、需求1:新增/重新入职填写社保公积金开始年月+基数 ### 前端 AddEmployeeModal - 新增4个必填字段(2列布局): - 社保开始年月(type=month,缺省=入职日期年月,可修改) - 社保基数(type=number,缺省=月薪,可修改) - 公积金开始年月(type=month,缺省=入职日期年月,可修改) - 公积金基数(type=number,缺省=月薪,可修改) - 当入职日期变更时(`handleHireDateChange`),自动同步4个缺省值 - `canSubmit` 增加这4个字段的必填校验 ### 前端 RehireModal - 同 AddEmployeeModal,缺省=新入职日期年月 ### 后端 - `createEmployeeSchema` 增加 `socialInsStartMonth`、`socialInsBase`、`housingFundStartMonth`、`housingFundBase`(必填) - `createEmployee` 存储这些字段到 Employee 便捷字段,**同时创建一条 `EmployeeSocialInsRecord`(changeType=ONBOARDING)和一条 `EmployeeHousingFundRecord`(changeType=ONBOARDING)** - `rehireEmployee` 接收并更新这些字段,**同时创建新版本缴费记录(changeType=REHIRE)**,并将之前有效记录的 endMonth 设为重新入职前一个月 --- ## 二.5 需求补充:花名册增加调薪/调部门操作 ### 前端花名册列表 - 每行操作区增加「调薪」「调部门」按钮(与「离职」并列) ### 前端调薪弹窗(SalaryChangeModal) - 显示:员工姓名、当前月薪、当前部门 - 输入: - 新月薪(必填,缺省=当前月薪) - 生效年月(type=month,必填,缺省=当月) - 调薪原因(选填) - 提交后: - 后端创建 `SalaryChangeRecord`(oldSalary=当前月薪,newSalary=新月薪,effectiveMonth=生效年月, changeType=SALARY_CHANGE) - 将之前有效记录的 `endMonth` 设为生效月前一个月 - 同步 `Employee.monthlySalary` = 新月薪 ### 前端调部门弹窗(DepartmentChangeModal) - 显示:员工姓名、当前部门 - 输入: - 新部门(必填,缺省=当前部门) - 生效年月(type=month,必填,缺省=当月) - 调部门原因(选填) - 提交后: - 后端创建 `EmployeeDepartmentRecord`(oldDepartment=当前部门,newDepartment=新部门,effectiveMonth=生效年月) - 将之前有效记录的 `endMonth` 设为生效月前一个月 - 同步 `Employee.department` = 新部门 ### 后端 - `POST /roster/:id/salary-change` — 调薪,创建版本记录 + 同步 Employee - `POST /roster/:id/department-change` — 调部门,创建版本记录 + 同步 Employee - `GET /roster/:id/salary-records` — 调薪历史 - `GET /roster/:id/department-records` — 调部门历史 ### 算薪和社保公积金月度处理 - 算薪时:根据发薪月份获取该月有效的 `SalaryChangeRecord`(`effectiveMonth <= month` 且 `endMonth == null 或 >= month`),使用该记录的 `newSalary` 作为发薪基数 - 社保公积金月度处理时:根据月份获取该月有效的 `EmployeeSocialInsRecord` / `EmployeeHousingFundRecord`,使用该记录的 `base` 作为缴费基数 - 部门信息:根据月份获取该月有效的 `EmployeeDepartmentRecord`,用于月度报表中的部门归属 - **调薪与社保基数关系**:调薪仅影响发薪基数,**不自动调整**社保公积金基数。社保公积金基数仅在每年7月统一调基时调整 --- ## 三、需求2:离职/解聘填写社保公积金截止年月 ### 前端 ResignModal(Roster.tsx) - 新增2个必填字段: - 社保截止年月(type=month,缺省=离职日期年月,可修改) - 公积金截止年月(type=month,缺省=离职日期年月,可修改) - 当离职日期变更时,自动同步缺省值 - `canSubmit` 增加必填校验 ### 前端 Termination.tsx(解聘向导 Step 1) - 在解聘日期下方增加社保截止年月、公积金截止年月输入 - 缺省=解聘日期年月,可修改 ### 后端 - `terminationChecklistSchema` 增加 `socialInsEndMonth`、`housingFundEndMonth`(必填) - `createTermination` 和 `createResignation` 存储这些字段到 TerminationRecord - **同时更新 Employee 便捷字段**(`socialInsEndMonth`、`housingFundEndMonth`) - **同时创建/更新缴费记录**:将当前有效记录的 `endMonth` 设为截止年月,同步 Employee 便捷字段 --- ## 四、需求3:社保公积金Tab增加月度增减员+在职申报+导出 ### 4.1 前端 SocialInsurance.tsx 改造 增加顶层 Tab 切换: - **「社保」Tab**:社保配置管理 + 社保调基 + 社保月度增减员 + 社保在职申报 - **「公积金」Tab**:公积金配置管理 + 公积金调基 + 公积金月度增减员 + 公积金在职申报 每个 Tab 内再分子 Tab: - 配置管理(现有功能,社保/公积金各自独立) - 月度增减员 - 在职申报 ### 4.2 月度增减员 **后端 API**: - `GET /social/monthly-changes?month=YYYY-MM` — 社保增减员 - `GET /housing/monthly-changes?month=YYYY-MM` — 公积金增减员 **逻辑**(统一使用 Record 表查询,确保历史月份也能查到已离职员工): - **增员**:查 `EmployeeSocialInsRecord.startMonth == month`(姓名、部门、基数、开始年月、changeType) - **减员**:查 `EmployeeSocialInsRecord.endMonth == month` 且 `changeType == 'TERMINATION'`(姓名、部门、基数、截止年月、离职类型) - 支持导出 CSV(前端生成,无需后端依赖) **前端**:选择月份 → 显示增员表和减员表(两个表格或折叠分区)→ 导出按钮 ### 4.3 在职申报 **后端 API**: - `GET /social/active-declaration?month=YYYY-MM` — 社保在保人员 - `GET /housing/active-declaration?month=YYYY-MM` — 公积金在保人员 **逻辑**(使用 Record 表查询): - 筛选条件:`EmployeeSocialInsRecord.startMonth <= month` 且 `endMonth == null 或 >= month` - 返回:姓名、身份证号、部门、社保基数、开始年月、截止年月 - 支持导出 CSV(前端生成,无需后端依赖) **前端**:选择月份 → 显示在保人员表格 → 导出按钮 ### 4.4 调基拆分 现有调基操作同时调整社保和公积金基数。改为: - 社保调基:只调整社保基数,使用 `SocialInsuranceConfig` 的上下限 - 将当前有效记录的 `endMonth` 设为调基月前一个月 - 创建新 `EmployeeSocialInsRecord`(changeType=ADJUST),startMonth=调基月,base=新基数 - 同步 Employee.socialInsBase / socialInsStartMonth - 公积金调基:只调整公积金基数,使用 `HousingFundConfig` 的上下限 - 将当前有效记录的 `endMonth` 设为调基月前一个月 - 创建新 `EmployeeHousingFundRecord`(changeType=ADJUST),startMonth=调基月,base=新基数 - 同步 Employee.housingFundBase / housingFundStartMonth - 两个调基操作独立执行,各自有 `adjustmentDone` 标记 --- ## 五、需求4:已离职员工按月发放补偿金 ### 后端 - `PayrollBatchType` 增加 `SEVERANCE` - `calcBatchEntry`:当 `batchType === 'SEVERANCE'` 时: - `socialEmp=0`、`housingEmp=0`、`socialOrg=0`、`housingOrg=0`(无社保公积金) - `tax`:经济补偿金在当地社平工资3倍以内免征个税,超过部分按单独税率计税。简化处理:`tax=0`,备注注明「补偿金免征个税(社平3倍以内)」,如超过3倍需手动计算 - 允许 `status === 'RESIGNED'` 的员工加入 `SEVERANCE` 批次 - 补偿金发放可设置**发放月数**(如约定发放6个月),到期后自动标记为已完成 - 也可手动停止发放 ### 前端 Money.tsx - 批次类型下拉增加「补偿金发放」选项 - `SEVERANCE` 批次:员工选择列表包含已离职员工 - 输入项简化:只有补偿金金额(baseSalary),无加班/津贴/扣款 - 可设置发放月数 - 工资条显示:社保=0、公积金=0、个税=0(备注:补偿金免征) --- ## 六、需求5:日期不一致提醒 ### 发薪列表提醒 在发薪批次详情中,对每个员工检查发薪月份与入离职日期的一致性: - 发薪月份 < 入职日期年月 → ⚠️ "该员工2025-07入职,当前发薪月份2025-06尚未入职" - 发薪月份 > 离职日期年月 → ⚠️ "该员工已于2025-06离职,当前发薪月份2025-07已离职" - 同时也检查社保公积金年月范围,如有不一致也提醒 ### 社保/公积金申报列表提醒 - **增员**:`socialInsStartMonth` 与 `hireDate` 年月不一致 → ⚠️ "社保开始年月与入职日期不一致" - **减员**:`socialInsEndMonth` 与 `terminationDate` 年月不一致 → ⚠️ "社保截止年月与离职日期不一致" - **在职申报**:`hireDate` 年月与 `socialInsStartMonth` 不一致、`terminationDate` 年月与 `socialInsEndMonth` 不一致 → ⚠️ 提醒 --- ## 七、实施顺序 | 步骤 | 内容 | 涉及 | |------|------|------| | 1 | Schema 改动(拆分配置、新增缴费/部门记录模型、扩展SalaryChangeRecord、增加字段、增加枚举)+ `prisma db push` | 后端 | | 1.5 | 数据迁移脚本:为现有员工创建初始 Record | 后端 | | 2 | 后端:`createEmployee`/`rehireEmployee` 接收社保公积金字段 + 创建缴费记录版本 | 后端 | | 3 | 后端:`createTermination`/`createResignation` 接收截止年月 + 更新缴费记录版本 | 后端 | | 3.5 | 后端:调薪/调部门 API + 创建/扩展版本记录 + 同步 Employee | 后端 | | 4 | 前端:AddEmployeeModal 增加社保公积金输入 | 前端 | | 5 | 前端:RehireModal 同步 | 前端 | | 6 | 前端:ResignModal 增加截止年月 | 前端 | | 6.5 | 前端:花名册增加调薪/调部门弹窗 | 前端 | | 7 | 前端:Termination.tsx 解聘向导增加截止年月 | 前端 | | 8 | 后端:月度增减员 + 在职申报 API | 后端 | | 9 | 后端:`SEVERANCE` 批次类型 + `calcBatchEntry` 修改 | 后端 | | 10 | 前端:SocialInsurance.tsx 改造(Tab拆分+增减员+申报+导出) | 前端 | | 11 | 前端:Money.tsx 增加补偿金批次 | 前端 | | 12 | 前端:日期不一致提醒 | 前端 | | 13 | 编译验证 + git 推送 | 全部 |