修复bug
This commit is contained in:
1 parent
1850ac640e
commit
892ba5886d
20 files changed
+1078
-178
No files matched your search
+37
-30
@@ -669,41 +669,48 @@ func (c *AppOaController) oaDashboardReimburse(tid, uid uint64) map[string]inter
|
||||
}
|
||||
}
|
||||
expenseTypes := []map[string]interface{}{}
|
||||
paidIDList := make([]uint64, 0, len(paidIDs))
|
||||
for id := range paidIDs {
|
||||
paidIDList = append(paidIDList, id)
|
||||
}
|
||||
var items []models.BackendReimbursementItem
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("tid", tid).Filter("uid", uid).Filter("is_deleted", 0).
|
||||
All(&items); err == nil {
|
||||
typeAgg := map[string]*struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
for i := range items {
|
||||
if !paidIDs[items[i].ReimbursementID] {
|
||||
continue
|
||||
if len(paidIDList) > 0 {
|
||||
// 按已打款主单 ID 查明细,不依赖明细行的 tid/uid(历史数据可能不一致)
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("reimbursement_id__in", paidIDList).Filter("is_deleted", 0).
|
||||
All(&items); err == nil {
|
||||
typeAgg := map[string]*struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
for i := range items {
|
||||
if !paidIDs[items[i].ReimbursementID] {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(items[i].ExpenseType)
|
||||
if name == "" {
|
||||
name = "未分类"
|
||||
}
|
||||
agg, ok := typeAgg[name]
|
||||
if !ok {
|
||||
agg = &struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
typeAgg[name] = agg
|
||||
}
|
||||
agg.count++
|
||||
agg.amount += items[i].Amount
|
||||
}
|
||||
name := strings.TrimSpace(items[i].ExpenseType)
|
||||
if name == "" {
|
||||
name = "未分类"
|
||||
for name, agg := range typeAgg {
|
||||
expenseTypes = append(expenseTypes, map[string]interface{}{
|
||||
"name": name, "count": agg.count, "amount": agg.amount,
|
||||
})
|
||||
}
|
||||
agg, ok := typeAgg[name]
|
||||
if !ok {
|
||||
agg = &struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
typeAgg[name] = agg
|
||||
}
|
||||
agg.count++
|
||||
agg.amount += items[i].Amount
|
||||
}
|
||||
for name, agg := range typeAgg {
|
||||
expenseTypes = append(expenseTypes, map[string]interface{}{
|
||||
"name": name, "count": agg.count, "amount": agg.amount,
|
||||
sort.Slice(expenseTypes, func(i, j int) bool {
|
||||
return expenseTypes[i]["amount"].(float64) > expenseTypes[j]["amount"].(float64)
|
||||
})
|
||||
}
|
||||
sort.Slice(expenseTypes, func(i, j int) bool {
|
||||
return expenseTypes[i]["amount"].(float64) > expenseTypes[j]["amount"].(float64)
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
|
||||
@@ -258,7 +258,7 @@ func (c *AppOaController) ExpenseSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
code, msg := reimburseSubmitCore(claims, id)
|
||||
code, msg := reimburseSubmitCore(claims, id, readReimburseComment(&c.Controller))
|
||||
if msg != "" {
|
||||
c.oaErr(code, code, msg)
|
||||
return
|
||||
|
||||
@@ -24,6 +24,10 @@ type AdminMenuController = BackendMenuController
|
||||
// - 写接口(增删改、改状态)只有平台端能用,租户端能改菜单等于给自己开功能;
|
||||
// - 读接口两端都能用,但租户端只能读租户端那一份(cid 强制为 2)。
|
||||
func (c *BackendMenuController) denyMenu(code int, msg string) {
|
||||
// 必须同时设置 HTTP 状态码:只把 code 写进 body 而返回 200 时,
|
||||
// 前端 axios 拦截器会把这次鉴权失败当成成功响应,既不静默续期也不跳登录,
|
||||
// 菜单等页面只会停在「token已过期」的报错里。
|
||||
c.Ctx.Output.SetStatus(code)
|
||||
c.Data["json"] = map[string]interface{}{"code": code, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -454,8 +454,10 @@ func reimburseDetailData(claims *jwtutil.Claims, id uint64, scheme, host string)
|
||||
return nil, 404, "报销单不存在"
|
||||
}
|
||||
var items []models.BackendReimbursementItem
|
||||
// 只按 reimbursement_id 查明细:主单已按 tid + 申请人校验过。
|
||||
// 历史单据存在明细 tid 与主单不一致的情况,再按 tid 过滤会导致明细「消失」。
|
||||
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("reimbursement_id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).
|
||||
Filter("reimbursement_id", id).Filter("is_deleted", 0).
|
||||
OrderBy("id").All(&items)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, 500, "查询明细失败: " + err.Error()
|
||||
@@ -533,6 +535,10 @@ func (c *BackendReimburseController) Create() {
|
||||
}
|
||||
|
||||
// reimburseCreateCore 创建报销单(草稿)共享逻辑。
|
||||
//
|
||||
// 创建只落草稿:不接受客户端传入的状态,也不启动审批流程。
|
||||
// 提交审批必须由用户显式调用 Submit(Web 端为列表操作栏的「提交审核」),
|
||||
// 在此之前草稿可反复编辑,满足周期性报销多次修改的需求。
|
||||
func reimburseCreateCore(claims *jwtutil.Claims, payload reimbursementPayload) (uint64, int, string) {
|
||||
applyDate, err := parseReimburseDate(payload.ApplyDate)
|
||||
if err != nil {
|
||||
@@ -541,7 +547,8 @@ func reimburseCreateCore(claims *jwtutil.Claims, payload reimbursementPayload) (
|
||||
uid := uint64(claims.UserID)
|
||||
row := &models.BackendReimbursement{
|
||||
Tid: uint64(claims.TenantId), UID: &uid, UserID: uid, DepartmentID: payload.DepartmentID,
|
||||
TotalAmount: 0, Status: 0, ApplyDate: applyDate,
|
||||
TotalAmount: 0, Status: reimbursementStatusDraft, ApplyDate: applyDate,
|
||||
CurrentStep: 0, CurrentApproverID: 0,
|
||||
Description: nullableReimburseString(payload.Description),
|
||||
}
|
||||
if err := saveReimbursement(row); err != nil {
|
||||
@@ -575,14 +582,14 @@ func (c *BackendReimburseController) Update() {
|
||||
c.reply(200, "success", map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// reimburseUpdateCore 编辑报销单(仅草稿)共享逻辑。
|
||||
// reimburseUpdateCore 编辑报销单(草稿/已驳回/已撤回)共享逻辑。
|
||||
func reimburseUpdateCore(claims *jwtutil.Claims, id uint64, payload reimbursementPayload) (int, string) {
|
||||
row, err := findOwnedReimbursement(claims, id)
|
||||
if err != nil {
|
||||
return 404, "报销单不存在"
|
||||
}
|
||||
if row.Status != 0 {
|
||||
return 400, "只有草稿可以编辑"
|
||||
if !canEditReimbursement(row.Status) {
|
||||
return 400, "当前状态不可编辑,请先撤回"
|
||||
}
|
||||
applyDate, err := parseReimburseDate(payload.ApplyDate)
|
||||
if err != nil {
|
||||
@@ -621,7 +628,7 @@ func reimburseDeleteCore(claims *jwtutil.Claims, id uint64) (int, string) {
|
||||
if err != nil {
|
||||
return 404, "报销单不存在"
|
||||
}
|
||||
if row.Status != 0 {
|
||||
if row.Status != reimbursementStatusDraft {
|
||||
return 400, "只有草稿可以删除"
|
||||
}
|
||||
now := time.Now()
|
||||
@@ -651,13 +658,13 @@ func (c *BackendReimburseController) getItemID() (uint64, bool) {
|
||||
}
|
||||
|
||||
// findOwnedReimbursementItem 查询本人报销单下的费用明细(包级共享,移动端复用)。
|
||||
func findOwnedReimbursementItem(claims *jwtutil.Claims, id, reimbursementID uint64) (*models.BackendReimbursementItem, error) {
|
||||
// 主单归属已由 ensureEditableReimbursement 按 tid + 申请人校验,这里只按
|
||||
// id + reimbursement_id 定位,避免历史明细 tid/uid 不一致时无法编辑。
|
||||
func findOwnedReimbursementItem(_ *jwtutil.Claims, id, reimbursementID uint64) (*models.BackendReimbursementItem, error) {
|
||||
item := &models.BackendReimbursementItem{}
|
||||
err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("id", id).
|
||||
Filter("reimbursement_id", reimbursementID).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("uid", claims.UserID).
|
||||
Filter("is_deleted", 0).
|
||||
One(item)
|
||||
return item, err
|
||||
@@ -677,15 +684,82 @@ func validateReimbursementItem(item reimbursementItemPayload) (time.Time, error)
|
||||
return date, nil
|
||||
}
|
||||
|
||||
// ensureDraftReimbursement 校验报销单存在且为草稿(包级共享,移动端复用)。
|
||||
// canEditReimbursement 可编辑状态:草稿、已驳回、已撤回。
|
||||
// 审批中需先撤回;已通过/已打款不可编辑,保证审批结果与打款金额不被改动。
|
||||
func canEditReimbursement(status int8) bool {
|
||||
switch status {
|
||||
case reimbursementStatusDraft, reimbursementStatusRejected, reimbursementStatusWithdrawn:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveReimburseUserName 按 identity_id 解析姓名:
|
||||
// 认证中心企业绑定表 → 身份表 → 老租户用户表。历史令牌/导入数据可能缺映射,逐级兜底。
|
||||
func resolveReimburseUserName(tid, userID uint64) string {
|
||||
if userID == 0 {
|
||||
return ""
|
||||
}
|
||||
if tid != 0 {
|
||||
var bind models.AuthTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("identity_id", userID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&bind); err == nil {
|
||||
if bind.Name != nil && strings.TrimSpace(*bind.Name) != "" {
|
||||
return strings.TrimSpace(*bind.Name)
|
||||
}
|
||||
if bind.Account != nil && strings.TrimSpace(*bind.Account) != "" {
|
||||
return strings.TrimSpace(*bind.Account)
|
||||
}
|
||||
}
|
||||
}
|
||||
var ident models.AuthIdentity
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", userID).
|
||||
One(&ident); err == nil {
|
||||
if ident.Nickname != nil && strings.TrimSpace(*ident.Nickname) != "" {
|
||||
return strings.TrimSpace(*ident.Nickname)
|
||||
}
|
||||
if ident.Account != nil && strings.TrimSpace(*ident.Account) != "" {
|
||||
return strings.TrimSpace(*ident.Account)
|
||||
}
|
||||
if ident.Mobile != nil && strings.TrimSpace(*ident.Mobile) != "" {
|
||||
return strings.TrimSpace(*ident.Mobile)
|
||||
}
|
||||
}
|
||||
account, name := services.OperationLogUser(&tid, userID)
|
||||
if strings.TrimSpace(name) != "" {
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
return strings.TrimSpace(account)
|
||||
}
|
||||
|
||||
// reimburseActorName 审批时间线的操作人名称:
|
||||
// 优先令牌中的 username,缺失时回落到认证中心/租户用户表。
|
||||
func reimburseActorName(tid uint64, claims *jwtutil.Claims) string {
|
||||
if claims == nil {
|
||||
return "系统"
|
||||
}
|
||||
if name := strings.TrimSpace(claims.Username); name != "" {
|
||||
return name
|
||||
}
|
||||
if name := resolveReimburseUserName(tid, uint64(claims.UserID)); name != "" {
|
||||
return name
|
||||
}
|
||||
return "系统"
|
||||
}
|
||||
|
||||
// ensureEditableReimbursement 校验报销单存在且处于可编辑状态(包级共享,移动端复用)。
|
||||
// 返回 (报销单, HTTP 状态码, 错误信息),错误信息为空串表示通过。
|
||||
func ensureDraftReimbursement(claims *jwtutil.Claims, id uint64) (*models.BackendReimbursement, int, string) {
|
||||
func ensureEditableReimbursement(claims *jwtutil.Claims, id uint64) (*models.BackendReimbursement, int, string) {
|
||||
row, err := findOwnedReimbursement(claims, id)
|
||||
if err != nil {
|
||||
return nil, 404, "报销单不存在"
|
||||
}
|
||||
if row.Status != 0 {
|
||||
return nil, 400, "只有草稿可以维护费用明细"
|
||||
if !canEditReimbursement(row.Status) {
|
||||
return nil, 400, "当前状态不可编辑费用明细,请先撤回"
|
||||
}
|
||||
return row, 200, ""
|
||||
}
|
||||
@@ -737,7 +811,7 @@ func (c *BackendReimburseController) AddItem() {
|
||||
|
||||
// reimburseAddItemCore 新增费用明细共享逻辑(仅草稿;自动重算报销总额)。
|
||||
func reimburseAddItemCore(claims *jwtutil.Claims, reimbursementID uint64, payload reimbursementItemPayload, scheme, host string) (map[string]interface{}, int, string) {
|
||||
if _, _, msg := ensureDraftReimbursement(claims, reimbursementID); msg != "" {
|
||||
if _, _, msg := ensureEditableReimbursement(claims, reimbursementID); msg != "" {
|
||||
code := 400
|
||||
if msg == "报销单不存在" {
|
||||
code = 404
|
||||
@@ -802,7 +876,7 @@ func (c *BackendReimburseController) UpdateItem() {
|
||||
|
||||
// reimburseUpdateItemCore 更新费用明细共享逻辑(仅草稿;自动重算报销总额)。
|
||||
func reimburseUpdateItemCore(claims *jwtutil.Claims, reimbursementID, itemID uint64, payload reimbursementItemPayload, scheme, host string) (map[string]interface{}, int, string) {
|
||||
if _, _, msg := ensureDraftReimbursement(claims, reimbursementID); msg != "" {
|
||||
if _, _, msg := ensureEditableReimbursement(claims, reimbursementID); msg != "" {
|
||||
code := 400
|
||||
if msg == "报销单不存在" {
|
||||
code = 404
|
||||
@@ -864,7 +938,7 @@ func (c *BackendReimburseController) DeleteItem() {
|
||||
|
||||
// reimburseDeleteItemCore 删除费用明细共享逻辑(仅草稿;自动重算报销总额)。
|
||||
func reimburseDeleteItemCore(claims *jwtutil.Claims, reimbursementID, itemID uint64) (int, string) {
|
||||
if _, _, msg := ensureDraftReimbursement(claims, reimbursementID); msg != "" {
|
||||
if _, _, msg := ensureEditableReimbursement(claims, reimbursementID); msg != "" {
|
||||
if msg == "报销单不存在" {
|
||||
return 404, msg
|
||||
}
|
||||
@@ -898,7 +972,7 @@ func (c *BackendReimburseController) Submit() {
|
||||
c.reply(400, "无效ID", nil)
|
||||
return
|
||||
}
|
||||
code, msg := reimburseSubmitCore(claims, id)
|
||||
code, msg := reimburseSubmitCore(claims, id, c.readReimburseActPayload())
|
||||
if msg != "" {
|
||||
c.reply(code, msg, nil)
|
||||
return
|
||||
@@ -906,21 +980,104 @@ func (c *BackendReimburseController) Submit() {
|
||||
c.reply(200, "success", nil)
|
||||
}
|
||||
|
||||
// Withdraw POST /backend/reimbursements/:id/withdraw
|
||||
//
|
||||
// 报销人撤回审批中的单据:撤掉运行中的审批实例,并把单据退回草稿,
|
||||
// 撤回后可在详情继续编辑费用条目,确认无误后再次提交审核。
|
||||
func (c *BackendReimburseController) Withdraw() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
c.reply(401, "未登录或无权限", nil)
|
||||
return
|
||||
}
|
||||
id, ok := c.getID()
|
||||
if !ok {
|
||||
c.reply(400, "无效ID", nil)
|
||||
return
|
||||
}
|
||||
code, msg := reimburseWithdrawCore(claims, id, c.readReimburseActPayload())
|
||||
if msg != "" {
|
||||
c.reply(code, msg, nil)
|
||||
return
|
||||
}
|
||||
c.reply(200, "success", nil)
|
||||
}
|
||||
|
||||
// reimburseWithdrawCore 撤回审批中的报销单并退回草稿。
|
||||
//
|
||||
// 与「审批中心」的发起人撤回一致:运行中的实例置为已撤回、待办任务置为已跳过;
|
||||
// 区别是报销单本身退回草稿(0)而不是已撤回(4),这样用户能继续编辑条目并重新提交。
|
||||
func reimburseWithdrawCore(claims *jwtutil.Claims, id uint64, comment string) (int, string) {
|
||||
row, err := findOwnedReimbursement(claims, id)
|
||||
if err != nil {
|
||||
return 404, "报销单不存在"
|
||||
}
|
||||
if row.Status != reimbursementStatusApproving {
|
||||
return 400, "只有审批中的报销单可以撤回"
|
||||
}
|
||||
|
||||
// 历史单据可能没有审批实例:没有实例时直接退回草稿,保证仍可编辑
|
||||
inst, ok := findReimburseInstance(row.Tid, row.ID)
|
||||
if ok {
|
||||
if inst.Status != models.ApprovalInstanceRunning {
|
||||
return 400, "审批已结束,无法撤回"
|
||||
}
|
||||
now := time.Now()
|
||||
if _, err = models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||||
Filter("id", inst.ID).
|
||||
Update(orm.Params{
|
||||
"status": models.ApprovalInstanceWithdrawn,
|
||||
"finish_time": now,
|
||||
"update_time": now,
|
||||
}); err != nil {
|
||||
return 500, "撤回审批失败"
|
||||
}
|
||||
_, _ = models.Orm.QueryTable(new(models.ApprovalTask)).
|
||||
Filter("instance_id", inst.ID).
|
||||
Filter("status", models.ApprovalTaskPending).
|
||||
Update(orm.Params{"status": models.ApprovalTaskSkipped, "update_time": now})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if _, err = models.Orm.QueryTable(new(models.BackendReimbursement)).
|
||||
Filter("id", id).Filter("tid", claims.TenantId).
|
||||
Update(orm.Params{
|
||||
"status": reimbursementStatusDraft, "current_step": 0,
|
||||
"current_approver_id": 0, "update_time": now,
|
||||
}); err != nil {
|
||||
return 500, "撤回失败"
|
||||
}
|
||||
// 时间线补一条撤回记录(不调用引擎 SyncBizResult,避免把单据同步成「已撤回」)
|
||||
_, _ = models.Orm.Insert(&models.BackendApprovalRecord{
|
||||
Tid: uint64(claims.TenantId), UID: uint64Ptr(uint64(claims.UserID)),
|
||||
ReimbursementID: id, ApproverID: uint64(claims.UserID),
|
||||
ApproverName: reimburseActorName(uint64(claims.TenantId), claims),
|
||||
Action: models.ApprovalActionWithdraw, Comment: nullableReimburseString(comment),
|
||||
Step: row.CurrentStep, PrevStep: row.CurrentStep,
|
||||
})
|
||||
return 200, ""
|
||||
}
|
||||
|
||||
// reimburseActPayload 审批操作请求体。
|
||||
type reimburseActPayload struct {
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// readReimburseActPayload 读取审批意见(可空)。
|
||||
func (c *BackendReimburseController) readReimburseActPayload() string {
|
||||
// readReimburseComment 读取审批/撤回意见(可空),Web 与移动端共用。
|
||||
func readReimburseComment(ctrl *beego.Controller) string {
|
||||
var payload reimburseActPayload
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
raw, err := io.ReadAll(ctrl.Ctx.Request.Body)
|
||||
if err == nil && len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &payload)
|
||||
}
|
||||
return strings.TrimSpace(payload.Comment)
|
||||
}
|
||||
|
||||
// readReimburseActPayload 读取审批意见(可空)。
|
||||
func (c *BackendReimburseController) readReimburseActPayload() string {
|
||||
return readReimburseComment(&c.Controller)
|
||||
}
|
||||
|
||||
// Approve POST /backend/reimbursements/approve/:id 通过当前审批节点(body: {comment})。
|
||||
func (c *BackendReimburseController) Approve() {
|
||||
claims, ok := c.claims()
|
||||
@@ -1000,12 +1157,12 @@ var reimburseFallbackBizTypes = []string{models.ApprovalBizReimburse, "oa"}
|
||||
// 审批最终结果由 services/approval.SyncBizResult 回写到报销单状态,保证
|
||||
// 「审批中心」与「报销管理」看到的状态一致。
|
||||
// 提交后报销单置为审批中(1),并写入 action=1 的审批记录供业务详情时间线展示。
|
||||
func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
|
||||
func reimburseSubmitCore(claims *jwtutil.Claims, id uint64, comment string) (int, string) {
|
||||
row, err := findOwnedReimbursement(claims, id)
|
||||
if err != nil {
|
||||
return 404, "报销单不存在"
|
||||
}
|
||||
if row.Status != 0 {
|
||||
if !canEditReimbursement(row.Status) {
|
||||
return 400, "当前状态不可提交"
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
@@ -1013,6 +1170,7 @@ func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
|
||||
if flowID == 0 {
|
||||
return 400, "尚未配置审批流程"
|
||||
}
|
||||
actorName := reimburseActorName(tid, claims)
|
||||
|
||||
instID, err := approvalEngine.Submit(approvalEngine.SubmitInput{
|
||||
Scope: models.ApprovalScopeTenant,
|
||||
@@ -1022,7 +1180,7 @@ func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
|
||||
BizID: id,
|
||||
BizTitle: reimburseBizTitle(row),
|
||||
SubmitUID: uint64(claims.UserID),
|
||||
SubmitName: claims.Username,
|
||||
SubmitName: actorName,
|
||||
DeptID: reimburseSubmitDeptID(tid, uint64(claims.UserID)),
|
||||
FormData: map[string]interface{}{
|
||||
"id": id,
|
||||
@@ -1037,7 +1195,7 @@ func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
|
||||
}
|
||||
|
||||
// 流程未配置审批节点时引擎会直接通过,此时报销单同步为已通过(2)
|
||||
status := int8(1)
|
||||
status := reimbursementStatusApproving
|
||||
var inst models.ApprovalInstance
|
||||
if e := models.Orm.QueryTable(new(models.ApprovalInstance)).Filter("id", instID).One(&inst); e == nil {
|
||||
switch inst.Status {
|
||||
@@ -1057,7 +1215,8 @@ func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
|
||||
}
|
||||
_, err = models.Orm.Insert(&models.BackendApprovalRecord{
|
||||
Tid: tid, UID: uint64Ptr(uint64(claims.UserID)), ReimbursementID: id,
|
||||
ApproverID: uint64(claims.UserID), ApproverName: claims.Username, Action: models.ApprovalActionSubmit,
|
||||
ApproverID: uint64(claims.UserID), ApproverName: actorName,
|
||||
Action: models.ApprovalActionSubmit, Comment: nullableReimburseString(comment),
|
||||
Step: 0, PrevStep: 0,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1118,8 +1277,14 @@ type reimbursementStatusPayload struct {
|
||||
|
||||
// 报销单状态(0草稿 1审批中 2已通过 3已驳回 4已撤回 5已打款)
|
||||
const (
|
||||
reimbursementStatusApproved int8 = 2 // 已通过
|
||||
reimbursementStatusPaid int8 = 5 // 已打款
|
||||
// reimbursementStatusDraft 草稿:新建后的唯一初始状态。
|
||||
// 草稿可反复编辑/增删明细,只有用户在列表点「提交审核」才会进入审批流。
|
||||
reimbursementStatusDraft int8 = 0
|
||||
reimbursementStatusApproving int8 = 1 // 审批中
|
||||
reimbursementStatusApproved int8 = 2 // 已通过
|
||||
reimbursementStatusRejected int8 = 3 // 已驳回
|
||||
reimbursementStatusWithdrawn int8 = 4 // 已撤回
|
||||
reimbursementStatusPaid int8 = 5 // 已打款
|
||||
)
|
||||
|
||||
// UpdateStatus POST /backend/reimbursements/:id/status — 报销人确认款项到账(置为「已打款」)。
|
||||
@@ -1217,6 +1382,18 @@ func reimburseRecordsData(claims *jwtutil.Claims, id uint64) ([]models.BackendAp
|
||||
if rows == nil {
|
||||
rows = []models.BackendApprovalRecord{}
|
||||
}
|
||||
// 历史记录的 approver_name 可能为空(旧令牌没有 username 字段),读取时补齐
|
||||
tid := uint64(claims.TenantId)
|
||||
for i := range rows {
|
||||
if strings.TrimSpace(rows[i].ApproverName) != "" {
|
||||
continue
|
||||
}
|
||||
name := resolveReimburseUserName(tid, rows[i].ApproverID)
|
||||
if name == "" {
|
||||
name = "系统"
|
||||
}
|
||||
rows[i].ApproverName = name
|
||||
}
|
||||
return rows, 200, ""
|
||||
}
|
||||
|
||||
@@ -1603,38 +1780,44 @@ func (c *BackendReimburseController) Dashboard() {
|
||||
paidIDs[rows[i].ID] = true
|
||||
}
|
||||
}
|
||||
var items []models.BackendReimbursementItem
|
||||
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("uid", claims.UserID).
|
||||
Filter("is_deleted", 0).
|
||||
All(&items)
|
||||
if err != nil {
|
||||
c.reply(500, "查询失败", nil)
|
||||
return
|
||||
paidIDList := make([]uint64, 0, len(paidIDs))
|
||||
for id := range paidIDs {
|
||||
paidIDList = append(paidIDList, id)
|
||||
}
|
||||
typeAgg := map[string]*struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
for i := range items {
|
||||
if !paidIDs[items[i].ReimbursementID] {
|
||||
continue
|
||||
if len(paidIDList) > 0 {
|
||||
// 按已打款主单 ID 查明细,不依赖明细行的 tid/uid(历史数据可能不一致)
|
||||
var items []models.BackendReimbursementItem
|
||||
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
|
||||
Filter("reimbursement_id__in", paidIDList).
|
||||
Filter("is_deleted", 0).
|
||||
All(&items)
|
||||
if err != nil {
|
||||
c.reply(500, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(items[i].ExpenseType)
|
||||
if name == "" {
|
||||
name = "未分类"
|
||||
for i := range items {
|
||||
if !paidIDs[items[i].ReimbursementID] {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(items[i].ExpenseType)
|
||||
if name == "" {
|
||||
name = "未分类"
|
||||
}
|
||||
agg, ok := typeAgg[name]
|
||||
if !ok {
|
||||
agg = &struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
typeAgg[name] = agg
|
||||
}
|
||||
agg.count++
|
||||
agg.amount += items[i].Amount
|
||||
}
|
||||
agg, ok := typeAgg[name]
|
||||
if !ok {
|
||||
agg = &struct {
|
||||
count int
|
||||
amount float64
|
||||
}{}
|
||||
typeAgg[name] = agg
|
||||
}
|
||||
agg.count++
|
||||
agg.amount += items[i].Amount
|
||||
}
|
||||
expenseTypes := make([]map[string]interface{}, 0, len(typeAgg))
|
||||
for name, agg := range typeAgg {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package controllers
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCanEditReimbursement(t *testing.T) {
|
||||
editable := []int8{
|
||||
reimbursementStatusDraft,
|
||||
reimbursementStatusRejected,
|
||||
reimbursementStatusWithdrawn,
|
||||
}
|
||||
for _, status := range editable {
|
||||
if !canEditReimbursement(status) {
|
||||
t.Errorf("status %d should be editable", status)
|
||||
}
|
||||
}
|
||||
|
||||
locked := []int8{
|
||||
reimbursementStatusApproving,
|
||||
reimbursementStatusApproved,
|
||||
reimbursementStatusPaid,
|
||||
}
|
||||
for _, status := range locked {
|
||||
if canEditReimbursement(status) {
|
||||
t.Errorf("status %d should not be editable", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func reimburseActCore(claims *jwtutil.Claims, id uint64, pass bool, comment stri
|
||||
}
|
||||
|
||||
applyReimburseApprovalResult(uint64(claims.TenantId), id,
|
||||
uint64(claims.UserID), claims.Username, task.Sort,
|
||||
uint64(claims.UserID), reimburseActorName(uint64(claims.TenantId), claims), task.Sort,
|
||||
pass, comment, result.Finished, result.InstanceStat)
|
||||
|
||||
hint := result.Message
|
||||
|
||||
Reference in new issue
Block a user