import dayjs from "dayjs"; import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js"; import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; import { mapSignModeToApi } from "../../ApproveManage/approve-template/approveTemplateConstants.js"; import { EXPENSE_CATEGORY_OPTIONS } from "../cost-reimburse/costReimburseUtils.js"; import { EXPENSE_SUBJECT_OPTIONS } from "../travel-reimburse/travelReimburseUtils.js"; import { mapTasksToFlowNodes } from "../../ApproveManage/approve-list/approveListConstants.js"; import { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js"; /** 报销类型:1-差旅报销,2-费用报销 */ export const FIN_REIMBURSEMENT_TYPE = { TRAVEL: "1", COST: "2", }; const REIMBURSEMENT_TYPE_LABEL = { [FIN_REIMBURSEMENT_TYPE.TRAVEL]: "差旅报销", [FIN_REIMBURSEMENT_TYPE.COST]: "费用报销", }; /** 归一化报销类型:1-差旅,2-费用 */ export function normalizeReimbursementType(val) { const s = String(val ?? "").trim(); if (s === "1" || s === FIN_REIMBURSEMENT_TYPE.TRAVEL) { return FIN_REIMBURSEMENT_TYPE.TRAVEL; } if (s === "2" || s === FIN_REIMBURSEMENT_TYPE.COST) { return FIN_REIMBURSEMENT_TYPE.COST; } return ""; } export function reimbursementTypeLabel(type) { return REIMBURSEMENT_TYPE_LABEL[normalizeReimbursementType(type)] || "—"; } export function getModuleKeyByReimbursementType(type) { const t = normalizeReimbursementType(type); if (t === FIN_REIMBURSEMENT_TYPE.TRAVEL) { return APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE; } if (t === FIN_REIMBURSEMENT_TYPE.COST) { return APPROVAL_MODULE_KEYS.COST_REIMBURSE; } return ""; } /** 优先接口 reimbursementType,其次页面 moduleKey / 入参 */ export function resolveReimbursementType(raw, fallback) { const fromApi = normalizeReimbursementType(raw?.reimbursementType); if (fromApi) return fromApi; return ( normalizeReimbursementType(fallback) || getReimbursementTypeByModuleKey(fallback) || "" ); } export function isTravelReimbursementType(type) { return ( resolveReimbursementType({ reimbursementType: type }, type) === FIN_REIMBURSEMENT_TYPE.TRAVEL ); } export function filterRowsByReimbursementType(rows, expectedType) { const expected = normalizeReimbursementType(expectedType); if (!expected) return rows || []; return (rows || []).filter((row) => { const t = resolveReimbursementType(row, expected); return t === expected; }); } export function getReimbursementTypeByModuleKey(moduleKey) { if (moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE) { return FIN_REIMBURSEMENT_TYPE.TRAVEL; } if (moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE) { return FIN_REIMBURSEMENT_TYPE.COST; } return ""; } export function unwrapFinReimbursementPage(res) { const data = res?.data ?? res; if (!data || typeof data !== "object") { return { records: [], total: 0 }; } if (Array.isArray(data.records)) { return { records: data.records, total: Number(data.total ?? 0) }; } const nested = data.data; if (nested && typeof nested === "object" && Array.isArray(nested.records)) { return { records: nested.records, total: Number(nested.total ?? 0) }; } return { records: [], total: 0 }; } /** 详情接口 data 解包 */ export function unwrapFinReimbursementDetail(res) { const data = res?.data ?? res; if (!data || typeof data !== "object") return {}; if (data.billNo != null || data.id != null || data.reimbursementType != null) { return data; } const nested = data.data; if (nested && typeof nested === "object" && !Array.isArray(nested)) { return nested; } if (data.finReimbursementDto && typeof data.finReimbursementDto === "object") { return data.finReimbursementDto; } return data; } /** 详情查询参数(query finReimbursementDto) */ export function buildFinReimbursementDetailParams(id) { const raw = id?.id != null ? id.id : id; const n = toNumber(raw); return { finReimbursementDto: { id: n != null ? n : raw } }; } /** 详情 DTO → 页面行(按 reimbursementType 映射,含 tasks / storageBlobVOList) */ export function mapFinReimbursementDetailRow(raw, reimbursementTypeOrModuleKey) { const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey); let mapped = {}; if (type === FIN_REIMBURSEMENT_TYPE.TRAVEL) { mapped = mapTravelReimbursementRow(raw); } else if (type === FIN_REIMBURSEMENT_TYPE.COST) { mapped = mapCostReimbursementRow(raw); } else { mapped = raw || {}; } let formApprovalFlowNodes = mapNodesToFormFlow(resolveRowApiNodes(raw)); if (!formApprovalFlowNodes.length && Array.isArray(raw?.tasks) && raw.tasks.length) { formApprovalFlowNodes = mapNodesToFormFlow(mapTasksToFlowNodes(raw.tasks)); } const enriched = applyFinReimbursementDetailEnrichment(mapped, raw); return { ...enriched, approvalFlowNodes: formApprovalFlowNodes.length ? formApprovalFlowNodes : enriched.approvalFlowNodes, reimbursementType: type, reimbursementTypeLabel: reimbursementTypeLabel(type), moduleKey: getModuleKeyByReimbursementType(type), }; } /** 单据状态 → 页面 approvalResult(兼容 statusLabel) */ export function mapBillStatusToApprovalResult(billStatus) { const upper = String(billStatus ?? "").trim().toUpperCase(); if (upper === "DRAFT") return "draft"; if (upper === "IN_APPROVAL") return "pending"; if (upper === "APPROVED") return "approved"; if (upper === "REJECTED") return "rejected"; if (upper === "WITHDRAWN") return "cancelled"; if (upper === "PAID") return "paid"; return "pending"; } function pickApplicantQuery(searchForm = {}) { const kw = (searchForm.applicantKeyword || "").trim(); if (!kw) return {}; if (/[\u4e00-\u9fa5]/.test(kw)) return { applicantName: kw }; return { applicantCode: kw }; } /** 组装 listPage 查询参数(page + finReimbursementDto) */ export function buildFinReimbursementListParams({ page, searchForm, reimbursementType, extraDto = {}, }) { const dto = { reimbursementType, ...pickApplicantQuery(searchForm), ...(extraDto && typeof extraDto === "object" ? extraDto : {}), }; if (searchForm?.billStatus) { dto.billStatus = searchForm.billStatus; } const range = searchForm?.createTimeRange ?? searchForm?.applyDateRange ?? (searchForm?.applyTimeFrom || searchForm?.applyTimeTo ? [searchForm.applyTimeFrom, searchForm.applyTimeTo] : null); if (Array.isArray(range) && range[0]) { dto.createTimeStart = range[0]; } if (Array.isArray(range) && range[1]) { dto.createTimeEnd = range[1]; } if (reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL) { if (searchForm?.travelStartFrom) { dto.startTimeStart = searchForm.travelStartFrom; } if (searchForm?.travelEndTo) { dto.endTimeEnd = searchForm.travelEndTo; } } return { page: { current: page.current, size: page.size, }, finReimbursementDto: dto, }; } function pickTravelField(obj, keys) { if (!obj || typeof obj !== "object") return ""; for (const key of keys) { const v = obj[key]; if (v != null && v !== "") return v; } return ""; } /** 兼容 list/detail 多种差旅子对象结构 */ export function pickTravelFromRow(row) { if (!row || typeof row !== "object") return {}; const nested = (row.travel && typeof row.travel === "object" ? row.travel : null) || row.finReimbursementTravel || row.finReimbursementTravelDto || row.travelDto || row.travelVO || {}; const src = nested && typeof nested === "object" && Object.keys(nested).length ? nested : row; return { startTime: pickTravelField(src, [ "startTime", "travelStartTime", "startDate", "travelStartDate", "departureTime", ]), endTime: pickTravelField(src, [ "endTime", "travelEndTime", "endDate", "travelEndDate", "returnTime", ]), travelDays: src.travelDays, departureCity: pickTravelField(src, [ "departureCity", "departurePlace", "departure", ]), destinationCity: pickTravelField(src, [ "destinationCity", "destination", "destinationPlace", ]), hotelStandard: src.hotelStandard, lodgingDays: src.lodgingDays ?? src.hotelDays, mealAllowance: src.mealAllowance ?? src.livingSubsidy, transportAllowance: src.transportAllowance ?? src.transportSubsidy, lodgingLimit: src.lodgingLimit, withinStandard: src.withinStandard, standardTag: src.standardTag || "", id: src.id, reimbursementId: src.reimbursementId, }; } /** 列表/详情时间展示(ISO → YYYY-MM-DD HH:mm:ss) */ export function formatReimbursementDateTime(val) { if (val == null || val === "") return ""; const d = dayjs(val); if (!d.isValid()) return String(val); const raw = String(val); const hasTime = raw.includes("T") || /:\d{2}/.test(raw); return hasTime ? d.format("YYYY-MM-DD HH:mm:ss") : d.format("YYYY-MM-DD"); } /** 接口行 → 差旅报销列表行(兼容 useTravelReimburse 字段) */ export function mapTravelReimbursementRow(row) { if (!row) return {}; const travel = pickTravelFromRow(row); const details = Array.isArray(row.details) ? row.details : []; const base = { ...row, id: row.id, reimbursementId: row.id, approvalInstanceId: row.approvalInstanceId, reimburseNo: row.billNo || "", applicantId: row.applicantId, applicantNo: row.applicantCode || "", applicantName: row.applicantName || "", employeeNo: row.applicantCode || "", employeeName: row.applicantName || "", applicantDeptName: row.applicantDeptName || "", reimburseReason: row.reason || "", travelStartTime: formatReimbursementDateTime(travel.startTime), travelEndTime: formatReimbursementDateTime(travel.endTime), travelDays: travel.travelDays, departurePlace: travel.departureCity || "", destination: travel.destinationCity || "", hotelStandard: travel.hotelStandard, hotelDays: travel.lodgingDays, livingSubsidy: travel.mealAllowance, transportSubsidy: travel.transportAllowance, lodgingLimit: travel.lodgingLimit, needSpecialApproval: travel.withinStandard === "0" || travel.withinStandard === 0, standardTag: travel.standardTag || "", applyAmount: row.applyAmount, payee: row.payeeName || "", payeeAccount: row.payeeAccount || "", payeeBank: row.payeeBank || "", billStatus: row.billStatus, approvalResult: mapBillStatusToApprovalResult(row.billStatus), createTime: formatReimbursementDateTime(row.createTime), expenseDetails: details.map((d) => ({ ...d, expenseSubject: d.expenseCategory, })), travel: row.travel && typeof row.travel === "object" && Object.keys(row.travel).length ? row.travel : travel, details, nodes: resolveRowApiNodes(row), approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)), tasks: row.tasks || [], approvalFlowSummary: buildApprovalFlowSummaryForRow(row), }; return base; } /** 接口行 → 费用报销列表行(兼容 useCostReimburse 字段) */ export function mapCostReimbursementRow(row) { if (!row) return {}; const details = Array.isArray(row.details) ? row.details : []; const apiNodes = resolveRowApiNodes(row); const approvalFlowNodes = mapNodesToFormFlow(apiNodes); return { ...row, id: row.id, reimbursementId: row.id, approvalInstanceId: row.approvalInstanceId, reimburseNo: row.billNo || "", applicantId: row.applicantId, applicantNo: row.applicantCode || "", applicantName: row.applicantName || "", employeeNo: row.applicantCode || "", employeeName: row.applicantName || "", applicantDeptName: row.applicantDeptName || "", reimburseReason: row.reason || "", expenseCategory: row.expenseType || "", applyAmount: row.applyAmount, applyTime: formatReimbursementDateTime(row.createTime), payee: row.payeeName || "", payeeAccount: row.payeeAccount || "", bankBranch: row.payeeBank || "", billStatus: row.billStatus, approvalResult: mapBillStatusToApprovalResult(row.billStatus), createTime: formatReimbursementDateTime(row.createTime), expenseDetails: details.map((d) => ({ ...d, expenseSubject: d.expenseCategory, })), details, nodes: apiNodes, approvalFlowNodes, tasks: row.tasks || [], approvalFlowSummary: buildApprovalFlowSummaryForRow({ ...row, nodes: apiNodes, approvalFlowNodes, }), }; } function toNumber(val) { if (val == null || val === "") return undefined; const n = Number(val); return Number.isNaN(n) ? undefined : n; } function expenseSubjectToCategory(subject) { const hit = EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === subject); return hit?.label || subject || ""; } function expenseCategoryToType(category) { const hit = EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === category); return hit?.label || category || ""; } /** 列表/详情行上的审批节点(listPage 常不返回,需详情补全) */ export function resolveRowApiNodes(row) { if (!row || typeof row !== "object") return []; const list = row.nodes || row.flowNodes || row.approveNodes || row.finReimbursementNodes || row.nodeList || row.reimbursementNodeList || []; return Array.isArray(list) ? list : []; } function sortFlowNodesByLevel(nodes = []) { return [...(Array.isArray(nodes) ? nodes : [])].sort((a, b) => { const la = Number(a?.levelNo ?? a?.nodeOrder ?? a?.sortOrder ?? 0); const lb = Number(b?.levelNo ?? b?.nodeOrder ?? b?.sortOrder ?? 0); return la - lb; }); } function formatApiNodeApproverLabel(node, index) { if (!node || typeof node !== "object") return ""; const approvers = Array.isArray(node.approvers) ? node.approvers : []; const names = approvers .map((a) => (a?.approverName || "").trim()) .filter(Boolean); if (names.length) return names.join("/"); return (node.approverName || "").trim() || `节点${index + 1}`; } /** 接口 nodes → 页面审批流(单审批人节点) */ export function mapNodesToFormFlow(nodes = []) { return sortFlowNodesByLevel(nodes).map((n, i) => { const approvers = Array.isArray(n.approvers) ? n.approvers : []; const first = approvers[0] || null; const names = approvers .map((a) => (a?.approverName || "").trim()) .filter(Boolean); return { ...n, nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1, signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign", approverId: toNumber(first?.approverId ?? n.approverId) ?? first?.approverId ?? n.approverId ?? null, approverName: names.join("、") || first?.approverName || n.approverName || "", nodeStatus: n.nodeStatus, }; }); } function formatTasksToFlowSummary(tasks = []) { const list = sortFlowNodesByLevel( (Array.isArray(tasks) ? tasks : []).map((t, i) => ({ levelNo: t.levelNo ?? t.taskLevel ?? i + 1, approverName: (t.approverName || t.operatorName || t.createUserName || "").trim() || "", })) ); const parts = list.map((t) => t.approverName).filter(Boolean); return parts.length ? parts.join(" → ") : ""; } function buildApprovalFlowSummaryForRow(row) { const apiNodes = sortFlowNodesByLevel(resolveRowApiNodes(row)); let flowNodes = row?.approvalFlowNodes?.length > 0 ? sortFlowNodesByLevel(row.approvalFlowNodes) : mapNodesToFormFlow(apiNodes); if (!flowNodes.length && apiNodes.length) { const line = apiNodes .map((n, i) => formatApiNodeApproverLabel(n, i)) .filter(Boolean) .join(" → "); if (line) return line; } if (!flowNodes.length) { const fromTasks = formatTasksToFlowSummary(row?.tasks); if (fromTasks) return fromTasks; return "—"; } return flowNodes .map((n, i) => { const name = (n.approverName || "").trim() || `节点${i + 1}`; if (n.nodeStatus === "finish") return `${name}✓`; if (n.nodeStatus === "error") return `${name}✗`; if (n.nodeStatus === "process") return `${name}…`; return name; }) .join(" → "); } /** 列表「审批流程」列文案 */ export function formatApprovalFlowSummary(row) { return buildApprovalFlowSummaryForRow(row); } /** listPage 常不带完整 nodes,列表加载后统一拉详情补全多级审批流程 */ export async function enrichReimbursementListRowsWithApprovalFlow( rows, reimbursementType ) { const list = Array.isArray(rows) ? rows : []; if (!list.length) return list; const needIds = list .map((r) => resolveReimbursementDeleteId(r)) .filter((id) => id != null); if (!needIds.length) return list; const detailById = new Map(); await Promise.all( needIds.map(async (id) => { try { const res = await getFinReimbursementDetail(id); detailById.set(String(id), unwrapFinReimbursementDetail(res)); } catch { /* 单行失败不影响列表 */ } }) ); const mapRow = reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL ? mapTravelReimbursementRow : mapCostReimbursementRow; return list.map((row) => { const id = resolveReimbursementDeleteId(row); const detail = id != null ? detailById.get(String(id)) : null; if (!detail) return row; const merged = { ...row, ...detail, id: row.id ?? detail.id, reimbursementId: row.reimbursementId ?? row.id ?? detail.id, reimbursementType: detail.reimbursementType ?? row.reimbursementType, }; return mapRow(merged); }); } /** 表单上的审批流(兼容 approvalFlowNodes / nodes / flowNodes) */ export function resolveFormApprovalFlowNodes(form) { const list = form?.approvalFlowNodes ?? form?.nodes ?? form?.flowNodes ?? []; return Array.isArray(list) ? list : []; } /** 页面审批节点 → 接口 nodes */ export function mapApprovalFlowNodesToApi(nodes = [], templateId) { const list = Array.isArray(nodes) ? nodes : []; return list .map((n, i) => { let approvers = []; if (Array.isArray(n.approvers) && n.approvers.length) { approvers = n.approvers .filter((a) => a?.approverId != null && a.approverId !== "") .map((a, idx) => { const item = { approverId: toNumber(a.approverId) ?? a.approverId, approverName: a.approverName || "", sortNo: a.sortNo ?? idx + 1, }; if (a.id != null) item.id = a.id; if (a.nodeId != null) item.nodeId = a.nodeId; if (a.templateId != null) item.templateId = a.templateId; else if (templateId != null) item.templateId = templateId; if (a.roleKey) item.roleKey = a.roleKey; return item; }); } else if (n.approverId != null && n.approverId !== "") { const item = { approverId: toNumber(n.approverId) ?? n.approverId, approverName: n.approverName || "", sortNo: 1, }; if (n.roleKey) item.roleKey = n.roleKey; approvers = [item]; } if (!approvers.length) return null; const node = { levelNo: n.levelNo ?? n.nodeOrder ?? n.sortOrder ?? i + 1, approveType: n.approveType || mapSignModeToApi(n.signMode), approvers, }; if (n.id != null) node.id = n.id; if (n.templateId != null) node.templateId = n.templateId; else if (templateId != null) node.templateId = templateId; if (n.roleKey) node.roleKey = n.roleKey; return node; }) .filter(Boolean); } /** 保存前校验 nodes 已配置 */ export function validateReimbursementApprovalNodes(dto) { if (Array.isArray(dto?.nodes) && dto.nodes.length > 0) { return { ok: true }; } return { ok: false, message: "请配置审批流程并选择审批人" }; } function mapDetailsToApi(details = []) { return (details || []).map((d, i) => { const item = { rowNo: d.rowNo ?? i + 1, invoiceDate: d.invoiceDate || undefined, expenseCategory: expenseSubjectToCategory(d.expenseSubject ?? d.expenseCategory), amount: toNumber(d.amount), description: d.description || "", invoiceNo: d.invoiceNo || undefined, invoiceType: d.invoiceType || undefined, invoiceAmount: toNumber(d.invoiceAmount), taxRate: toNumber(d.taxRate), taxAmount: toNumber(d.taxAmount), remark: d.remark || undefined, }; if (d.id != null && !String(d.id).startsWith("ed_")) { item.id = toNumber(d.id) ?? d.id; } if (d.reimbursementId != null) item.reimbursementId = toNumber(d.reimbursementId); return item; }); } function sumDetailAmount(details = []) { const sum = (details || []).reduce((s, d) => s + (Number(d.amount) || 0), 0); return Math.round(sum * 100) / 100; } /** 表单附件列表(兼容多种字段名) */ export function resolveFormAttachmentList(form) { const list = form?.attachmentList ?? form?.storageBlobDTOs ?? form?.storageBlobVOList ?? form?.invoiceAttachments ?? []; return Array.isArray(list) ? list : []; } /** 页面附件 → 保存 DTO(storageBlobVOList / storageBlobDTOs) */ export function mapFormAttachmentsToApi(list = [], reimbursementId) { const rid = reimbursementId != null ? toNumber(reimbursementId) ?? reimbursementId : undefined; return (list || []) .map((item, i) => { if (!item) return null; const url = item.url || item.fileUrl || item.downloadUrl || item.downloadURL || item.previewUrl || item.previewURL || item.link || ""; const name = item.fileName || item.originalFilename || item.originalFileName || item.blobName || item.name || `附件${i + 1}`; const idRaw = item.id ?? item.blobId; const isTempId = idRaw != null && /^(inv_|att_|ed_|local_)/.test(String(idRaw)); if (!url && (idRaw == null || isTempId)) return null; const blob = { fileName: name, originalFilename: name, fileUrl: url || undefined, url: url || undefined, }; if (idRaw != null && !isTempId) { const n = toNumber(idRaw); blob.id = n != null ? n : idRaw; blob.blobId = blob.id; } if (rid != null) blob.reimbursementId = rid; return blob; }) .filter(Boolean); } function applyStorageBlobsToSaveDto(dto, form) { const blobs = mapFormAttachmentsToApi( resolveFormAttachmentList(form), dto?.id ?? form?.reimbursementId ?? form?.id ); if (blobs.length) { dto.storageBlobVOList = blobs; dto.storageBlobDTOs = blobs; } return dto; } /** 修改时补齐主表与子表关联 ID */ function applyReimbursementRelations(dto) { const rid = dto?.id; if (rid == null) return dto; if (dto.travel && typeof dto.travel === "object") { dto.travel.reimbursementId = rid; } if (Array.isArray(dto.details)) { dto.details.forEach((d) => { d.reimbursementId = rid; }); } const blobLists = [dto.storageBlobVOList, dto.storageBlobDTOs].filter(Array.isArray); blobLists.forEach((list) => { list.forEach((b) => { b.reimbursementId = rid; }); }); return dto; } function resolveReimbursementId(form) { const rawId = form?.reimbursementId ?? form?.id; if (rawId == null || rawId === "" || String(rawId).startsWith("local_")) { return undefined; } return toNumber(rawId) ?? rawId; } /** 差旅表单 → FinReimbursementDto */ export function buildTravelReimbursementSaveDto(form, { computeTravelDays } = {}) { const details = mapDetailsToApi(form.expenseDetails); const detailTotal = sumDetailAmount(form.expenseDetails); const applyAmount = toNumber(form.applyAmount) ?? detailTotal; const travelDays = form.travelDays != null ? toNumber(form.travelDays) : computeTravelDays?.(form.travelStartTime, form.travelEndTime); const dto = { reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL, expenseType: "差旅费", applicantId: toNumber(form.applicantId), applicantCode: form.employeeNo || form.applicantNo || "", applicantName: form.employeeName || form.applicantName || "", applicantDeptId: toNumber(form.applicantDeptId), applicantDeptName: form.applicantDeptName || form.deptName || "", reason: (form.reimburseReason || "").trim(), applyAmount, detailTotalAmount: detailTotal, payeeName: form.payee || "", payeeAccount: form.payeeAccount || undefined, payeeBank: form.payeeBank || undefined, billStatus: "IN_APPROVAL", deptId: toNumber(form.deptId), travel: { startTime: form.travelStartTime || undefined, endTime: form.travelEndTime || undefined, travelDays, departureCity: form.departurePlace || "", destinationCity: form.destination || "", hotelStandard: toNumber(form.hotelStandard), lodgingDays: toNumber(form.hotelDays), mealAllowance: toNumber(form.livingSubsidy), transportAllowance: toNumber(form.transportSubsidy), lodgingLimit: toNumber(form.lodgingLimit), standardTag: form.standardTag || (form.needSpecialApproval ? "超标特批" : "在标准范围内"), withinStandard: form.needSpecialApproval ? "0" : "1", }, details, nodes: mapApprovalFlowNodesToApi( resolveFormApprovalFlowNodes(form), form.templateId ), }; const id = resolveReimbursementId(form); if (id != null) dto.id = id; if (form.billNo || form.reimburseNo) { dto.billNo = form.billNo || form.reimburseNo; } if (form.approvalInstanceId != null) { dto.approvalInstanceId = toNumber(form.approvalInstanceId); } if (form.approveProcessId != null) { dto.approveProcessId = toNumber(form.approveProcessId); } if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id); applyStorageBlobsToSaveDto(dto, form); return applyReimbursementRelations(dto); } /** 费用表单 → FinReimbursementDto */ export function buildCostReimbursementSaveDto(form) { const details = mapDetailsToApi(form.expenseDetails); const detailTotal = sumDetailAmount(form.expenseDetails); const applyAmount = toNumber(form.applyAmount) ?? detailTotal; const dto = { reimbursementType: FIN_REIMBURSEMENT_TYPE.COST, expenseType: expenseCategoryToType(form.expenseCategory), applicantId: toNumber(form.applicantId), applicantCode: form.employeeNo || form.applicantNo || "", applicantName: form.employeeName || form.applicantName || "", applicantDeptId: toNumber(form.applicantDeptId), applicantDeptName: form.applicantDeptName || form.deptName || "", reason: (form.reimburseReason || "").trim(), applyAmount, detailTotalAmount: detailTotal, payeeName: form.payee || "", payeeAccount: form.payeeAccount || "", payeeBank: form.bankBranch || form.payeeBank || "", billStatus: "IN_APPROVAL", deptId: toNumber(form.deptId), details, nodes: mapApprovalFlowNodesToApi( resolveFormApprovalFlowNodes(form), form.templateId ), }; const id = resolveReimbursementId(form); if (id != null) dto.id = id; if (form.billNo || form.reimburseNo) { dto.billNo = form.billNo || form.reimburseNo; } if (form.approvalInstanceId != null) { dto.approvalInstanceId = toNumber(form.approvalInstanceId); } if (form.approveProcessId != null) { dto.approveProcessId = toNumber(form.approveProcessId); } applyStorageBlobsToSaveDto(dto, form); return applyReimbursementRelations(dto); } /** 列表行主键(删除/修改用 fin_reimbursement.id) */ export function resolveReimbursementDeleteId(row) { const raw = row?.reimbursementId ?? row?.id; if (raw == null || raw === "" || String(raw).startsWith("local_")) { return undefined; } const n = toNumber(raw); return n != null ? n : raw; } /** 是否允许删除(审批中、已通过、已付款不可删) */ export function canDeleteReimbursementRow(row) { const key = mapBillStatusToApprovalResult( row?.billStatus ?? row?.approvalResult ?? row?.status ); return key !== "pending" && key !== "approved" && key !== "paid"; } /** 是否允许编辑(与删除规则一致) */ export function canEditReimbursementRow(row) { return canDeleteReimbursementRow(row); } /** 修改场景必须带主键 ID */ export function validateReimbursementPersistDto(dto, isEdit) { if (!isEdit) return { ok: true }; if (dto?.id != null && dto.id !== "") return { ok: true }; return { ok: false, message: "无法修改:缺少报销单 ID" }; }