feat(research): 完善经费拨付流程支持财务报销审批
- 添加报销审批中和已驳回状态定义
- 在表格中增加财务报销单号和驳回原因字段显示
- 移除拨付状态下拉选择,改为自动管理状态流转
- 强制凭证必传,确保报销流程完整性
- 更新保存成功提示信息,明确财务审批状态
- 添加驳回原因显示和重新发起报销功能
- 增加顶部状态提醒,展示审批进度信息
- 优化标签显示逻辑,统一凭证状态标识
- 修改API注释,明确状态码含义和操作说明
| | |
| | | amount: number; // 拨付金额(元) |
| | | payDate?: string; // 拨付日期 |
| | | payee?: string; // 收款单位/账户名 |
| | | status?: number; // 拨付状态,字典 research_fund_status |
| | | status?: number; // 拨付状态,字典 research_fund_status(0-待拨付 15-报销审批中 20-已拨付 30-已驳回) |
| | | reimbursementId?: number; // 关联财务报销单 ID |
| | | reimbursementNo?: string; // 关联财务报销单号 |
| | | rejectReason?: string; // 财务报销驳回原因(BPM 回写) |
| | | rejectTime?: string; // 财务报销驳回时间 |
| | | remark?: string; // 备注 |
| | | creator?: string; // 创建人 ID |
| | | creatorName?: string; // 创建人姓名 |
| | |
| | | ); |
| | | } |
| | | |
| | | /** 新增经费拨付记录 */ |
| | | /** 登记经费拨付记录(登记后自动生成并提交财务报销审批) */ |
| | | export function createFundRecord(data: ResearchFundRecordApi.FundRecord) { |
| | | return requestClient.post('/research/fund-record/create', data); |
| | | } |
| | | |
| | | /** 修改经费拨付记录(仅待拨付可改) */ |
| | | /** 修改并重新发起报销(仅「待拨付」「已驳回」可改) */ |
| | | export function updateFundRecord(data: ResearchFundRecordApi.FundRecord) { |
| | | return requestClient.put('/research/fund-record/update', data); |
| | | } |
| | |
| | | /** 经费拨付状态(对应字典 research_fund_status) */ |
| | | export const FundStatus = { |
| | | PENDING: 0, // 待拨付 |
| | | APPROVING: 15, // 报销审批中 |
| | | PAID: 20, // 已拨付 |
| | | REJECTED: 30, // 已驳回 |
| | | } as const; |
| | | |
| | | /** 课题申报书表单配置 */ |
| | |
| | | { |
| | | field: 'status', |
| | | title: '拨付状态', |
| | | minWidth: 100, |
| | | minWidth: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_FUND_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'reimbursementNo', |
| | | title: '财务报销单号', |
| | | minWidth: 160, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'rejectReason', |
| | | title: '驳回原因', |
| | | minWidth: 200, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: '拨付凭证', |
| | | minWidth: 100, |
| | |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Alert, message } from 'ant-design-vue'; |
| | |
| | | const isEdit = ref(false); |
| | | /** 申请经费减去已登记拨付金额后的可用余额 */ |
| | | const remainder = ref<number>(); |
| | | /** 编辑「已驳回」记录时展示的财务驳回原因 */ |
| | | const rejectReason = ref(''); |
| | | |
| | | const alertMessage = computed(() => { |
| | | if (remainder.value === undefined) { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: '拨付状态', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_FUND_STATUS, 'number'), |
| | | placeholder: '请选择拨付状态', |
| | | }, |
| | | rules: 'selectRequired', |
| | | help: '选择「已拨付」时必须上传拨付凭证附件', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '备注', |
| | | component: 'Textarea', |
| | |
| | | multiple: true, |
| | | showDescription: true, |
| | | }, |
| | | rules: 'required', |
| | | help: '必填:凭证将随自动生成的财务报销单一起送交财务审批', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ], |
| | |
| | | } |
| | | const values = |
| | | (await formApi.getValues()) as ResearchFundRecordApi.FundRecord; |
| | | // 「已拨付」必须有凭证,先在前端拦一道,避免用户白等一次请求 |
| | | if ( |
| | | values.status === FundStatus.PAID && |
| | | (!values.blobIds || values.blobIds.length === 0) |
| | | ) { |
| | | message.warning('拨付状态为「已拨付」时,请先上传拨付凭证附件'); |
| | | // 凭证必填,先在前端拦一道,避免用户白等一次请求 |
| | | if (!values.blobIds || values.blobIds.length === 0) { |
| | | message.warning('请先上传本次拨付凭证,凭证将随财务报销单送交财务审批'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | amount: values.amount, |
| | | payDate: values.payDate, |
| | | payee: values.payee, |
| | | status: values.status, |
| | | remark: values.remark, |
| | | blobIds: values.blobIds ?? [], |
| | | blobIds: values.blobIds, |
| | | } as ResearchFundRecordApi.FundRecord; |
| | | try { |
| | | await (data.id ? updateFundRecord(data) : createFundRecord(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('保存成功'); |
| | | message.success( |
| | | data.id ? '已重新发起财务报销审批' : '登记成功,已自动提交财务报销审批', |
| | | ); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | if (!isOpen) { |
| | | isEdit.value = false; |
| | | remainder.value = undefined; |
| | | rejectReason.value = ''; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | |
| | | }>(); |
| | | isEdit.value = !!data.record; |
| | | remainder.value = data.remainder; |
| | | rejectReason.value = |
| | | data.record?.status === FundStatus.REJECTED |
| | | ? data.record.rejectReason || '财务未填写驳回原因' |
| | | : ''; |
| | | await formApi.resetForm(); |
| | | if (data.record) { |
| | | const blobIds = (data.record.attachmentList ?? []).map((item) => ({ |
| | |
| | | await formApi.setValues( |
| | | { |
| | | applicationId: data.applicationId, |
| | | status: FundStatus.PENDING, |
| | | }, |
| | | false, |
| | | ); |
| | |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="isEdit ? '编辑拨款记录' : '登记拨款记录'" |
| | | :title="isEdit ? '修改并重新发起报销' : '登记拨款(登记后自动提交财务报销审批)'" |
| | | class="w-1/2" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <Alert |
| | | v-if="rejectReason" |
| | | class="mx-3 mb-3" |
| | | type="error" |
| | | show-icon |
| | | :message="`财务报销已驳回:${rejectReason}`" |
| | | /> |
| | | <Alert |
| | | v-if="alertMessage" |
| | | class="mx-3 mb-3" |
| | | type="info" |
| | |
| | | () => (Number(application.value?.budgetAmount) || 0) - registeredAmount.value, |
| | | ); |
| | | |
| | | /** 各状态记录数量,用于顶部提示(0 条时不提示) */ |
| | | const statusCount = (status: number) => |
| | | records.value.filter((item) => item.status === status).length; |
| | | |
| | | const [FundFormModal, fundFormModalApi] = useVbenModal({ |
| | | connectedComponent: FundForm, |
| | | destroyOnClose: true, |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="statusCount(FundStatus.APPROVING) > 0" |
| | | type="info" |
| | | show-icon |
| | | :message="`有 ${statusCount(FundStatus.APPROVING)} 条拨付记录正在报销审批中,暂不可修改;财务审批结果将自动回写本记录`" |
| | | /> |
| | | <Alert |
| | | v-if="statusCount(FundStatus.PAID) > 0" |
| | | type="success" |
| | | show-icon |
| | | :message="`有 ${statusCount(FundStatus.PAID)} 条拨付记录财务已通过,拨款完成`" |
| | | /> |
| | | <Alert |
| | | v-if="statusCount(FundStatus.REJECTED) > 0" |
| | | type="error" |
| | | show-icon |
| | | :message="`有 ${statusCount(FundStatus.REJECTED)} 条拨付记录财务已驳回,请修改金额或凭证后点「重新发起报销」`" |
| | | /> |
| | | |
| | | <div v-if="editable"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | <Grid> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'warning'"> |
| | | {{ |
| | | row.hasAttachment |
| | | ? '已上传' |
| | | : row.status === FundStatus.PAID |
| | | ? '缺失' |
| | | : '不需要' |
| | | }} |
| | | {{ row.hasAttachment ? '已上传' : '缺失' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: '重新发起报销', |
| | | type: 'link', |
| | | icon: ACTION_ICON.REFRESH, |
| | | auth: ['research:fund:update'], |
| | | ifShow: () => row.status === FundStatus.REJECTED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: '删除', |
| | | type: 'link', |
| | | danger: true, |