feat(P20T1-1,P20T1-2,P20T1-3): 差旅模型、申请/订单接口与工作流分支
这个提交包含在:
@@ -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,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户