feat(P20T1-1,P20T1-2,P20T1-3): 差旅模型、申请/订单接口与工作流分支

这个提交包含在:
2026-03-10 19:58:46 +08:00
父节点 427ae2db53
当前提交 7b78945112
共修改 19 个文件,包含 2266 行新增和 59 行删除
+166
查看文件
@@ -458,6 +458,15 @@ function getWorkflowEntityMeta(entityType, entityRecord, entityId) {
};
}
if (normalizedEntityType === "travel_request") {
return {
label: "差旅申请",
reference: entityRecord?.request_no ?? entityId,
targetType: "travel_request",
detailPath: `/travel-requests?id=${entityId}`,
};
}
return {
label: "审批单",
reference: entityId,
@@ -525,6 +534,18 @@ async function loadPurchaseNotifyContext(tx, purchaseRequestId) {
});
}
async function loadTravelNotifyContext(tx, travelRequestId) {
return tx.travelRequest.findUnique({
where: { id: travelRequestId },
select: {
id: true,
request_no: true,
applicant_employee_id: true,
submitter_id: true,
},
});
}
async function loadWorkflowNotifyContext(tx, entityType, entityId) {
const normalizedEntityType = entityType === 'procurement' ? 'purchase_request' : entityType;
@@ -552,6 +573,14 @@ async function loadWorkflowNotifyContext(tx, entityType, entityId) {
};
}
if (normalizedEntityType === 'travel_request') {
const record = await loadTravelNotifyContext(tx, entityId);
return {
record,
recipientEmployeeIds: uniq([record?.applicant_employee_id, record?.submitter_id]),
};
}
return {
record: null,
recipientEmployeeIds: [],
@@ -565,12 +594,14 @@ function resolveWorkflowOutcomeNotificationType(entityType, outcome) {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementApproved;
if (normalizedEntityType === 'leave') return NOTIFICATION_TYPES.leaveApproved;
if (normalizedEntityType === 'purchase_request') return NOTIFICATION_TYPES.purchaseRequestApproved;
if (normalizedEntityType === 'travel_request') return NOTIFICATION_TYPES.travelRequestApproved;
}
if (outcome === 'rejected') {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementRejected;
if (normalizedEntityType === 'leave') return NOTIFICATION_TYPES.leaveRejected;
if (normalizedEntityType === 'purchase_request') return NOTIFICATION_TYPES.purchaseRequestRejected;
if (normalizedEntityType === 'travel_request') return NOTIFICATION_TYPES.travelRequestRejected;
}
if (outcome === 'returned') {
@@ -973,6 +1004,50 @@ async function workflowRoutes(fastify) {
},
},
},
travel_request: {
select: {
id: true,
request_no: true,
status: true,
purpose: true,
trip_type: true,
start_date: true,
end_date: true,
estimated_amount: true,
applicant: {
select: {
id: true,
employee_no: true,
name: true,
},
},
legal_entity: {
select: {
id: true,
code: true,
name: true,
},
},
cost_center: {
select: {
id: true,
code: true,
name: true,
},
},
segments: {
orderBy: [{ sort_order: "asc" }, { created_at: "asc" }],
select: {
id: true,
sort_order: true,
segment_role: true,
transport_type: true,
from_city: true,
to_city: true,
},
},
},
},
},
},
},
@@ -1166,6 +1241,47 @@ async function workflowRoutes(fastify) {
: null,
}
: null,
travelRequest: task.workflow_instance?.travel_request
? {
id: task.workflow_instance.travel_request.id,
requestNo: task.workflow_instance.travel_request.request_no ?? null,
status: task.workflow_instance.travel_request.status,
purpose: task.workflow_instance.travel_request.purpose,
tripType: task.workflow_instance.travel_request.trip_type,
startDate: formatDateOnly(task.workflow_instance.travel_request.start_date),
endDate: formatDateOnly(task.workflow_instance.travel_request.end_date),
estimatedAmount: toMoneyString(task.workflow_instance.travel_request.estimated_amount),
applicant: task.workflow_instance.travel_request.applicant
? {
id: task.workflow_instance.travel_request.applicant.id,
employeeNo: task.workflow_instance.travel_request.applicant.employee_no,
name: task.workflow_instance.travel_request.applicant.name,
}
: null,
legalEntity: task.workflow_instance.travel_request.legal_entity
? {
id: task.workflow_instance.travel_request.legal_entity.id,
code: task.workflow_instance.travel_request.legal_entity.code,
name: task.workflow_instance.travel_request.legal_entity.name,
}
: null,
costCenter: task.workflow_instance.travel_request.cost_center
? {
id: task.workflow_instance.travel_request.cost_center.id,
code: task.workflow_instance.travel_request.cost_center.code,
name: task.workflow_instance.travel_request.cost_center.name,
}
: null,
segments: task.workflow_instance.travel_request.segments.map((item) => ({
id: item.id,
sortOrder: item.sort_order,
segmentRole: item.segment_role,
transportType: item.transport_type,
fromCity: item.from_city,
toCity: item.to_city,
})),
}
: null,
allowedActions: listAllowedActionsForTask(request, task, actionable),
history: historyItems.map((item) => ({
id: item.id,
@@ -1477,6 +1593,21 @@ async function workflowRoutes(fastify) {
"Failed to enqueue purchase approval ERP events",
);
}
} else if (workflowInstance.entity_type === "travel_request") {
await tx.travelRequest.update({
where: { id: workflowInstance.entity_id },
data: {
status: "approved",
updated_by: request.employee.id,
},
});
await confirmBudgetForEntity(tx, {
entityType: "travel_request",
entityId: workflowInstance.entity_id,
actorId: request.employee.id,
note: "approved_confirm_travel_budget",
});
}
await tx.workflowHistory.create({
@@ -1520,6 +1651,7 @@ async function workflowRoutes(fastify) {
nextTaskId: null,
reimbursementStatus: workflowInstance.entity_type === "reimbursement" ? "approved" : null,
purchaseRequestStatus: workflowInstance.entity_type === "purchase_request" ? "approved" : null,
travelRequestStatus: workflowInstance.entity_type === "travel_request" ? "approved" : null,
};
});
@@ -1645,6 +1777,21 @@ async function workflowRoutes(fastify) {
actorId: request.employee.id,
note: "rejected_release_purchase_budget",
});
} else if (workflowInstance.entity_type === "travel_request") {
await tx.travelRequest.update({
where: { id: workflowInstance.entity_id },
data: {
status: "rejected",
updated_by: request.employee.id,
},
});
await releaseReservedBudgetIfAny(tx, {
entityType: "travel_request",
entityId: workflowInstance.entity_id,
actorId: request.employee.id,
note: "rejected_release_travel_budget",
});
}
await tx.workflowHistory.create({
@@ -1688,6 +1835,7 @@ async function workflowRoutes(fastify) {
workflowInstanceId: workflowInstance.id,
reimbursementStatus: workflowInstance.entity_type === "reimbursement" ? "rejected" : null,
purchaseRequestStatus: workflowInstance.entity_type === "purchase_request" ? "rejected" : null,
travelRequestStatus: workflowInstance.entity_type === "travel_request" ? "rejected" : null,
};
});
@@ -1813,6 +1961,21 @@ async function workflowRoutes(fastify) {
actorId: request.employee.id,
note: "returned_release_purchase_budget",
});
} else if (workflowInstance.entity_type === "travel_request") {
await tx.travelRequest.update({
where: { id: workflowInstance.entity_id },
data: {
status: "draft",
updated_by: request.employee.id,
},
});
await releaseReservedBudgetIfAny(tx, {
entityType: "travel_request",
entityId: workflowInstance.entity_id,
actorId: request.employee.id,
note: "returned_release_travel_budget",
});
}
await tx.workflowHistory.create({
@@ -1856,6 +2019,7 @@ async function workflowRoutes(fastify) {
workflowInstanceId: workflowInstance.id,
reimbursementStatus: workflowInstance.entity_type === "reimbursement" ? "draft" : null,
purchaseRequestStatus: workflowInstance.entity_type === "purchase_request" ? "draft" : null,
travelRequestStatus: workflowInstance.entity_type === "travel_request" ? "draft" : null,
};
});
@@ -2012,4 +2176,6 @@ module.exports = workflowRoutes;
module.exports.__testables = {
handlePurchaseApprovalErpSync,
listAllowedActionsForTask,
getWorkflowEntityMeta,
resolveWorkflowOutcomeNotificationType,
};