680 lines
27 KiB
Go
680 lines
27 KiB
Go
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})
|
|
}
|