feat(P20T1-1,P20T1-2,P20T1-3): 差旅模型、申请/订单接口与工作流分支
这个提交包含在:
@@ -44,6 +44,7 @@ const assetMovementsRoutes = require("./routes/assetMovements");
|
||||
const inventoryChecksRoutes = require("./routes/inventoryChecks");
|
||||
const integrationsRoutes = require("./routes/integrations");
|
||||
const travelOrdersRoutes = require("./routes/travelOrders");
|
||||
const travelRequestsRoutes = require("./routes/travelRequests");
|
||||
const aiRoutes = require("./routes/ai");
|
||||
const { startJobs } = require("./jobs/jobSystem");
|
||||
const { initMetrics } = require("./observability");
|
||||
@@ -306,6 +307,7 @@ async function buildApp() {
|
||||
await app.register(inventoryChecksRoutes, { prefix: "/api" });
|
||||
await app.register(integrationsRoutes, { prefix: "/api" });
|
||||
await app.register(travelOrdersRoutes, { prefix: "/api" });
|
||||
await app.register(travelRequestsRoutes, { prefix: "/api" });
|
||||
await app.register(aiRoutes, { prefix: "/api" });
|
||||
await app.register(exportsRoutes, { prefix: "/api" });
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const NOTIFICATION_DOMAIN_SCOPES = Object.freeze({
|
||||
reimbursement: 'reimbursement',
|
||||
leave: 'leave',
|
||||
procurement: 'procurement',
|
||||
travel: 'travel',
|
||||
asset: 'asset',
|
||||
inventory: 'inventory',
|
||||
});
|
||||
@@ -17,6 +18,8 @@ const NOTIFICATION_TYPES = Object.freeze({
|
||||
leaveRejected: 'leave.rejected',
|
||||
purchaseRequestApproved: 'purchase_request.approved',
|
||||
purchaseRequestRejected: 'purchase_request.rejected',
|
||||
travelRequestApproved: 'travel_request.approved',
|
||||
travelRequestRejected: 'travel_request.rejected',
|
||||
assetAssigned: 'asset.assigned',
|
||||
assetReturned: 'asset.returned',
|
||||
assetTransferred: 'asset.transferred',
|
||||
@@ -33,6 +36,8 @@ const IM_SUPPORTED_NOTIFICATION_TYPES = Object.freeze([
|
||||
NOTIFICATION_TYPES.leaveRejected,
|
||||
NOTIFICATION_TYPES.purchaseRequestApproved,
|
||||
NOTIFICATION_TYPES.purchaseRequestRejected,
|
||||
NOTIFICATION_TYPES.travelRequestApproved,
|
||||
NOTIFICATION_TYPES.travelRequestRejected,
|
||||
NOTIFICATION_TYPES.assetAssigned,
|
||||
NOTIFICATION_TYPES.assetReturned,
|
||||
NOTIFICATION_TYPES.assetTransferred,
|
||||
@@ -49,6 +54,8 @@ const NOTIFICATION_TYPE_TO_DOMAIN_SCOPE = Object.freeze({
|
||||
[NOTIFICATION_TYPES.leaveRejected]: NOTIFICATION_DOMAIN_SCOPES.leave,
|
||||
[NOTIFICATION_TYPES.purchaseRequestApproved]: NOTIFICATION_DOMAIN_SCOPES.procurement,
|
||||
[NOTIFICATION_TYPES.purchaseRequestRejected]: NOTIFICATION_DOMAIN_SCOPES.procurement,
|
||||
[NOTIFICATION_TYPES.travelRequestApproved]: NOTIFICATION_DOMAIN_SCOPES.travel,
|
||||
[NOTIFICATION_TYPES.travelRequestRejected]: NOTIFICATION_DOMAIN_SCOPES.travel,
|
||||
[NOTIFICATION_TYPES.assetAssigned]: NOTIFICATION_DOMAIN_SCOPES.asset,
|
||||
[NOTIFICATION_TYPES.assetReturned]: NOTIFICATION_DOMAIN_SCOPES.asset,
|
||||
[NOTIFICATION_TYPES.assetTransferred]: NOTIFICATION_DOMAIN_SCOPES.asset,
|
||||
@@ -60,6 +67,7 @@ const TARGET_TYPE_TO_DOMAIN_SCOPE = Object.freeze({
|
||||
reimbursement_form: NOTIFICATION_DOMAIN_SCOPES.reimbursement,
|
||||
leave_request: NOTIFICATION_DOMAIN_SCOPES.leave,
|
||||
purchase_request: NOTIFICATION_DOMAIN_SCOPES.procurement,
|
||||
travel_request: NOTIFICATION_DOMAIN_SCOPES.travel,
|
||||
asset_card: NOTIFICATION_DOMAIN_SCOPES.asset,
|
||||
inventory_check: NOTIFICATION_DOMAIN_SCOPES.inventory,
|
||||
});
|
||||
|
||||
@@ -22,6 +22,12 @@ const PROCUREMENT_SELF_SERVICE_PERMISSIONS = [
|
||||
"procurement.submit",
|
||||
];
|
||||
|
||||
const TRAVEL_SELF_SERVICE_PERMISSIONS = [
|
||||
"travel.create",
|
||||
"travel.view_own",
|
||||
"travel.submit",
|
||||
];
|
||||
|
||||
const ASSET_SELF_SERVICE_PERMISSIONS = ["asset.view_own"];
|
||||
|
||||
const MASTERDATA_VIEW_PERMISSIONS = ["masterdata.view"];
|
||||
@@ -30,6 +36,7 @@ const MASTERDATA_MANAGE_PERMISSIONS = ["masterdata.manage"];
|
||||
const EMPLOYEE_BASE_PERMISSIONS = [
|
||||
...EXPENSE_SELF_SERVICE_PERMISSIONS,
|
||||
...LEAVE_SELF_SERVICE_PERMISSIONS,
|
||||
...TRAVEL_SELF_SERVICE_PERMISSIONS,
|
||||
];
|
||||
|
||||
const ENTITY_PERMISSION_MAP = {
|
||||
@@ -79,6 +86,22 @@ const ENTITY_PERMISSION_MAP = {
|
||||
approve: "procurement.approve",
|
||||
manage: "procurement.manage",
|
||||
},
|
||||
travel_request: {
|
||||
create: "travel.create",
|
||||
view_own: "travel.view_own",
|
||||
view_all: "travel.view_all",
|
||||
submit: "travel.submit",
|
||||
approve: "travel.approve",
|
||||
manage: "travel.manage",
|
||||
},
|
||||
travel: {
|
||||
create: "travel.create",
|
||||
view_own: "travel.view_own",
|
||||
view_all: "travel.view_all",
|
||||
submit: "travel.submit",
|
||||
approve: "travel.approve",
|
||||
manage: "travel.manage",
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_PERMISSIONS = {
|
||||
@@ -96,6 +119,8 @@ const ROLE_PERMISSIONS = {
|
||||
"leave.approve",
|
||||
"procurement.approve",
|
||||
"procurement.view_all",
|
||||
"travel.approve",
|
||||
"travel.view_all",
|
||||
"asset.view_all",
|
||||
"report.dashboard.view",
|
||||
"budget.view",
|
||||
@@ -112,6 +137,8 @@ const ROLE_PERMISSIONS = {
|
||||
"leave.view_all",
|
||||
"procurement.approve",
|
||||
"procurement.view_all",
|
||||
"travel.approve",
|
||||
"travel.view_all",
|
||||
"asset.view_all",
|
||||
"report.dashboard.view",
|
||||
"budget.view",
|
||||
@@ -127,6 +154,7 @@ const ROLE_PERMISSIONS = {
|
||||
"expense.finance.pay",
|
||||
"expense.view_all",
|
||||
"procurement.view_all",
|
||||
"travel.view_all",
|
||||
"asset.view_all",
|
||||
],
|
||||
finance_mgr: [
|
||||
@@ -146,6 +174,8 @@ const ROLE_PERMISSIONS = {
|
||||
"expense.finance.override_duplicate",
|
||||
"procurement.view_all",
|
||||
"procurement.manage",
|
||||
"travel.view_all",
|
||||
"travel.manage",
|
||||
"asset.view_all",
|
||||
"asset.manage",
|
||||
"asset.inventory",
|
||||
@@ -157,6 +187,8 @@ const ROLE_PERMISSIONS = {
|
||||
...EMPLOYEE_BASE_PERMISSIONS,
|
||||
"procurement.view_all",
|
||||
"procurement.manage",
|
||||
"travel.view_all",
|
||||
"travel.manage",
|
||||
"asset.view_all",
|
||||
"asset.manage",
|
||||
"asset.transfer",
|
||||
|
||||
@@ -10,6 +10,7 @@ const DOCUMENT_CATEGORY_ENTITY_TYPES = [
|
||||
"reimbursement",
|
||||
"leave",
|
||||
"purchase_request",
|
||||
"travel_request",
|
||||
"asset_card",
|
||||
"general",
|
||||
];
|
||||
|
||||
+114
-59
@@ -8,6 +8,7 @@ const { normalizeTravelImportRows } = require("../services/integrations/travel")
|
||||
const IMPORT_SCHEMA = z.object({
|
||||
orders: z.array(z.record(z.string(), z.unknown())).min(1).max(100),
|
||||
});
|
||||
const UUID_SCHEMA = z.string().uuid();
|
||||
|
||||
function mapTravelOrder(item) {
|
||||
return {
|
||||
@@ -17,6 +18,7 @@ function mapTravelOrder(item) {
|
||||
employeeId: item.employee_id,
|
||||
departmentId: item.department_id ?? null,
|
||||
linkedReimbursementId: item.linked_reimbursement_id ?? null,
|
||||
travelRequestId: item.travel_request_id ?? null,
|
||||
occurredAt: item.occurred_at?.toISOString?.()?.slice(0, 10) ?? null,
|
||||
amount: item.amount?.toString?.() ?? null,
|
||||
currency: item.currency,
|
||||
@@ -26,6 +28,15 @@ function mapTravelOrder(item) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildValidationError(details, statusCode = 400) {
|
||||
return new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode,
|
||||
details,
|
||||
});
|
||||
}
|
||||
|
||||
async function travelOrdersRoutes(fastify) {
|
||||
fastify.post(
|
||||
"/travel/orders/import",
|
||||
@@ -34,49 +45,56 @@ async function travelOrdersRoutes(fastify) {
|
||||
},
|
||||
async (request, reply) => {
|
||||
if (!fastify.config.travelSyncEnabled) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { integration: "travel_sync_disabled" },
|
||||
});
|
||||
throw buildValidationError({ integration: "travel_sync_disabled" }, 409);
|
||||
}
|
||||
|
||||
const parsed = IMPORT_SCHEMA.safeParse(request.body ?? {});
|
||||
if (!parsed.success) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: parsed.error.flatten(),
|
||||
});
|
||||
throw buildValidationError(parsed.error.flatten());
|
||||
}
|
||||
|
||||
const rows = normalizeTravelImportRows(parsed.data.orders);
|
||||
if (!rows.length) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: { orders: "empty_after_normalization" },
|
||||
});
|
||||
throw buildValidationError({ orders: "empty_after_normalization" });
|
||||
}
|
||||
|
||||
const employeeIds = Array.from(new Set(rows.map((item) => item.employeeId)));
|
||||
const employees = await fastify.prisma.employee.findMany({
|
||||
where: {
|
||||
id: { in: employeeIds },
|
||||
status: "active",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
department_id: true,
|
||||
},
|
||||
});
|
||||
const employeeMap = new Map(employees.map((item) => [item.id, item]));
|
||||
const travelRequestIds = Array.from(
|
||||
new Set(
|
||||
rows
|
||||
.filter((item) => item.hasTravelRequestId && item.travelRequestId)
|
||||
.map((item) => item.travelRequestId),
|
||||
),
|
||||
).filter((item) => UUID_SCHEMA.safeParse(item).success);
|
||||
|
||||
const created = [];
|
||||
const [employees, travelRequests] = await Promise.all([
|
||||
fastify.prisma.employee.findMany({
|
||||
where: {
|
||||
id: { in: employeeIds },
|
||||
status: "active",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
department_id: true,
|
||||
},
|
||||
}),
|
||||
travelRequestIds.length
|
||||
? fastify.prisma.travelRequest.findMany({
|
||||
where: { id: { in: travelRequestIds } },
|
||||
select: {
|
||||
id: true,
|
||||
applicant_employee_id: true,
|
||||
},
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const employeeMap = new Map(employees.map((item) => [item.id, item]));
|
||||
const travelRequestMap = new Map(travelRequests.map((item) => [item.id, item]));
|
||||
|
||||
const imported = [];
|
||||
const skipped = [];
|
||||
let linkedTravelRequestCount = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
const employee = employeeMap.get(row.employeeId);
|
||||
@@ -89,6 +107,61 @@ async function travelOrdersRoutes(fastify) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nextTravelRequestId;
|
||||
if (row.hasTravelRequestId && row.travelRequestId) {
|
||||
const travelRequest = travelRequestMap.get(row.travelRequestId);
|
||||
if (!travelRequest) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalOrderNo: row.externalOrderNo,
|
||||
reason: "travel_request_not_found",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (travelRequest.applicant_employee_id !== row.employeeId) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalOrderNo: row.externalOrderNo,
|
||||
reason: "travel_request_employee_mismatch",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
nextTravelRequestId = travelRequest.id;
|
||||
} else if (row.hasTravelRequestId && !row.travelRequestId) {
|
||||
nextTravelRequestId = undefined;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
employee_id: row.employeeId,
|
||||
department_id: employee.department_id,
|
||||
linked_reimbursement_id: row.linkedReimbursementId,
|
||||
occurred_at: row.occurredAt,
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
status: "imported",
|
||||
raw_payload: row.rawPayload,
|
||||
updated_by: request.employee.id,
|
||||
};
|
||||
if (nextTravelRequestId !== undefined) {
|
||||
updateData.travel_request_id = nextTravelRequestId;
|
||||
}
|
||||
|
||||
const createData = {
|
||||
source: row.source,
|
||||
external_order_no: row.externalOrderNo,
|
||||
employee_id: row.employeeId,
|
||||
department_id: employee.department_id,
|
||||
linked_reimbursement_id: row.linkedReimbursementId,
|
||||
travel_request_id: nextTravelRequestId ?? null,
|
||||
occurred_at: row.occurredAt,
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
status: "imported",
|
||||
raw_payload: row.rawPayload,
|
||||
created_by: request.employee.id,
|
||||
updated_by: request.employee.id,
|
||||
};
|
||||
|
||||
const upserted = await fastify.prisma.travelOrder.upsert({
|
||||
where: {
|
||||
source_external_order_no: {
|
||||
@@ -96,34 +169,14 @@ async function travelOrdersRoutes(fastify) {
|
||||
external_order_no: row.externalOrderNo,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
employee_id: row.employeeId,
|
||||
department_id: employee.department_id,
|
||||
linked_reimbursement_id: row.linkedReimbursementId,
|
||||
occurred_at: row.occurredAt,
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
status: "imported",
|
||||
raw_payload: row.rawPayload,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
create: {
|
||||
source: row.source,
|
||||
external_order_no: row.externalOrderNo,
|
||||
employee_id: row.employeeId,
|
||||
department_id: employee.department_id,
|
||||
linked_reimbursement_id: row.linkedReimbursementId,
|
||||
occurred_at: row.occurredAt,
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
status: "imported",
|
||||
raw_payload: row.rawPayload,
|
||||
created_by: request.employee.id,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
update: updateData,
|
||||
create: createData,
|
||||
});
|
||||
|
||||
created.push(upserted);
|
||||
if (upserted.travel_request_id) {
|
||||
linkedTravelRequestCount += 1;
|
||||
}
|
||||
imported.push(upserted);
|
||||
}
|
||||
|
||||
await fastify.prisma.auditLog.create({
|
||||
@@ -133,7 +186,8 @@ async function travelOrdersRoutes(fastify) {
|
||||
targetType: "travel_order",
|
||||
extra: {
|
||||
requested: rows.length,
|
||||
createdOrUpdated: created.length,
|
||||
createdOrUpdated: imported.length,
|
||||
linkedTravelRequestCount,
|
||||
skipped,
|
||||
},
|
||||
}),
|
||||
@@ -141,9 +195,10 @@ async function travelOrdersRoutes(fastify) {
|
||||
|
||||
return sendData(reply, request, {
|
||||
requested: rows.length,
|
||||
imported: created.length,
|
||||
imported: imported.length,
|
||||
linkedTravelRequestCount,
|
||||
skipped,
|
||||
items: created.map(mapTravelOrder),
|
||||
items: imported.map(mapTravelOrder),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
文件差异内容过多而无法显示
加载差异
@@ -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,
|
||||
};
|
||||
|
||||
@@ -118,6 +118,19 @@ const DEFAULT_PURCHASE_WORKFLOW_DEFINITION = {
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_TRAVEL_WORKFLOW_DEFINITION = {
|
||||
steps: [
|
||||
{
|
||||
id: "travel_approval_manager",
|
||||
name: "直属经理审批",
|
||||
type: "approval",
|
||||
assignee_rule: {
|
||||
type: "department_manager",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_DOCUMENT_CATEGORIES = [
|
||||
{
|
||||
code: "reimbursement_general",
|
||||
@@ -149,6 +162,16 @@ const DEFAULT_DOCUMENT_CATEGORIES = [
|
||||
default_workflow_key: null,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
code: "TR_STANDARD",
|
||||
name: "标准差旅申请",
|
||||
entity_type: "travel_request",
|
||||
sequence_prefix: "CL",
|
||||
number_length: 4,
|
||||
reset_period: "yearly",
|
||||
default_workflow_key: null,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
code: "ASSET_CARD_DEFAULT",
|
||||
name: "固定资产卡片",
|
||||
@@ -529,6 +552,38 @@ async function seedWorkflowDefinition(prisma) {
|
||||
created_by: null,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.workflowDefinition.updateMany({
|
||||
where: {
|
||||
entity_type: "travel_request",
|
||||
},
|
||||
data: {
|
||||
is_active: false,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.workflowDefinition.upsert({
|
||||
where: {
|
||||
entity_type_version: {
|
||||
entity_type: "travel_request",
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
name: "差旅申请审批流程(V1)",
|
||||
definition_json: DEFAULT_TRAVEL_WORKFLOW_DEFINITION,
|
||||
is_active: true,
|
||||
created_by: null,
|
||||
},
|
||||
create: {
|
||||
name: "差旅申请审批流程(V1)",
|
||||
entity_type: "travel_request",
|
||||
definition_json: DEFAULT_TRAVEL_WORKFLOW_DEFINITION,
|
||||
version: 1,
|
||||
is_active: true,
|
||||
created_by: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -39,6 +39,17 @@ const DOCUMENT_TYPE_CONFIG = {
|
||||
resetPeriod: "yearly",
|
||||
},
|
||||
},
|
||||
travel_request: {
|
||||
defaultCategoryCode: "TR_STANDARD",
|
||||
preferLegacySequence: false,
|
||||
fallback: {
|
||||
prefix: "CL",
|
||||
dateFormat: "YYYYMMDD",
|
||||
separator: "-",
|
||||
numberLength: 4,
|
||||
resetPeriod: "yearly",
|
||||
},
|
||||
},
|
||||
asset_card: {
|
||||
defaultCategoryCode: "ASSET_CARD_DEFAULT",
|
||||
preferLegacySequence: false,
|
||||
|
||||
@@ -18,6 +18,9 @@ function normalizeTravelImportRows(rows) {
|
||||
? new Date(`${String(item.occurredAt).slice(0, 10)}T00:00:00.000Z`)
|
||||
: null;
|
||||
|
||||
const hasTravelRequestId = Object.prototype.hasOwnProperty.call(item ?? {}, "travelRequestId");
|
||||
const rawTravelRequestId = String(item?.travelRequestId ?? "").trim();
|
||||
|
||||
return {
|
||||
source,
|
||||
externalOrderNo,
|
||||
@@ -26,6 +29,8 @@ function normalizeTravelImportRows(rows) {
|
||||
item?.linkedReimbursementId && String(item.linkedReimbursementId).trim()
|
||||
? String(item.linkedReimbursementId).trim()
|
||||
: null,
|
||||
travelRequestId: rawTravelRequestId || null,
|
||||
hasTravelRequestId,
|
||||
amount: Number.isFinite(amount) ? amount.toFixed(2) : null,
|
||||
currency: String(item?.currency ?? "CNY").trim().toUpperCase() || "CNY",
|
||||
occurredAt,
|
||||
|
||||
在新工单中引用
屏蔽一个用户