增加档案功能
This commit is contained in:
@@ -3,6 +3,7 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -68,15 +69,25 @@ func efIsValidDate(s string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// efNormalizeDate 将 ORM 读出的日期值统一为 "YYYY-MM-DD"。
|
||||
// DSN 开启 parseTime=True 后 DATE 列会被驱动解析为 time.Time,
|
||||
// 赋给 string 字段后是 Go 默认格式,取前 10 位(与 OA 日程一致)。
|
||||
func efNormalizeDate(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) >= 10 {
|
||||
return s[:10]
|
||||
// efNormalizeDate 将可空 DATE 列统一输出为 "YYYY-MM-DD"。
|
||||
func efNormalizeDate(value *time.Time) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
return value.Format("2006-01-02")
|
||||
}
|
||||
|
||||
// efDatePointer 将前端可选日期转为可空 DATE 值;空字符串明确保存为 SQL NULL。
|
||||
func efDatePointer(value string) *time.Time {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
date, err := time.ParseInLocation("2006-01-02", value, time.Local)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &date
|
||||
}
|
||||
|
||||
// efQuery 档案查询基座:强制附加租户过滤与未删除条件。
|
||||
@@ -204,6 +215,48 @@ type employeeFileDTO struct {
|
||||
Nation string `json:"nation"`
|
||||
HomeAddress string `json:"home_address"`
|
||||
AccountStatus int8 `json:"account_status"`
|
||||
HireDate string `json:"hire_date"`
|
||||
RegularDate string `json:"regular_date"`
|
||||
LeaveDate string `json:"leave_date"`
|
||||
Completeness int `json:"completeness"`
|
||||
}
|
||||
|
||||
// efFileCompleteness 计算档案资料完整度。仅统计应由档案模块维护的有效字段:
|
||||
// 离职日期仅在离职时要求,转正日期仅在正式员工时要求,备注为可选项不计入。
|
||||
func efFileCompleteness(file models.BackendEmployeeFile) int {
|
||||
items := []bool{
|
||||
strings.TrimSpace(file.IDCard) != "",
|
||||
strings.TrimSpace(file.PoliticalStatus) != "",
|
||||
file.MaritalStatus > 0,
|
||||
strings.TrimSpace(file.NativePlace) != "",
|
||||
strings.TrimSpace(file.HouseholdAddress) != "",
|
||||
strings.TrimSpace(file.CurrentAddress) != "",
|
||||
strings.TrimSpace(file.WorkEmail) != "",
|
||||
file.HireDate != nil,
|
||||
strings.TrimSpace(file.EmergencyContact) != "",
|
||||
strings.TrimSpace(file.EmergencyPhone) != "",
|
||||
strings.TrimSpace(file.EmergencyRelation) != "",
|
||||
strings.TrimSpace(file.EducationPhoto) != "",
|
||||
strings.TrimSpace(file.IDCardFront) != "",
|
||||
strings.TrimSpace(file.IDCardBack) != "",
|
||||
}
|
||||
if file.EmploymentStatus == 2 {
|
||||
items = append(items, file.RegularDate != nil)
|
||||
}
|
||||
if file.EmploymentStatus == 3 {
|
||||
items = append(items, file.LeaveDate != nil)
|
||||
}
|
||||
|
||||
filled := 0
|
||||
for _, item := range items {
|
||||
if item {
|
||||
filled++
|
||||
}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return 0
|
||||
}
|
||||
return filled * 100 / len(items)
|
||||
}
|
||||
|
||||
// efAssembleDTOList 档案列表批量组装员工展示信息。
|
||||
@@ -230,6 +283,7 @@ func (c *BackendEmployeeFileController) efAssembleDTOList(tid int, files []model
|
||||
|
||||
for _, f := range files {
|
||||
dto := employeeFileDTO{BackendEmployeeFile: f}
|
||||
dto.Completeness = efFileCompleteness(f)
|
||||
dto.HireDate = efNormalizeDate(f.HireDate)
|
||||
dto.RegularDate = efNormalizeDate(f.RegularDate)
|
||||
dto.LeaveDate = efNormalizeDate(f.LeaveDate)
|
||||
@@ -386,11 +440,6 @@ func (c *BackendEmployeeFileController) Detail() {
|
||||
if records == nil {
|
||||
records = []models.BackendEmployeeFileRecord{}
|
||||
}
|
||||
for i := range records {
|
||||
records[i].StartDate = efNormalizeDate(records[i].StartDate)
|
||||
records[i].EndDate = efNormalizeDate(records[i].EndDate)
|
||||
}
|
||||
|
||||
list := c.efAssembleDTOList(tid, []models.BackendEmployeeFile{file})
|
||||
c.efOk(map[string]interface{}{"file": list[0], "records": records})
|
||||
}
|
||||
@@ -507,6 +556,11 @@ func (c *BackendEmployeeFileController) Create() {
|
||||
fileNo = "EF" + time.Now().Format("20060102150405")
|
||||
}
|
||||
|
||||
// 证照资料只能在建档完成后,于档案详情中维护;创建接口不接受附件。
|
||||
payload.IDCardFront = ""
|
||||
payload.IDCardBack = ""
|
||||
payload.EducationPhoto = ""
|
||||
|
||||
now := time.Now()
|
||||
item := &models.BackendEmployeeFile{
|
||||
Tid: tid,
|
||||
@@ -525,9 +579,9 @@ func (c *BackendEmployeeFileController) Create() {
|
||||
EmergencyPhone: payload.EmergencyPhone,
|
||||
EmergencyRelation: payload.EmergencyRelation,
|
||||
WorkEmail: payload.WorkEmail,
|
||||
HireDate: strings.TrimSpace(payload.HireDate),
|
||||
RegularDate: strings.TrimSpace(payload.RegularDate),
|
||||
LeaveDate: strings.TrimSpace(payload.LeaveDate),
|
||||
HireDate: efDatePointer(payload.HireDate),
|
||||
RegularDate: efDatePointer(payload.RegularDate),
|
||||
LeaveDate: efDatePointer(payload.LeaveDate),
|
||||
EmploymentStatus: payload.EmploymentStatus,
|
||||
Remark: payload.Remark,
|
||||
IsDeleted: 0,
|
||||
@@ -535,7 +589,8 @@ func (c *BackendEmployeeFileController) Create() {
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
c.efErr(500, 500, "保存失败")
|
||||
log.Printf("员工档案创建失败: tid=%d employee_id=%d err=%v", tid, payload.EmployeeID, err)
|
||||
c.efErr(500, 500, "保存失败,请检查员工档案表字段及日期数据")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -572,9 +627,7 @@ func (c *BackendEmployeeFileController) Update() {
|
||||
now := time.Now()
|
||||
item.FileNo = payload.FileNo
|
||||
item.IDCard = payload.IDCard
|
||||
item.IDCardFront = payload.IDCardFront
|
||||
item.IDCardBack = payload.IDCardBack
|
||||
item.EducationPhoto = payload.EducationPhoto
|
||||
// 证照字段由 UpdateCertificates 独立维护,基础资料保存不得覆盖已有附件。
|
||||
item.PoliticalStatus = payload.PoliticalStatus
|
||||
item.MaritalStatus = payload.MaritalStatus
|
||||
item.NativePlace = payload.NativePlace
|
||||
@@ -584,20 +637,81 @@ func (c *BackendEmployeeFileController) Update() {
|
||||
item.EmergencyPhone = payload.EmergencyPhone
|
||||
item.EmergencyRelation = payload.EmergencyRelation
|
||||
item.WorkEmail = payload.WorkEmail
|
||||
item.HireDate = strings.TrimSpace(payload.HireDate)
|
||||
item.RegularDate = strings.TrimSpace(payload.RegularDate)
|
||||
item.LeaveDate = strings.TrimSpace(payload.LeaveDate)
|
||||
item.HireDate = efDatePointer(payload.HireDate)
|
||||
item.RegularDate = efDatePointer(payload.RegularDate)
|
||||
item.LeaveDate = efDatePointer(payload.LeaveDate)
|
||||
item.EmploymentStatus = payload.EmploymentStatus
|
||||
item.Remark = payload.Remark
|
||||
item.UpdateTime = &now
|
||||
|
||||
if _, err := models.Orm.Update(&item,
|
||||
"FileNo", "IDCard", "IDCardFront", "IDCardBack", "EducationPhoto",
|
||||
"PoliticalStatus", "MaritalStatus", "NativePlace",
|
||||
"FileNo", "IDCard", "PoliticalStatus", "MaritalStatus", "NativePlace",
|
||||
"HouseholdAddress", "CurrentAddress", "EmergencyContact", "EmergencyPhone",
|
||||
"EmergencyRelation", "WorkEmail", "HireDate", "RegularDate", "LeaveDate",
|
||||
"EmploymentStatus", "Remark", "UpdateTime"); err != nil {
|
||||
c.efErr(500, 500, "保存失败")
|
||||
log.Printf("员工档案更新失败: tid=%d file_id=%d err=%v", tid, id, err)
|
||||
c.efErr(500, 500, "保存失败,请检查员工档案表字段及日期数据")
|
||||
return
|
||||
}
|
||||
|
||||
c.efOk(item)
|
||||
}
|
||||
|
||||
// employeeFileCertificatesPayload 证照资料单独更新请求体。
|
||||
// 该接口仅在档案详情中使用,避免上传附件时覆盖基础资料。
|
||||
type employeeFileCertificatesPayload struct {
|
||||
IDCardFront string `json:"id_card_front"`
|
||||
IDCardBack string `json:"id_card_back"`
|
||||
EducationPhoto string `json:"education_photo"`
|
||||
}
|
||||
|
||||
// UpdateCertificates POST /backend/oa/employeefile/certificates/:id
|
||||
// 仅更新学历照片、身份证正反面;档案必须已创建,不能通过建档接口上传。
|
||||
func (c *BackendEmployeeFileController) UpdateCertificates() {
|
||||
claims, err := c.efClaims()
|
||||
if err != nil {
|
||||
c.efErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.efErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var payload employeeFileCertificatesPayload
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil || json.Unmarshal(raw, &payload) != nil {
|
||||
c.efErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
for label, url := range map[string]string{
|
||||
"身份证正面": payload.IDCardFront,
|
||||
"身份证反面": payload.IDCardBack,
|
||||
"学历照片": payload.EducationPhoto,
|
||||
} {
|
||||
if len(url) > 500 {
|
||||
c.efErr(400, 400, label+"地址过长")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var item models.BackendEmployeeFile
|
||||
if err := c.efQuery(tid).Filter("id", id).One(&item); err != nil {
|
||||
c.efErr(404, 404, "档案不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item.IDCardFront = strings.TrimSpace(payload.IDCardFront)
|
||||
item.IDCardBack = strings.TrimSpace(payload.IDCardBack)
|
||||
item.EducationPhoto = strings.TrimSpace(payload.EducationPhoto)
|
||||
item.UpdateTime = &now
|
||||
if _, err := models.Orm.Update(&item,
|
||||
"IDCardFront", "IDCardBack", "EducationPhoto", "UpdateTime"); err != nil {
|
||||
c.efErr(500, 500, "证照保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -743,8 +857,8 @@ func (c *BackendEmployeeFileController) CreateRecord() {
|
||||
Title: payload.Title,
|
||||
SubTitle: payload.SubTitle,
|
||||
Extra: payload.Extra,
|
||||
StartDate: payload.StartDate,
|
||||
EndDate: payload.EndDate,
|
||||
StartDate: efDatePointer(payload.StartDate),
|
||||
EndDate: efDatePointer(payload.EndDate),
|
||||
Description: payload.Description,
|
||||
AttachmentURL: payload.AttachmentURL,
|
||||
Sort: payload.Sort,
|
||||
@@ -795,8 +909,8 @@ func (c *BackendEmployeeFileController) UpdateRecord() {
|
||||
item.Title = payload.Title
|
||||
item.SubTitle = payload.SubTitle
|
||||
item.Extra = payload.Extra
|
||||
item.StartDate = payload.StartDate
|
||||
item.EndDate = payload.EndDate
|
||||
item.StartDate = efDatePointer(payload.StartDate)
|
||||
item.EndDate = efDatePointer(payload.EndDate)
|
||||
item.Description = payload.Description
|
||||
item.AttachmentURL = payload.AttachmentURL
|
||||
item.Sort = payload.Sort
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendOaCompensationController OA 薪酬管理接口。
|
||||
// 数据按租户隔离;工资金额使用薪资单快照,避免组织资料或历史方案变动影响已生成工资。
|
||||
type BackendOaCompensationController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendOaCompensationController) compensationClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if auth == "" || len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil || claims.UserType != "backend" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendOaCompensationController) compensationError(httpStatus int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": httpStatus, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendOaCompensationController) compensationOK(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func compensationID(raw string) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(raw, 10, 64)
|
||||
return id, err == nil && id > 0
|
||||
}
|
||||
|
||||
func compensationMoney(v float64) float64 {
|
||||
return math.Round(v*100) / 100
|
||||
}
|
||||
|
||||
func compensationString(v *string) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
var compensationMonthPattern = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])$`)
|
||||
|
||||
type compensationItemPayload struct {
|
||||
ID uint64 `json:"id"`
|
||||
ItemName string `json:"item_name"`
|
||||
ItemType int8 `json:"item_type"` // 1 增项;2 扣项
|
||||
Amount float64 `json:"amount"`
|
||||
Remark string `json:"remark"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
type compensationSchemePayload struct {
|
||||
EmployeeID uint64 `json:"employee_id"`
|
||||
SchemeName string `json:"scheme_name"`
|
||||
EffectiveDate string `json:"effective_date"`
|
||||
ExpiryDate string `json:"expiry_date"`
|
||||
BaseSalary float64 `json:"base_salary"`
|
||||
PostAllowance float64 `json:"post_allowance"`
|
||||
PerformanceSalary float64 `json:"performance_salary"`
|
||||
TransportAllowance float64 `json:"transport_allowance"`
|
||||
MealAllowance float64 `json:"meal_allowance"`
|
||||
CommunicationAllowance float64 `json:"communication_allowance"`
|
||||
SocialInsuranceBase float64 `json:"social_insurance_base"`
|
||||
HousingFundBase float64 `json:"housing_fund_base"`
|
||||
SocialInsuranceRate float64 `json:"social_insurance_rate"`
|
||||
HousingFundRate float64 `json:"housing_fund_rate"`
|
||||
Status int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type compensationPayrollPayload struct {
|
||||
EmployeeID uint64 `json:"employee_id"`
|
||||
SchemeID *uint64 `json:"scheme_id"`
|
||||
PayrollMonth string `json:"payroll_month"`
|
||||
BaseSalary float64 `json:"base_salary"`
|
||||
PostAllowance float64 `json:"post_allowance"`
|
||||
PerformanceSalary float64 `json:"performance_salary"`
|
||||
TransportAllowance float64 `json:"transport_allowance"`
|
||||
MealAllowance float64 `json:"meal_allowance"`
|
||||
CommunicationAllowance float64 `json:"communication_allowance"`
|
||||
OvertimePay float64 `json:"overtime_pay"`
|
||||
Bonus float64 `json:"bonus"`
|
||||
OtherAddition float64 `json:"other_addition"`
|
||||
LeaveDeduction float64 `json:"leave_deduction"`
|
||||
LateDeduction float64 `json:"late_deduction"`
|
||||
OtherDeduction float64 `json:"other_deduction"`
|
||||
SocialInsurance float64 `json:"social_insurance"`
|
||||
HousingFund float64 `json:"housing_fund"`
|
||||
PersonalIncomeTax float64 `json:"personal_income_tax"`
|
||||
Remark string `json:"remark"`
|
||||
Items []compensationItemPayload `json:"items"`
|
||||
}
|
||||
|
||||
func (c *BackendOaCompensationController) parseBody(target interface{}) bool {
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil || json.Unmarshal(raw, target) != nil {
|
||||
c.compensationError(400, "参数格式错误")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validCompensationDate(s string) (time.Time, bool) {
|
||||
s = strings.TrimSpace(s)
|
||||
value, err := time.ParseInLocation("2006-01-02", s, time.Local)
|
||||
return value, err == nil
|
||||
}
|
||||
|
||||
func validateNonNegative(values ...float64) bool {
|
||||
for _, value := range values {
|
||||
if value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *BackendOaCompensationController) employee(tenantID uint64, employeeID uint64) (*models.BackendEmployee, bool) {
|
||||
var employee models.BackendEmployee
|
||||
err := models.Orm.QueryTable(new(models.BackendEmployee)).
|
||||
Filter("id", employeeID).
|
||||
Filter("tid", tenantID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&employee)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &employee, true
|
||||
}
|
||||
|
||||
func payrollMap(row models.BackendOaPayroll) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID, "employee_id": row.EmployeeID, "scheme_id": row.SchemeID,
|
||||
"payroll_month": row.PayrollMonth, "employee_name": row.EmployeeName,
|
||||
"department": row.Department, "position": row.Position,
|
||||
"base_salary": row.BaseSalary, "post_allowance": row.PostAllowance,
|
||||
"performance_salary": row.PerformanceSalary, "transport_allowance": row.TransportAllowance,
|
||||
"meal_allowance": row.MealAllowance, "communication_allowance": row.CommunicationAllowance,
|
||||
"overtime_pay": row.OvertimePay, "bonus": row.Bonus, "other_addition": row.OtherAddition,
|
||||
"leave_deduction": row.LeaveDeduction, "late_deduction": row.LateDeduction,
|
||||
"other_deduction": row.OtherDeduction, "social_insurance": row.SocialInsurance,
|
||||
"housing_fund": row.HousingFund, "personal_income_tax": row.PersonalIncomeTax,
|
||||
"gross_salary": row.GrossSalary, "total_deduction": row.TotalDeduction,
|
||||
"net_salary": row.NetSalary, "status": row.Status, "confirmed_at": row.ConfirmedAt,
|
||||
"paid_at": row.PaidAt, "remark": row.Remark, "create_time": row.CreateTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BackendOaCompensationController) fillPayroll(row *models.BackendOaPayroll, payload compensationPayrollPayload, employee *models.BackendEmployee) {
|
||||
row.SchemeID = payload.SchemeID
|
||||
row.BaseSalary = compensationMoney(payload.BaseSalary)
|
||||
row.PostAllowance = compensationMoney(payload.PostAllowance)
|
||||
row.PerformanceSalary = compensationMoney(payload.PerformanceSalary)
|
||||
row.TransportAllowance = compensationMoney(payload.TransportAllowance)
|
||||
row.MealAllowance = compensationMoney(payload.MealAllowance)
|
||||
row.CommunicationAllowance = compensationMoney(payload.CommunicationAllowance)
|
||||
row.OvertimePay = compensationMoney(payload.OvertimePay)
|
||||
row.Bonus = compensationMoney(payload.Bonus)
|
||||
row.OtherAddition = compensationMoney(payload.OtherAddition)
|
||||
row.LeaveDeduction = compensationMoney(payload.LeaveDeduction)
|
||||
row.LateDeduction = compensationMoney(payload.LateDeduction)
|
||||
row.OtherDeduction = compensationMoney(payload.OtherDeduction)
|
||||
row.SocialInsurance = compensationMoney(payload.SocialInsurance)
|
||||
row.HousingFund = compensationMoney(payload.HousingFund)
|
||||
row.PersonalIncomeTax = compensationMoney(payload.PersonalIncomeTax)
|
||||
row.Remark = strings.TrimSpace(payload.Remark)
|
||||
row.EmployeeName = employee.Name
|
||||
row.Department = compensationString(employee.Department)
|
||||
row.Position = compensationString(employee.Position)
|
||||
|
||||
customAdd, customDeduct := 0.0, 0.0
|
||||
for _, item := range payload.Items {
|
||||
if item.ItemType == 1 {
|
||||
customAdd += item.Amount
|
||||
} else if item.ItemType == 2 {
|
||||
customDeduct += item.Amount
|
||||
}
|
||||
}
|
||||
row.GrossSalary = compensationMoney(row.BaseSalary + row.PostAllowance + row.PerformanceSalary +
|
||||
row.TransportAllowance + row.MealAllowance + row.CommunicationAllowance + row.OvertimePay +
|
||||
row.Bonus + row.OtherAddition + customAdd)
|
||||
row.TotalDeduction = compensationMoney(row.LeaveDeduction + row.LateDeduction + row.OtherDeduction +
|
||||
row.SocialInsurance + row.HousingFund + row.PersonalIncomeTax + customDeduct)
|
||||
row.NetSalary = compensationMoney(row.GrossSalary - row.TotalDeduction)
|
||||
}
|
||||
|
||||
func (c *BackendOaCompensationController) saveItems(tenantID, payrollID uint64, payloads []compensationItemPayload) error {
|
||||
_, err := models.Orm.QueryTable(new(models.BackendOaPayrollItem)).
|
||||
Filter("tid", tenantID).Filter("payroll_id", payrollID).Filter("is_deleted", 0).
|
||||
Update(orm.Params{"is_deleted": int8(1), "delete_time": time.Now()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index, payload := range payloads {
|
||||
name := strings.TrimSpace(payload.ItemName)
|
||||
if name == "" || (payload.ItemType != 1 && payload.ItemType != 2) || !validateNonNegative(payload.Amount) {
|
||||
return orm.ErrArgs
|
||||
}
|
||||
item := &models.BackendOaPayrollItem{
|
||||
Tid: tenantID, PayrollID: payrollID, ItemName: name, ItemType: payload.ItemType,
|
||||
Amount: compensationMoney(payload.Amount), Remark: strings.TrimSpace(payload.Remark),
|
||||
SortOrder: index, IsDeleted: 0,
|
||||
}
|
||||
if _, err = models.Orm.Insert(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Employees GET /backend/oa/compensation/employees
|
||||
func (c *BackendOaCompensationController) Employees() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
qs := models.Orm.QueryTable(new(models.BackendEmployee)).Filter("tid", claims.TenantId).Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("name__icontains", keyword)
|
||||
}
|
||||
var employees []models.BackendEmployee
|
||||
_, err = qs.OrderBy("name", "id").All(&employees)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.compensationError(500, "员工查询失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(employees))
|
||||
for _, employee := range employees {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": employee.ID, "name": employee.Name, "account": employee.Account,
|
||||
"department": compensationString(employee.Department), "position": compensationString(employee.Position),
|
||||
})
|
||||
}
|
||||
c.compensationOK(list)
|
||||
}
|
||||
|
||||
// SchemeList GET /backend/oa/compensation/schemes
|
||||
func (c *BackendOaCompensationController) SchemeList() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
employeeID, _ := c.GetInt64("employee_id", 0)
|
||||
qs := models.Orm.QueryTable(new(models.BackendOaCompensationScheme)).Filter("tid", claims.TenantId).Filter("is_deleted", 0)
|
||||
if employeeID > 0 {
|
||||
qs = qs.Filter("employee_id", employeeID)
|
||||
}
|
||||
var rows []models.BackendOaCompensationScheme
|
||||
_, err = qs.OrderBy("-effective_date", "-id").All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.compensationError(500, "查询薪酬方案失败")
|
||||
return
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []models.BackendOaCompensationScheme{}
|
||||
}
|
||||
c.compensationOK(rows)
|
||||
}
|
||||
|
||||
// CreateScheme POST /backend/oa/compensation/schemes
|
||||
func (c *BackendOaCompensationController) CreateScheme() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
var payload compensationSchemePayload
|
||||
if !c.parseBody(&payload) {
|
||||
return
|
||||
}
|
||||
date, ok := validCompensationDate(payload.EffectiveDate)
|
||||
if !ok || payload.EmployeeID == 0 || strings.TrimSpace(payload.SchemeName) == "" {
|
||||
c.compensationError(400, "请填写员工、方案名称和生效日期")
|
||||
return
|
||||
}
|
||||
employee, ok := c.employee(uint64(claims.TenantId), payload.EmployeeID)
|
||||
if !ok || employee == nil {
|
||||
c.compensationError(400, "员工不存在或不属于当前租户")
|
||||
return
|
||||
}
|
||||
if !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance,
|
||||
payload.MealAllowance, payload.CommunicationAllowance, payload.SocialInsuranceBase, payload.HousingFundBase,
|
||||
payload.SocialInsuranceRate, payload.HousingFundRate) || payload.SocialInsuranceRate > 1 || payload.HousingFundRate > 1 {
|
||||
c.compensationError(400, "薪酬金额或比例无效")
|
||||
return
|
||||
}
|
||||
var expiry *time.Time
|
||||
if strings.TrimSpace(payload.ExpiryDate) != "" {
|
||||
value, valid := validCompensationDate(payload.ExpiryDate)
|
||||
if !valid || value.Before(date) {
|
||||
c.compensationError(400, "失效日期无效")
|
||||
return
|
||||
}
|
||||
expiry = &value
|
||||
}
|
||||
status := payload.Status
|
||||
if status != 0 {
|
||||
status = 1
|
||||
}
|
||||
row := &models.BackendOaCompensationScheme{
|
||||
Tid: uint64(claims.TenantId), EmployeeID: payload.EmployeeID, SchemeName: strings.TrimSpace(payload.SchemeName),
|
||||
EffectiveDate: date, ExpiryDate: expiry, BaseSalary: compensationMoney(payload.BaseSalary),
|
||||
PostAllowance: compensationMoney(payload.PostAllowance), PerformanceSalary: compensationMoney(payload.PerformanceSalary),
|
||||
TransportAllowance: compensationMoney(payload.TransportAllowance), MealAllowance: compensationMoney(payload.MealAllowance),
|
||||
CommunicationAllowance: compensationMoney(payload.CommunicationAllowance), SocialInsuranceBase: compensationMoney(payload.SocialInsuranceBase),
|
||||
HousingFundBase: compensationMoney(payload.HousingFundBase), SocialInsuranceRate: payload.SocialInsuranceRate,
|
||||
HousingFundRate: payload.HousingFundRate, Status: status, Remark: strings.TrimSpace(payload.Remark),
|
||||
}
|
||||
if _, err = models.Orm.Insert(row); err != nil {
|
||||
c.compensationError(500, "保存薪酬方案失败")
|
||||
return
|
||||
}
|
||||
c.compensationOK(row)
|
||||
}
|
||||
|
||||
// UpdateScheme POST /backend/oa/compensation/schemes/:id
|
||||
func (c *BackendOaCompensationController) UpdateScheme() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, valid := compensationID(c.Ctx.Input.Param(":id"))
|
||||
if !valid {
|
||||
c.compensationError(400, "无效方案ID")
|
||||
return
|
||||
}
|
||||
var payload compensationSchemePayload
|
||||
if !c.parseBody(&payload) {
|
||||
return
|
||||
}
|
||||
var row models.BackendOaCompensationScheme
|
||||
if models.Orm.QueryTable(new(models.BackendOaCompensationScheme)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil {
|
||||
c.compensationError(404, "薪酬方案不存在")
|
||||
return
|
||||
}
|
||||
// 保持更新逻辑与创建一致,先软删除旧值再由统一创建验证的字段约束赋值。
|
||||
date, ok := validCompensationDate(payload.EffectiveDate)
|
||||
if !ok || payload.EmployeeID == 0 || strings.TrimSpace(payload.SchemeName) == "" || !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance, payload.MealAllowance, payload.CommunicationAllowance, payload.SocialInsuranceBase, payload.HousingFundBase, payload.SocialInsuranceRate, payload.HousingFundRate) || payload.SocialInsuranceRate > 1 || payload.HousingFundRate > 1 {
|
||||
c.compensationError(400, "方案内容无效")
|
||||
return
|
||||
}
|
||||
if _, ok = c.employee(uint64(claims.TenantId), payload.EmployeeID); !ok {
|
||||
c.compensationError(400, "员工不存在或不属于当前租户")
|
||||
return
|
||||
}
|
||||
var expiry *time.Time
|
||||
if strings.TrimSpace(payload.ExpiryDate) != "" {
|
||||
value, dateOK := validCompensationDate(payload.ExpiryDate)
|
||||
if !dateOK || value.Before(date) {
|
||||
c.compensationError(400, "失效日期无效")
|
||||
return
|
||||
}
|
||||
expiry = &value
|
||||
}
|
||||
now := time.Now()
|
||||
row.EmployeeID, row.SchemeName, row.EffectiveDate, row.ExpiryDate = payload.EmployeeID, strings.TrimSpace(payload.SchemeName), date, expiry
|
||||
row.BaseSalary, row.PostAllowance, row.PerformanceSalary = compensationMoney(payload.BaseSalary), compensationMoney(payload.PostAllowance), compensationMoney(payload.PerformanceSalary)
|
||||
row.TransportAllowance, row.MealAllowance, row.CommunicationAllowance = compensationMoney(payload.TransportAllowance), compensationMoney(payload.MealAllowance), compensationMoney(payload.CommunicationAllowance)
|
||||
row.SocialInsuranceBase, row.HousingFundBase, row.SocialInsuranceRate, row.HousingFundRate = compensationMoney(payload.SocialInsuranceBase), compensationMoney(payload.HousingFundBase), payload.SocialInsuranceRate, payload.HousingFundRate
|
||||
row.Status, row.Remark, row.UpdateTime = payload.Status, strings.TrimSpace(payload.Remark), &now
|
||||
if row.Status != 0 {
|
||||
row.Status = 1
|
||||
}
|
||||
if _, err = models.Orm.Update(&row); err != nil {
|
||||
c.compensationError(500, "更新薪酬方案失败")
|
||||
return
|
||||
}
|
||||
c.compensationOK(row)
|
||||
}
|
||||
|
||||
// DeleteScheme DELETE /backend/oa/compensation/schemes/:id
|
||||
func (c *BackendOaCompensationController) DeleteScheme() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, valid := compensationID(c.Ctx.Input.Param(":id"))
|
||||
if !valid {
|
||||
c.compensationError(400, "无效方案ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
count, err := models.Orm.QueryTable(new(models.BackendOaCompensationScheme)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).Update(orm.Params{"is_deleted": int8(1), "delete_time": now, "update_time": now})
|
||||
if err != nil || count == 0 {
|
||||
c.compensationError(404, "薪酬方案不存在")
|
||||
return
|
||||
}
|
||||
c.compensationOK(nil)
|
||||
}
|
||||
|
||||
// List GET /backend/oa/compensation/payrolls
|
||||
func (c *BackendOaCompensationController) List() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 200 {
|
||||
pageSize = 20
|
||||
}
|
||||
month, keyword := strings.TrimSpace(c.GetString("payroll_month")), strings.TrimSpace(c.GetString("keyword"))
|
||||
status := strings.TrimSpace(c.GetString("status"))
|
||||
qs := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("is_deleted", 0)
|
||||
if compensationMonthPattern.MatchString(month) {
|
||||
qs = qs.Filter("payroll_month", month)
|
||||
}
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("employee_name__icontains", keyword)
|
||||
}
|
||||
if status == "0" || status == "1" || status == "2" || status == "3" {
|
||||
value, _ := strconv.Atoi(status)
|
||||
qs = qs.Filter("status", int8(value))
|
||||
}
|
||||
total, _ := qs.Count()
|
||||
var rows []models.BackendOaPayroll
|
||||
_, err = qs.OrderBy("-payroll_month", "employee_name", "-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
log.Printf("薪资单查询失败: tid=%d month=%q keyword=%q status=%q err=%v", claims.TenantId, month, keyword, status, err)
|
||||
c.compensationError(500, "查询薪资单失败,请检查薪酬管理数据表是否已创建")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, payrollMap(row))
|
||||
}
|
||||
c.compensationOK(map[string]interface{}{"list": list, "total": total})
|
||||
}
|
||||
|
||||
// Detail GET /backend/oa/compensation/payrolls/:id
|
||||
func (c *BackendOaCompensationController) Detail() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, valid := compensationID(c.Ctx.Input.Param(":id"))
|
||||
if !valid {
|
||||
c.compensationError(400, "无效薪资单ID")
|
||||
return
|
||||
}
|
||||
var row models.BackendOaPayroll
|
||||
if models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil {
|
||||
c.compensationError(404, "薪资单不存在")
|
||||
return
|
||||
}
|
||||
var items []models.BackendOaPayrollItem
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendOaPayrollItem)).Filter("tid", claims.TenantId).Filter("payroll_id", id).Filter("is_deleted", 0).OrderBy("sort_order", "id").All(&items)
|
||||
data := payrollMap(row)
|
||||
data["items"] = items
|
||||
c.compensationOK(data)
|
||||
}
|
||||
|
||||
// Create POST /backend/oa/compensation/payrolls
|
||||
func (c *BackendOaCompensationController) Create() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
var payload compensationPayrollPayload
|
||||
if !c.parseBody(&payload) {
|
||||
return
|
||||
}
|
||||
if payload.EmployeeID == 0 || !compensationMonthPattern.MatchString(strings.TrimSpace(payload.PayrollMonth)) || !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance, payload.MealAllowance, payload.CommunicationAllowance, payload.OvertimePay, payload.Bonus, payload.OtherAddition, payload.LeaveDeduction, payload.LateDeduction, payload.OtherDeduction, payload.SocialInsurance, payload.HousingFund, payload.PersonalIncomeTax) {
|
||||
c.compensationError(400, "员工、工资月份或金额无效")
|
||||
return
|
||||
}
|
||||
employee, ok := c.employee(uint64(claims.TenantId), payload.EmployeeID)
|
||||
if !ok {
|
||||
c.compensationError(400, "员工不存在或不属于当前租户")
|
||||
return
|
||||
}
|
||||
exists := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("employee_id", payload.EmployeeID).Filter("payroll_month", payload.PayrollMonth).Filter("is_deleted", 0).Exist()
|
||||
if exists {
|
||||
c.compensationError(409, "该员工本月薪资单已存在")
|
||||
return
|
||||
}
|
||||
row := &models.BackendOaPayroll{Tid: uint64(claims.TenantId), EmployeeID: payload.EmployeeID, PayrollMonth: strings.TrimSpace(payload.PayrollMonth), Status: 0}
|
||||
c.fillPayroll(row, payload, employee)
|
||||
if _, err = models.Orm.Insert(row); err != nil {
|
||||
c.compensationError(500, "创建薪资单失败")
|
||||
return
|
||||
}
|
||||
if err = c.saveItems(uint64(claims.TenantId), row.ID, payload.Items); err != nil {
|
||||
c.compensationError(400, "自定义薪资项无效")
|
||||
return
|
||||
}
|
||||
c.compensationOK(payrollMap(*row))
|
||||
}
|
||||
|
||||
// Update POST /backend/oa/compensation/payrolls/:id
|
||||
func (c *BackendOaCompensationController) Update() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, valid := compensationID(c.Ctx.Input.Param(":id"))
|
||||
if !valid {
|
||||
c.compensationError(400, "无效薪资单ID")
|
||||
return
|
||||
}
|
||||
var payload compensationPayrollPayload
|
||||
if !c.parseBody(&payload) {
|
||||
return
|
||||
}
|
||||
var row models.BackendOaPayroll
|
||||
if models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil {
|
||||
c.compensationError(404, "薪资单不存在")
|
||||
return
|
||||
}
|
||||
if row.Status != 0 {
|
||||
c.compensationError(400, "仅草稿薪资单可以编辑")
|
||||
return
|
||||
}
|
||||
if payload.EmployeeID == 0 || !compensationMonthPattern.MatchString(strings.TrimSpace(payload.PayrollMonth)) || !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance, payload.MealAllowance, payload.CommunicationAllowance, payload.OvertimePay, payload.Bonus, payload.OtherAddition, payload.LeaveDeduction, payload.LateDeduction, payload.OtherDeduction, payload.SocialInsurance, payload.HousingFund, payload.PersonalIncomeTax) {
|
||||
c.compensationError(400, "薪资单内容无效")
|
||||
return
|
||||
}
|
||||
employee, ok := c.employee(uint64(claims.TenantId), payload.EmployeeID)
|
||||
if !ok {
|
||||
c.compensationError(400, "员工不存在")
|
||||
return
|
||||
}
|
||||
exists := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("employee_id", payload.EmployeeID).Filter("payroll_month", payload.PayrollMonth).Filter("is_deleted", 0).Filter("id__ne", id).Exist()
|
||||
if exists {
|
||||
c.compensationError(409, "该员工本月薪资单已存在")
|
||||
return
|
||||
}
|
||||
row.EmployeeID, row.PayrollMonth = payload.EmployeeID, strings.TrimSpace(payload.PayrollMonth)
|
||||
c.fillPayroll(&row, payload, employee)
|
||||
now := time.Now()
|
||||
row.UpdateTime = &now
|
||||
if _, err = models.Orm.Update(&row); err != nil {
|
||||
c.compensationError(500, "更新薪资单失败")
|
||||
return
|
||||
}
|
||||
if err = c.saveItems(uint64(claims.TenantId), row.ID, payload.Items); err != nil {
|
||||
c.compensationError(400, "自定义薪资项无效")
|
||||
return
|
||||
}
|
||||
c.compensationOK(payrollMap(row))
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/oa/compensation/payrolls/:id
|
||||
func (c *BackendOaCompensationController) Delete() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, valid := compensationID(c.Ctx.Input.Param(":id"))
|
||||
if !valid {
|
||||
c.compensationError(400, "无效薪资单ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
count, err := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("status", 0).Filter("is_deleted", 0).Update(orm.Params{"is_deleted": int8(1), "delete_time": now, "update_time": now})
|
||||
if err != nil || count == 0 {
|
||||
c.compensationError(400, "薪资单不存在或当前状态不可删除")
|
||||
return
|
||||
}
|
||||
c.compensationOK(nil)
|
||||
}
|
||||
|
||||
// UpdateStatus POST /backend/oa/compensation/payrolls/:id/status
|
||||
func (c *BackendOaCompensationController) UpdateStatus() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
id, valid := compensationID(c.Ctx.Input.Param(":id"))
|
||||
if !valid {
|
||||
c.compensationError(400, "无效薪资单ID")
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
if !c.parseBody(&payload) {
|
||||
return
|
||||
}
|
||||
if payload.Status < 0 || payload.Status > 3 {
|
||||
c.compensationError(400, "无效状态")
|
||||
return
|
||||
}
|
||||
var row models.BackendOaPayroll
|
||||
if models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil {
|
||||
c.compensationError(404, "薪资单不存在")
|
||||
return
|
||||
}
|
||||
if row.Status == 2 && payload.Status != 2 {
|
||||
c.compensationError(400, "已发放薪资单不可修改状态")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row.Status, row.UpdateTime = payload.Status, &now
|
||||
if payload.Status == 1 {
|
||||
row.ConfirmedAt = &now
|
||||
}
|
||||
if payload.Status == 2 {
|
||||
row.PaidAt = &now
|
||||
}
|
||||
if _, err = models.Orm.Update(&row, "Status", "ConfirmedAt", "PaidAt", "UpdateTime"); err != nil {
|
||||
c.compensationError(500, "更新状态失败")
|
||||
return
|
||||
}
|
||||
c.compensationOK(payrollMap(row))
|
||||
}
|
||||
|
||||
// Dashboard GET /backend/oa/compensation/dashboard
|
||||
func (c *BackendOaCompensationController) Dashboard() {
|
||||
claims, err := c.compensationClaims()
|
||||
if err != nil {
|
||||
c.compensationError(401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
month := strings.TrimSpace(c.GetString("payroll_month"))
|
||||
if !compensationMonthPattern.MatchString(month) {
|
||||
month = time.Now().Format("2006-01")
|
||||
}
|
||||
var rows []models.BackendOaPayroll
|
||||
_, err = models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("payroll_month", month).Filter("is_deleted", 0).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
log.Printf("薪酬仪表盘统计失败: tid=%d month=%q err=%v", claims.TenantId, month, err)
|
||||
c.compensationError(500, "统计失败,请检查薪酬管理数据表是否已创建")
|
||||
return
|
||||
}
|
||||
totalNet, totalGross, draft, confirmed, paid := 0.0, 0.0, 0, 0, 0
|
||||
for _, row := range rows {
|
||||
totalNet += row.NetSalary
|
||||
totalGross += row.GrossSalary
|
||||
switch row.Status {
|
||||
case 0:
|
||||
draft++
|
||||
case 1:
|
||||
confirmed++
|
||||
case 2:
|
||||
paid++
|
||||
}
|
||||
}
|
||||
c.compensationOK(map[string]interface{}{"payroll_month": month, "employee_count": len(rows), "total_gross_salary": compensationMoney(totalGross), "total_net_salary": compensationMoney(totalNet), "draft_count": draft, "confirmed_count": confirmed, "paid_count": paid})
|
||||
}
|
||||
@@ -24,9 +24,9 @@ type BackendEmployeeFile struct {
|
||||
EmergencyPhone string `orm:"column(emergency_phone);size(20);default()" json:"emergency_phone"`
|
||||
EmergencyRelation string `orm:"column(emergency_relation);size(30);default()" json:"emergency_relation"`
|
||||
WorkEmail string `orm:"column(work_email);size(100);default()" json:"work_email"`
|
||||
HireDate string `orm:"column(hire_date);type(date);null" json:"hire_date"`
|
||||
RegularDate string `orm:"column(regular_date);type(date);null" json:"regular_date"`
|
||||
LeaveDate string `orm:"column(leave_date);type(date);null" json:"leave_date"`
|
||||
HireDate *time.Time `orm:"column(hire_date);type(date);null" json:"hire_date"`
|
||||
RegularDate *time.Time `orm:"column(regular_date);type(date);null" json:"regular_date"`
|
||||
LeaveDate *time.Time `orm:"column(leave_date);type(date);null" json:"leave_date"`
|
||||
EmploymentStatus int8 `orm:"column(employment_status);default(2)" json:"employment_status"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
@@ -51,8 +51,8 @@ type BackendEmployeeFileRecord struct {
|
||||
Title string `orm:"column(title);size(100);default()" json:"title"`
|
||||
SubTitle string `orm:"column(sub_title);size(100);default()" json:"sub_title"`
|
||||
Extra string `orm:"column(extra);size(100);default()" json:"extra"`
|
||||
StartDate string `orm:"column(start_date);type(date);null" json:"start_date"`
|
||||
EndDate string `orm:"column(end_date);type(date);null" json:"end_date"`
|
||||
StartDate *time.Time `orm:"column(start_date);type(date);null" json:"start_date"`
|
||||
EndDate *time.Time `orm:"column(end_date);type(date);null" json:"end_date"`
|
||||
Description string `orm:"column(description);type(text);null" json:"description"`
|
||||
AttachmentURL string `orm:"column(attachment_url);size(500);default()" json:"attachment_url"`
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// BackendOaCompensationScheme 员工薪酬方案表 yz_backend_oa_compensation_schemes。
|
||||
// 一个员工可保留多份历史方案,生效中的方案由 effective_date 与 status 共同决定。
|
||||
type BackendOaCompensationScheme struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"`
|
||||
SchemeName string `orm:"column(scheme_name);size(100)" json:"scheme_name"`
|
||||
EffectiveDate time.Time `orm:"column(effective_date);type(date)" json:"effective_date"`
|
||||
ExpiryDate *time.Time `orm:"column(expiry_date);null;type(date)" json:"expiry_date"`
|
||||
BaseSalary float64 `orm:"column(base_salary);type(decimal);digits(12);decimals(2);default(0)" json:"base_salary"`
|
||||
PostAllowance float64 `orm:"column(post_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"post_allowance"`
|
||||
PerformanceSalary float64 `orm:"column(performance_salary);type(decimal);digits(12);decimals(2);default(0)" json:"performance_salary"`
|
||||
TransportAllowance float64 `orm:"column(transport_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"transport_allowance"`
|
||||
MealAllowance float64 `orm:"column(meal_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"meal_allowance"`
|
||||
CommunicationAllowance float64 `orm:"column(communication_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"communication_allowance"`
|
||||
SocialInsuranceBase float64 `orm:"column(social_insurance_base);type(decimal);digits(12);decimals(2);default(0)" json:"social_insurance_base"`
|
||||
HousingFundBase float64 `orm:"column(housing_fund_base);type(decimal);digits(12);decimals(2);default(0)" json:"housing_fund_base"`
|
||||
SocialInsuranceRate float64 `orm:"column(social_insurance_rate);type(decimal);digits(6);decimals(4);default(0)" json:"social_insurance_rate"`
|
||||
HousingFundRate float64 `orm:"column(housing_fund_rate);type(decimal);digits(6);decimals(4);default(0)" json:"housing_fund_rate"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *BackendOaCompensationScheme) TableName() string {
|
||||
return "yz_backend_oa_compensation_schemes"
|
||||
}
|
||||
|
||||
// BackendOaPayroll 月度薪资单主表 yz_backend_oa_compensation_payrolls。
|
||||
// EmployeeName / Department / Position 为生成时的快照,避免组织架构变更影响历史工资单。
|
||||
type BackendOaPayroll struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"`
|
||||
SchemeID *uint64 `orm:"column(scheme_id);null" json:"scheme_id"`
|
||||
PayrollMonth string `orm:"column(payroll_month);size(7)" json:"payroll_month"`
|
||||
EmployeeName string `orm:"column(employee_name);size(50)" json:"employee_name"`
|
||||
Department string `orm:"column(department);size(100)" json:"department"`
|
||||
Position string `orm:"column(position);size(100)" json:"position"`
|
||||
BaseSalary float64 `orm:"column(base_salary);type(decimal);digits(12);decimals(2);default(0)" json:"base_salary"`
|
||||
PostAllowance float64 `orm:"column(post_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"post_allowance"`
|
||||
PerformanceSalary float64 `orm:"column(performance_salary);type(decimal);digits(12);decimals(2);default(0)" json:"performance_salary"`
|
||||
TransportAllowance float64 `orm:"column(transport_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"transport_allowance"`
|
||||
MealAllowance float64 `orm:"column(meal_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"meal_allowance"`
|
||||
CommunicationAllowance float64 `orm:"column(communication_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"communication_allowance"`
|
||||
OvertimePay float64 `orm:"column(overtime_pay);type(decimal);digits(12);decimals(2);default(0)" json:"overtime_pay"`
|
||||
Bonus float64 `orm:"column(bonus);type(decimal);digits(12);decimals(2);default(0)" json:"bonus"`
|
||||
OtherAddition float64 `orm:"column(other_addition);type(decimal);digits(12);decimals(2);default(0)" json:"other_addition"`
|
||||
LeaveDeduction float64 `orm:"column(leave_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"leave_deduction"`
|
||||
LateDeduction float64 `orm:"column(late_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"late_deduction"`
|
||||
OtherDeduction float64 `orm:"column(other_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"other_deduction"`
|
||||
SocialInsurance float64 `orm:"column(social_insurance);type(decimal);digits(12);decimals(2);default(0)" json:"social_insurance"`
|
||||
HousingFund float64 `orm:"column(housing_fund);type(decimal);digits(12);decimals(2);default(0)" json:"housing_fund"`
|
||||
PersonalIncomeTax float64 `orm:"column(personal_income_tax);type(decimal);digits(12);decimals(2);default(0)" json:"personal_income_tax"`
|
||||
GrossSalary float64 `orm:"column(gross_salary);type(decimal);digits(12);decimals(2);default(0)" json:"gross_salary"`
|
||||
TotalDeduction float64 `orm:"column(total_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"total_deduction"`
|
||||
NetSalary float64 `orm:"column(net_salary);type(decimal);digits(12);decimals(2);default(0)" json:"net_salary"`
|
||||
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||
ConfirmedAt *time.Time `orm:"column(confirmed_at);null;type(datetime)" json:"confirmed_at"`
|
||||
PaidAt *time.Time `orm:"column(paid_at);null;type(datetime)" json:"paid_at"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *BackendOaPayroll) TableName() string {
|
||||
return "yz_backend_oa_compensation_payrolls"
|
||||
}
|
||||
|
||||
// BackendOaPayrollItem 薪资单自定义增减项 yz_backend_oa_compensation_payroll_items。
|
||||
type BackendOaPayrollItem struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
PayrollID uint64 `orm:"column(payroll_id)" json:"payroll_id"`
|
||||
ItemName string `orm:"column(item_name);size(100)" json:"item_name"`
|
||||
ItemType int8 `orm:"column(item_type);default(1)" json:"item_type"`
|
||||
Amount float64 `orm:"column(amount);type(decimal);digits(12);decimals(2);default(0)" json:"amount"`
|
||||
Remark string `orm:"column(remark);size(500);null" json:"remark"`
|
||||
SortOrder int `orm:"column(sort_order);default(0)" json:"sort_order"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *BackendOaPayrollItem) TableName() string {
|
||||
return "yz_backend_oa_compensation_payroll_items"
|
||||
}
|
||||
@@ -101,6 +101,9 @@ func Init(_ string) {
|
||||
new(BackendScheduleReminderSendLog),
|
||||
|
||||
new(OaSchedule),
|
||||
new(BackendOaCompensationScheme),
|
||||
new(BackendOaPayroll),
|
||||
new(BackendOaPayrollItem),
|
||||
new(BackendEmployeeFile),
|
||||
new(BackendEmployeeFileRecord),
|
||||
)
|
||||
|
||||
@@ -108,6 +108,7 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/oa/employeefile/detail/:id", &controllers.BackendEmployeeFileController{}, "get:Detail")
|
||||
beego.Router("/backend/oa/employeefile/create", &controllers.BackendEmployeeFileController{}, "post:Create")
|
||||
beego.Router("/backend/oa/employeefile/update/:id", &controllers.BackendEmployeeFileController{}, "post:Update")
|
||||
beego.Router("/backend/oa/employeefile/certificates/:id", &controllers.BackendEmployeeFileController{}, "post:UpdateCertificates")
|
||||
beego.Router("/backend/oa/employeefile/delete/:id", &controllers.BackendEmployeeFileController{}, "delete:Delete")
|
||||
beego.Router("/backend/oa/employeefile/filedEmployees", &controllers.BackendEmployeeFileController{}, "get:FiledEmployees")
|
||||
beego.Router("/backend/oa/employeefile/record/create", &controllers.BackendEmployeeFileController{}, "post:CreateRecord")
|
||||
@@ -240,6 +241,15 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder")
|
||||
beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder")
|
||||
|
||||
// OA 薪酬管理(薪酬方案 / 月度薪资单)
|
||||
beego.Router("/backend/oa/compensation/employees", &controllers.BackendOaCompensationController{}, "get:Employees")
|
||||
beego.Router("/backend/oa/compensation/dashboard", &controllers.BackendOaCompensationController{}, "get:Dashboard")
|
||||
beego.Router("/backend/oa/compensation/schemes", &controllers.BackendOaCompensationController{}, "get:SchemeList;post:CreateScheme")
|
||||
beego.Router("/backend/oa/compensation/schemes/:id", &controllers.BackendOaCompensationController{}, "post:UpdateScheme;delete:DeleteScheme")
|
||||
beego.Router("/backend/oa/compensation/payrolls", &controllers.BackendOaCompensationController{}, "get:List;post:Create")
|
||||
beego.Router("/backend/oa/compensation/payrolls/:id", &controllers.BackendOaCompensationController{}, "get:Detail;post:Update;delete:Delete")
|
||||
beego.Router("/backend/oa/compensation/payrolls/:id/status", &controllers.BackendOaCompensationController{}, "post:UpdateStatus")
|
||||
|
||||
// OA日程管理(日历待办)
|
||||
beego.Router("/backend/oa/schedule/list", &controllers.BackendOaScheduleController{}, "get:List")
|
||||
beego.Router("/backend/oa/schedule/stats", &controllers.BackendOaScheduleController{}, "get:Stats")
|
||||
|
||||
Reference in New Issue
Block a user