fix: 修复员工服务、报销单审批及前端交互细节
- 修复员工创建时组织架构关联与邮箱校验逻辑 - 修复报销单API端点参数及预审流程调用 - 优化审批中心、差旅详情等前端页面交互 - 更新侧边栏导航与请求视图模型 - 补充员工服务与报销单相关测试用例
This commit is contained in:
@@ -420,7 +420,7 @@ def submit_expense_claim(claim_id: str, db: DbSession, current_user: CurrentUser
|
|||||||
"/claims/{claim_id}/return",
|
"/claims/{claim_id}/return",
|
||||||
response_model=ExpenseClaimRead,
|
response_model=ExpenseClaimRead,
|
||||||
summary="退回报销单",
|
summary="退回报销单",
|
||||||
description="财务人员或高级管理人员可将可见报销单退回到待补充状态。",
|
description="财务人员或高级管理人员可将可见报销单退回到待提交状态。",
|
||||||
responses={
|
responses={
|
||||||
status.HTTP_404_NOT_FOUND: {
|
status.HTTP_404_NOT_FOUND: {
|
||||||
"model": ErrorResponse,
|
"model": ErrorResponse,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from datetime import date, datetime
|
from datetime import UTC, date, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from sqlalchemy import inspect, select, text
|
from sqlalchemy import inspect, select, text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -49,6 +50,7 @@ from app.services.employee_seed import (
|
|||||||
logger = get_logger("app.services.employee")
|
logger = get_logger("app.services.employee")
|
||||||
DEFAULT_EMPLOYEE_PASSWORD = "123456"
|
DEFAULT_EMPLOYEE_PASSWORD = "123456"
|
||||||
MAX_EMPLOYEE_CHANGE_LOGS = 5
|
MAX_EMPLOYEE_CHANGE_LOGS = 5
|
||||||
|
DISPLAY_TIMEZONE = ZoneInfo("Asia/Shanghai")
|
||||||
|
|
||||||
STATUS_TONE_MAP = {
|
STATUS_TONE_MAP = {
|
||||||
"在职": "success",
|
"在职": "success",
|
||||||
@@ -183,7 +185,7 @@ class EmployeeService:
|
|||||||
sync_state=payload.sync_state,
|
sync_state=payload.sync_state,
|
||||||
spotlight=payload.spotlight,
|
spotlight=payload.spotlight,
|
||||||
password_hash=hash_password(DEFAULT_EMPLOYEE_PASSWORD),
|
password_hash=hash_password(DEFAULT_EMPLOYEE_PASSWORD),
|
||||||
last_sync_at=datetime.now(),
|
last_sync_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
|
|
||||||
if payload.organization_unit_code:
|
if payload.organization_unit_code:
|
||||||
@@ -360,7 +362,7 @@ class EmployeeService:
|
|||||||
if not changed_fields and not password_changed and not role_changed:
|
if not changed_fields and not password_changed and not role_changed:
|
||||||
return self._serialize_employee(employee)
|
return self._serialize_employee(employee)
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now(UTC)
|
||||||
employee.last_sync_at = now
|
employee.last_sync_at = now
|
||||||
employee.sync_state = "已同步"
|
employee.sync_state = "已同步"
|
||||||
|
|
||||||
@@ -401,7 +403,7 @@ class EmployeeService:
|
|||||||
if employee.employment_status == "停用":
|
if employee.employment_status == "停用":
|
||||||
return self._serialize_employee(employee)
|
return self._serialize_employee(employee)
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now(UTC)
|
||||||
employee.employment_status = "停用"
|
employee.employment_status = "停用"
|
||||||
employee.sync_state = "已同步"
|
employee.sync_state = "已同步"
|
||||||
employee.last_sync_at = now
|
employee.last_sync_at = now
|
||||||
@@ -422,7 +424,7 @@ class EmployeeService:
|
|||||||
if employee.employment_status != "停用":
|
if employee.employment_status != "停用":
|
||||||
return self._serialize_employee(employee)
|
return self._serialize_employee(employee)
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now(UTC)
|
||||||
employee.employment_status = "在职"
|
employee.employment_status = "在职"
|
||||||
employee.sync_state = "已同步"
|
employee.sync_state = "已同步"
|
||||||
employee.last_sync_at = now
|
employee.last_sync_at = now
|
||||||
@@ -484,7 +486,7 @@ class EmployeeService:
|
|||||||
logger.exception("Employee import failed during database write")
|
logger.exception("Employee import failed during database write")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
imported_at = self._format_datetime(datetime.now()) or ""
|
imported_at = self._format_datetime(datetime.now(UTC)) or ""
|
||||||
message = f"导入成功:新增 {summary['created']} 人,更新 {summary['updated']} 人。"
|
message = f"导入成功:新增 {summary['created']} 人,更新 {summary['updated']} 人。"
|
||||||
logger.info(
|
logger.info(
|
||||||
"Imported employees created=%d updated=%d total=%d",
|
"Imported employees created=%d updated=%d total=%d",
|
||||||
@@ -624,7 +626,7 @@ class EmployeeService:
|
|||||||
}
|
}
|
||||||
created = 0
|
created = 0
|
||||||
updated = 0
|
updated = 0
|
||||||
now = datetime.now()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -979,7 +981,7 @@ class EmployeeService:
|
|||||||
employee=employee,
|
employee=employee,
|
||||||
action=action,
|
action=action,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
occurred_at=occurred_at or datetime.now(),
|
occurred_at=occurred_at or datetime.now(UTC),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1106,19 +1108,29 @@ class EmployeeService:
|
|||||||
return None
|
return None
|
||||||
return value.strftime("%Y-%m-%d")
|
return value.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_display_datetime(value: datetime) -> datetime:
|
||||||
|
if value.tzinfo is None:
|
||||||
|
normalized = value.replace(tzinfo=UTC)
|
||||||
|
else:
|
||||||
|
normalized = value.astimezone(UTC)
|
||||||
|
return normalized.astimezone(DISPLAY_TIMEZONE)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_datetime(value: datetime | None) -> str | None:
|
def _format_datetime(value: datetime | None) -> str | None:
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return value.strftime("%Y-%m-%d %H:%M")
|
local = EmployeeService._to_display_datetime(value)
|
||||||
|
return local.strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_history_datetime(value: datetime | None) -> str:
|
def _format_history_datetime(value: datetime | None) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
|
local = EmployeeService._to_display_datetime(value)
|
||||||
return (
|
return (
|
||||||
f"{value.year}年{value.month}月{value.day}日"
|
f"{local.year}年{local.month}月{local.day}日"
|
||||||
f"{value.hour}时{value.minute}分{value.second}秒"
|
f"{local.hour}时{local.minute}分"
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -867,13 +867,13 @@ class ExpenseClaimService:
|
|||||||
"source": "manual_return",
|
"source": "manual_return",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"label": "人工退回",
|
"label": "人工退回",
|
||||||
"message": return_reason or f"{operator} 已退回该报销单,请申请人补充后重新提交。",
|
"message": return_reason or f"{operator} 已退回该报销单,请申请人调整后重新提交。",
|
||||||
"operator": operator,
|
"operator": operator,
|
||||||
"created_at": datetime.now(UTC).isoformat(),
|
"created_at": datetime.now(UTC).isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
claim.status = "returned"
|
claim.status = "returned"
|
||||||
claim.approval_stage = "待补充"
|
claim.approval_stage = "待提交"
|
||||||
claim.risk_flags_json = [*list(claim.risk_flags_json or []), return_flag]
|
claim.risk_flags_json = [*list(claim.risk_flags_json or []), return_flag]
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
@@ -2634,8 +2634,8 @@ class ExpenseClaimService:
|
|||||||
|
|
||||||
def _ensure_draft_claim(self, claim: ExpenseClaim) -> None:
|
def _ensure_draft_claim(self, claim: ExpenseClaim) -> None:
|
||||||
normalized_status = str(claim.status or "").strip().lower()
|
normalized_status = str(claim.status or "").strip().lower()
|
||||||
if normalized_status not in {"draft", "supplement"}:
|
if normalized_status not in {"draft", "supplement", "returned"}:
|
||||||
raise ValueError("只有草稿或待补充状态的报销单才允许执行该操作。")
|
raise ValueError("只有草稿、待补充或退回待提交状态的报销单才允许执行该操作。")
|
||||||
|
|
||||||
def _run_ai_submission_review(self, claim: ExpenseClaim) -> dict[str, Any]:
|
def _run_ai_submission_review(self, claim: ExpenseClaim) -> dict[str, Any]:
|
||||||
base_flags = list(claim.risk_flags_json or [])
|
base_flags = list(claim.risk_flags_json or [])
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"updated_at": "2026-05-17T09:28:28.999515+00:00",
|
"updated_at": "2026-05-17T09:28:28.999515+00:00",
|
||||||
"uploaded_by": "admin",
|
"uploaded_by": "admin",
|
||||||
"version_number": 1,
|
"version_number": 1,
|
||||||
"ingest_status": 3,
|
"ingest_status": 1,
|
||||||
"ingest_status_updated_at": "2026-05-17T10:01:33.272539+00:00",
|
"ingest_status_updated_at": "2026-05-20T06:29:01.123795+00:00",
|
||||||
"ingest_completed_at": "2026-05-17T10:01:33.272539+00:00",
|
"ingest_completed_at": "2026-05-17T10:01:33.272539+00:00",
|
||||||
"ingest_document_name": "远光《公司支出管理办法(2024)》.pdf",
|
"ingest_document_name": "远光《公司支出管理办法(2024)》.pdf",
|
||||||
"ingest_document_updated_at": "2026-05-17T09:28:28.999515+00:00",
|
"ingest_document_updated_at": "2026-05-17T09:28:28.999515+00:00",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine, func, select
|
from sqlalchemy import create_engine, func, select
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
@@ -241,6 +243,14 @@ def test_update_employee_changes_manager() -> None:
|
|||||||
assert updated.manager == manager.name
|
assert updated.manager == manager.name
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_history_datetime_uses_local_timezone_without_seconds() -> None:
|
||||||
|
value = datetime(2026, 5, 20, 6, 30, 45, tzinfo=UTC)
|
||||||
|
formatted = EmployeeService._format_history_datetime(value)
|
||||||
|
|
||||||
|
assert formatted == "2026年5月20日14时30分"
|
||||||
|
assert "秒" not in formatted
|
||||||
|
|
||||||
|
|
||||||
def test_update_employee_rejects_invalid_date_format() -> None:
|
def test_update_employee_rejects_invalid_date_format() -> None:
|
||||||
with build_session() as db:
|
with build_session() as db:
|
||||||
service = EmployeeService(db)
|
service = EmployeeService(db)
|
||||||
|
|||||||
@@ -1312,7 +1312,7 @@ def test_privileged_user_can_return_and_delete_submitted_claim() -> None:
|
|||||||
|
|
||||||
assert returned is not None
|
assert returned is not None
|
||||||
assert returned.status == "returned"
|
assert returned.status == "returned"
|
||||||
assert returned.approval_stage == "待补充"
|
assert returned.approval_stage == "待提交"
|
||||||
assert any(
|
assert any(
|
||||||
isinstance(flag, dict)
|
isinstance(flag, dict)
|
||||||
and flag.get("source") == "manual_return"
|
and flag.get("source") == "manual_return"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const sidebarMeta = {
|
|||||||
overview: { label: '总览' },
|
overview: { label: '总览' },
|
||||||
workbench: { label: '个人工作台' },
|
workbench: { label: '个人工作台' },
|
||||||
requests: { label: '个人报销' },
|
requests: { label: '个人报销' },
|
||||||
approval: { label: '审批中心', badge: '12' },
|
approval: { label: '审批中心' },
|
||||||
policies: { label: '知识管理' },
|
policies: { label: '知识管理' },
|
||||||
audit: { label: '任务规则中心' },
|
audit: { label: '任务规则中心' },
|
||||||
logs: { label: '日志管理' },
|
logs: { label: '日志管理' },
|
||||||
|
|||||||
@@ -112,7 +112,11 @@ function resolveApprovalMeta(status) {
|
|||||||
return { key: 'draft', label: '草稿', tone: 'draft' }
|
return { key: 'draft', label: '草稿', tone: 'draft' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['supplement', 'returned'].includes(normalized)) {
|
if (normalized === 'returned') {
|
||||||
|
return { key: 'supplement', label: '待提交', tone: 'warning' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === 'supplement') {
|
||||||
return { key: 'supplement', label: '待补充', tone: 'warning' }
|
return { key: 'supplement', label: '待补充', tone: 'warning' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +132,10 @@ function resolveApprovalMeta(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveWorkflowNode(claim, approvalMeta) {
|
function resolveWorkflowNode(claim, approvalMeta) {
|
||||||
|
if (String(claim?.status || '').trim().toLowerCase() === 'returned') {
|
||||||
|
return '待提交'
|
||||||
|
}
|
||||||
|
|
||||||
const rawNode = String(claim?.approval_stage || '').trim()
|
const rawNode = String(claim?.approval_stage || '').trim()
|
||||||
|
|
||||||
if (rawNode) {
|
if (rawNode) {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const BACKEND_STATUS_META = {
|
|||||||
paid: { key: 'completed', label: '已完成', tone: 'success' },
|
paid: { key: 'completed', label: '已完成', tone: 'success' },
|
||||||
completed: { key: 'completed', label: '已完成', tone: 'success' },
|
completed: { key: 'completed', label: '已完成', tone: 'success' },
|
||||||
supplement: { key: 'supplement', label: '待补充', tone: 'warning' },
|
supplement: { key: 'supplement', label: '待补充', tone: 'warning' },
|
||||||
returned: { key: 'supplement', label: '待补充', tone: 'warning' },
|
returned: { key: 'supplement', label: '待提交', tone: 'warning' },
|
||||||
rejected: { key: 'rejected', label: '已退回', tone: 'danger' },
|
rejected: { key: 'rejected', label: '已退回', tone: 'danger' },
|
||||||
cancelled: { key: 'rejected', label: '已退回', tone: 'danger' }
|
cancelled: { key: 'rejected', label: '已退回', tone: 'danger' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -535,7 +535,7 @@
|
|||||||
badge="退回单据"
|
badge="退回单据"
|
||||||
badge-tone="warning"
|
badge-tone="warning"
|
||||||
:title="`确认退回 ${selectedRow?.id || ''} 吗?`"
|
:title="`确认退回 ${selectedRow?.id || ''} 吗?`"
|
||||||
description="退回后该单据会进入待补充状态,申请人需要补充后重新提交。"
|
description="退回后该单据会回到待提交状态,申请人需要调整后重新提交并再次经过 AI 预审。"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
confirm-text="确认退回"
|
confirm-text="确认退回"
|
||||||
busy-text="退回中..."
|
busy-text="退回中..."
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<span>智能录入</span>
|
<span>智能录入</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="isDraftRequest"
|
v-if="isEditableRequest"
|
||||||
class="smart-entry-btn secondary"
|
class="smart-entry-btn secondary"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="actionBusy"
|
:disabled="actionBusy"
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<th class="col-amount">金额</th>
|
<th class="col-amount">金额</th>
|
||||||
<th class="col-attachment">附件材料</th>
|
<th class="col-attachment">附件材料</th>
|
||||||
<th v-if="hasExpenseRiskColumn" class="col-risk">系统校验</th>
|
<th v-if="hasExpenseRiskColumn" class="col-risk">系统校验</th>
|
||||||
<th v-if="isDraftRequest" class="col-action">操作</th>
|
<th v-if="isEditableRequest" class="col-action">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="isDraftRequest" class="expense-action-cell col-action">
|
<td v-if="isEditableRequest" class="expense-action-cell col-action">
|
||||||
<div v-if="editingExpenseId === item.id" class="row-action-group">
|
<div v-if="editingExpenseId === item.id" class="row-action-group">
|
||||||
<button
|
<button
|
||||||
class="inline-action primary"
|
class="inline-action primary"
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article v-if="isDraftRequest" class="detail-card panel validation-card">
|
<article v-if="isEditableRequest" class="detail-card panel validation-card">
|
||||||
<div class="validation-head">
|
<div class="validation-head">
|
||||||
<div>
|
<div>
|
||||||
<h3>AI建议</h3>
|
<h3>AI建议</h3>
|
||||||
@@ -393,10 +393,10 @@
|
|||||||
<i class="mdi mdi-arrow-left"></i>
|
<i class="mdi mdi-arrow-left"></i>
|
||||||
<span>返回报销列表</span>
|
<span>返回报销列表</span>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="isDraftRequest" class="approval-action-group" aria-label="申请操作">
|
<div v-if="isEditableRequest" class="approval-action-group" aria-label="申请操作">
|
||||||
<button class="reject-action" type="button" :disabled="actionBusy" @click="handleDeleteRequest">
|
<button class="reject-action" type="button" :disabled="actionBusy" @click="handleDeleteRequest">
|
||||||
<i class="mdi mdi-trash-can-outline"></i>
|
<i class="mdi mdi-trash-can-outline"></i>
|
||||||
{{ deleteBusy ? '删除中' : '删除草稿' }}
|
{{ deleteBusy ? '删除中' : deleteActionLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button class="approve-action" type="button" :disabled="!canSubmit" @click="handleSubmit">
|
<button class="approve-action" type="button" :disabled="!canSubmit" @click="handleSubmit">
|
||||||
<i class="mdi mdi-send-circle-outline"></i>
|
<i class="mdi mdi-send-circle-outline"></i>
|
||||||
@@ -500,7 +500,7 @@
|
|||||||
badge="退回单据"
|
badge="退回单据"
|
||||||
badge-tone="warning"
|
badge-tone="warning"
|
||||||
:title="`确认退回 ${request.id} 吗?`"
|
:title="`确认退回 ${request.id} 吗?`"
|
||||||
description="退回后该单据会进入待补充状态,申请人需要补充后重新提交。"
|
description="退回后该单据会回到待提交状态,申请人需要调整后重新提交并再次经过 AI 预审。"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
confirm-text="确认退回"
|
confirm-text="确认退回"
|
||||||
busy-text="退回中..."
|
busy-text="退回中..."
|
||||||
|
|||||||
@@ -428,9 +428,9 @@ export default {
|
|||||||
actionBusy.value = true
|
actionBusy.value = true
|
||||||
try {
|
try {
|
||||||
await returnExpenseClaim(row.claimId, {
|
await returnExpenseClaim(row.claimId, {
|
||||||
reason: '审批中心退回,请申请人补充后重新提交。'
|
reason: '审批中心退回,请申请人调整后重新提交。'
|
||||||
})
|
})
|
||||||
toast(`${row.id} 已退回待补充。`)
|
toast(`${row.id} 已退回待提交。`)
|
||||||
returnDialogOpen.value = false
|
returnDialogOpen.value = false
|
||||||
selectedClaimId.value = ''
|
selectedClaimId.value = ''
|
||||||
await reload()
|
await reload()
|
||||||
|
|||||||
@@ -264,21 +264,21 @@ function formatEmployeeHistoryTime(value) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const matched = raw.match(
|
const chineseMatched = raw.match(
|
||||||
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/
|
/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2})时(\d{1,2})分(?:\d{1,2}秒)?$/
|
||||||
)
|
)
|
||||||
if (!matched) {
|
if (chineseMatched) {
|
||||||
return raw
|
const [, year, month, day, hour, minute] = chineseMatched
|
||||||
|
return `${year}年${Number(month)}月${Number(day)}日${Number(hour)}时${Number(minute)}分`
|
||||||
}
|
}
|
||||||
|
|
||||||
const year = Number.parseInt(matched[1], 10)
|
const isoMatched = raw.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}))?/)
|
||||||
const month = Number.parseInt(matched[2], 10)
|
if (isoMatched) {
|
||||||
const day = Number.parseInt(matched[3], 10)
|
const [, year, month, day, hour = '0', minute = '0'] = isoMatched
|
||||||
const hour = Number.parseInt(matched[4] || '0', 10)
|
return `${year}年${Number(month)}月${Number(day)}日${Number(hour)}时${Number(minute)}分`
|
||||||
const minute = Number.parseInt(matched[5] || '0', 10)
|
}
|
||||||
const second = Number.parseInt(matched[6] || '0', 10)
|
|
||||||
|
|
||||||
return `${year}年${month}月${day}日${hour}时${minute}分${second}秒`
|
return raw.replace(/(\d{1,2}分)\d{1,2}秒$/, '$1')
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveOrganizationUnitCode(employee) {
|
function resolveOrganizationUnitCode(employee) {
|
||||||
|
|||||||
@@ -454,8 +454,9 @@ export default {
|
|||||||
|
|
||||||
const isTravelRequest = computed(() => request.value.detailVariant === 'travel')
|
const isTravelRequest = computed(() => request.value.detailVariant === 'travel')
|
||||||
const isDraftRequest = computed(() => request.value.approvalKey === 'draft')
|
const isDraftRequest = computed(() => request.value.approvalKey === 'draft')
|
||||||
|
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
|
||||||
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
||||||
const canDeleteRequest = computed(() => isDraftRequest.value || canManageCurrentClaim.value)
|
const canDeleteRequest = computed(() => isEditableRequest.value || canManageCurrentClaim.value)
|
||||||
const canReturnRequest = computed(() =>
|
const canReturnRequest = computed(() =>
|
||||||
canManageCurrentClaim.value
|
canManageCurrentClaim.value
|
||||||
&& request.value.approvalKey === 'in_progress'
|
&& request.value.approvalKey === 'in_progress'
|
||||||
@@ -584,7 +585,7 @@ export default {
|
|||||||
const uploadedExpenseCount = computed(() => expenseItems.value.filter((item) => item.attachments.length).length)
|
const uploadedExpenseCount = computed(() => expenseItems.value.filter((item) => item.attachments.length).length)
|
||||||
const hasExpenseRiskColumn = computed(() => expenseItems.value.some((item) => item.attachments.length))
|
const hasExpenseRiskColumn = computed(() => expenseItems.value.some((item) => item.attachments.length))
|
||||||
const expenseTableColumnCount = computed(
|
const expenseTableColumnCount = computed(
|
||||||
() => 5 + (hasExpenseRiskColumn.value ? 1 : 0) + (isDraftRequest.value ? 1 : 0)
|
() => 5 + (hasExpenseRiskColumn.value ? 1 : 0) + (isEditableRequest.value ? 1 : 0)
|
||||||
)
|
)
|
||||||
const expenseSummaryText = computed(
|
const expenseSummaryText = computed(
|
||||||
() => request.value.expenseTableSummary || '请继续补充票据、说明和系统校验结果。'
|
() => request.value.expenseTableSummary || '请继续补充票据、说明和系统校验结果。'
|
||||||
@@ -595,9 +596,9 @@ export default {
|
|||||||
|| '暂无附加说明。可在这里补充特殊背景、例外原因、补件计划或其他需要财务和审批人重点关注的信息。'
|
|| '暂无附加说明。可在这里补充特殊背景、例外原因、补件计划或其他需要财务和审批人重点关注的信息。'
|
||||||
)
|
)
|
||||||
const draftBlockingIssues = computed(() =>
|
const draftBlockingIssues = computed(() =>
|
||||||
isDraftRequest.value ? buildDraftBlockingIssues(request.value, expenseItems.value) : []
|
isEditableRequest.value ? buildDraftBlockingIssues(request.value, expenseItems.value) : []
|
||||||
)
|
)
|
||||||
const canSubmit = computed(() => isDraftRequest.value && draftBlockingIssues.value.length === 0 && !actionBusy.value)
|
const canSubmit = computed(() => isEditableRequest.value && draftBlockingIssues.value.length === 0 && !actionBusy.value)
|
||||||
const locationInputPlaceholder = computed(() => resolveLocationInputPlaceholder(expenseEditor.itemType))
|
const locationInputPlaceholder = computed(() => resolveLocationInputPlaceholder(expenseEditor.itemType))
|
||||||
|
|
||||||
function applyLocalExpenseItemPatch(itemId, patch) {
|
function applyLocalExpenseItemPatch(itemId, patch) {
|
||||||
@@ -807,7 +808,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function startExpenseEdit(item) {
|
function startExpenseEdit(item) {
|
||||||
if (!isDraftRequest.value || actionBusy.value) {
|
if (!isEditableRequest.value || actionBusy.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,7 +851,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddExpenseItem() {
|
async function handleAddExpenseItem() {
|
||||||
if (!isDraftRequest.value || actionBusy.value) {
|
if (!isEditableRequest.value || actionBusy.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,7 +885,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function triggerExpenseUpload(item) {
|
function triggerExpenseUpload(item) {
|
||||||
if (!isDraftRequest.value || actionBusy.value) {
|
if (!isEditableRequest.value || actionBusy.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,10 +1199,10 @@ export default {
|
|||||||
returnBusy.value = true
|
returnBusy.value = true
|
||||||
try {
|
try {
|
||||||
await returnExpenseClaim(request.value.claimId, {
|
await returnExpenseClaim(request.value.claimId, {
|
||||||
reason: '详情页退回,请申请人补充后重新提交。'
|
reason: '详情页退回,请申请人调整后重新提交。'
|
||||||
})
|
})
|
||||||
returnDialogOpen.value = false
|
returnDialogOpen.value = false
|
||||||
toast(`${request.value.id} 已退回待补充。`)
|
toast(`${request.value.id} 已退回待提交。`)
|
||||||
emit('request-updated', { claimId: request.value.claimId })
|
emit('request-updated', { claimId: request.value.claimId })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast(error?.message || '退回单据失败,请稍后重试。')
|
toast(error?.message || '退回单据失败,请稍后重试。')
|
||||||
@@ -1270,6 +1271,7 @@ export default {
|
|||||||
hasExpenseRiskColumn,
|
hasExpenseRiskColumn,
|
||||||
heroFactItems,
|
heroFactItems,
|
||||||
isDraftRequest,
|
isDraftRequest,
|
||||||
|
isEditableRequest,
|
||||||
isTravelRequest,
|
isTravelRequest,
|
||||||
locationInputPlaceholder,
|
locationInputPlaceholder,
|
||||||
openAiEntry,
|
openAiEntry,
|
||||||
|
|||||||
Reference in New Issue
Block a user