From 1cd4290b4d1c8ab3ebdb57d5112cefb4aa67f1d3 Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 7 Mar 2026 20:59:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(P17A-1,P17A-2):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=87=87=E8=B4=AD=E6=A8=A1=E5=9E=8B=E6=9D=83=E9=99=90=E4=B8=8E?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=A7=8D=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/auth/permissions.ts | 22 ++- .../migration.sql | 135 ++++++++++++++++++ server/prisma/schema.prisma | 122 ++++++++++++++-- server/src/constants/permissions.js | 22 ++- server/src/scripts/seed-config.js | 50 ++++++- server/src/services/documentSequence.js | 2 +- .../tests/unit/constants/permissions.test.js | 9 ++ .../unit/documentSequence.service.test.js | 10 +- 8 files changed, 354 insertions(+), 18 deletions(-) create mode 100644 server/prisma/migrations/20260307124228_p17a_procurement_core/migration.sql diff --git a/client/src/auth/permissions.ts b/client/src/auth/permissions.ts index beb297de..87e8f760 100644 --- a/client/src/auth/permissions.ts +++ b/client/src/auth/permissions.ts @@ -16,6 +16,12 @@ const LEAVE_SELF_SERVICE_PERMISSIONS = [ "leave.submit", ] as const; +const PROCUREMENT_SELF_SERVICE_PERMISSIONS = [ + "procurement.create", + "procurement.view_own", + "procurement.submit", +] as const; + const MASTERDATA_VIEW_PERMISSIONS = ["masterdata.view"] as const; const MASTERDATA_MANAGE_PERMISSIONS = ["masterdata.manage"] as const; @@ -25,25 +31,34 @@ const EMPLOYEE_BASE_PERMISSIONS = [ ] as const; const ROLE_PERMISSIONS: Record = { - employee: EMPLOYEE_BASE_PERMISSIONS, + employee: [ + ...EMPLOYEE_BASE_PERMISSIONS, + ...PROCUREMENT_SELF_SERVICE_PERMISSIONS, + ], manager: [ ...EMPLOYEE_BASE_PERMISSIONS, + ...PROCUREMENT_SELF_SERVICE_PERMISSIONS, "expense.approve", "expense.transfer", "expense.view_all", "leave.approve", + "procurement.approve", + "procurement.view_all", "report.dashboard.view", "budget.view", "budget.manage", ], ceo: [ ...EMPLOYEE_BASE_PERMISSIONS, + ...PROCUREMENT_SELF_SERVICE_PERMISSIONS, ...MASTERDATA_VIEW_PERMISSIONS, "expense.approve", "expense.transfer", "expense.view_all", "leave.approve", "leave.view_all", + "procurement.approve", + "procurement.view_all", "report.dashboard.view", "budget.view", ], @@ -57,6 +72,7 @@ const ROLE_PERMISSIONS: Record = { "expense.approve", "expense.finance.pay", "expense.view_all", + "procurement.view_all", ], finance_mgr: [ ...EMPLOYEE_BASE_PERMISSIONS, @@ -72,12 +88,16 @@ const ROLE_PERMISSIONS: Record = { "expense.finance.pay", "expense.view_all", "expense.finance.override_duplicate", + "procurement.view_all", + "procurement.manage", "system.settings", "system.audit.view", ], administration: [ "expense.proxy_submit", ...EMPLOYEE_BASE_PERMISSIONS, + "procurement.view_all", + "procurement.manage", ], hr: [ ...LEAVE_SELF_SERVICE_PERMISSIONS, diff --git a/server/prisma/migrations/20260307124228_p17a_procurement_core/migration.sql b/server/prisma/migrations/20260307124228_p17a_procurement_core/migration.sql new file mode 100644 index 00000000..e50c4126 --- /dev/null +++ b/server/prisma/migrations/20260307124228_p17a_procurement_core/migration.sql @@ -0,0 +1,135 @@ +-- CreateEnum +CREATE TYPE "PurchaseRequestStatus" AS ENUM ('draft', 'reviewing', 'approved', 'rejected', 'cancelled'); + +-- CreateTable +CREATE TABLE "purchase_requests" ( + "id" UUID NOT NULL, + "request_no" TEXT, + "applicant_id" UUID NOT NULL, + "submitter_id" UUID, + "department_id" UUID NOT NULL, + "legal_entity_id" UUID, + "cost_center_id" UUID, + "supplier_id" UUID, + "document_category_id" UUID, + "budget_pool_id" UUID, + "title" TEXT NOT NULL DEFAULT '', + "reason" TEXT, + "currency_code" VARCHAR(8) NOT NULL DEFAULT 'CNY', + "total_amount" DECIMAL(14,2) NOT NULL DEFAULT 0, + "expected_delivery_date" DATE, + "status" "PurchaseRequestStatus" NOT NULL DEFAULT 'draft', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" UUID, + "updated_at" TIMESTAMP(3) NOT NULL, + "updated_by" UUID, + + CONSTRAINT "purchase_requests_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "purchase_request_items" ( + "id" UUID NOT NULL, + "purchase_request_id" UUID NOT NULL, + "description" TEXT NOT NULL, + "specification" TEXT, + "quantity" DECIMAL(14,2) NOT NULL, + "unit_price" DECIMAL(14,2) NOT NULL, + "line_amount" DECIMAL(14,2) NOT NULL, + "is_asset_candidate" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" UUID, + "updated_at" TIMESTAMP(3) NOT NULL, + "updated_by" UUID, + + CONSTRAINT "purchase_request_items_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "purchase_attachments" ( + "id" UUID NOT NULL, + "purchase_request_id" UUID NOT NULL, + "storage_path" TEXT NOT NULL, + "original_name" TEXT NOT NULL, + "mime_type" TEXT NOT NULL, + "file_size" INTEGER NOT NULL, + "uploaded_by" UUID NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "purchase_attachments_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "purchase_requests_request_no_key" ON "purchase_requests"("request_no"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_applicant_status_updated" ON "purchase_requests"("applicant_id", "status", "updated_at"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_submitter" ON "purchase_requests"("submitter_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_department" ON "purchase_requests"("department_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_legal_entity" ON "purchase_requests"("legal_entity_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_cost_center" ON "purchase_requests"("cost_center_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_supplier" ON "purchase_requests"("supplier_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_document_category" ON "purchase_requests"("document_category_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_budget_pool" ON "purchase_requests"("budget_pool_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_requests_status" ON "purchase_requests"("status"); + +-- CreateIndex +CREATE INDEX "idx_purchase_request_items_request" ON "purchase_request_items"("purchase_request_id"); + +-- CreateIndex +CREATE INDEX "idx_purchase_attachments_request_created" ON "purchase_attachments"("purchase_request_id", "created_at"); + +-- CreateIndex +CREATE INDEX "idx_purchase_attachments_uploader" ON "purchase_attachments"("uploaded_by"); + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_applicant_id_fkey" FOREIGN KEY ("applicant_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_submitter_id_fkey" FOREIGN KEY ("submitter_id") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_department_id_fkey" FOREIGN KEY ("department_id") REFERENCES "departments"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_legal_entity_id_fkey" FOREIGN KEY ("legal_entity_id") REFERENCES "legal_entities"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_cost_center_id_fkey" FOREIGN KEY ("cost_center_id") REFERENCES "cost_centers"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_document_category_id_fkey" FOREIGN KEY ("document_category_id") REFERENCES "document_categories"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_budget_pool_id_fkey" FOREIGN KEY ("budget_pool_id") REFERENCES "budget_pools"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_request_items" ADD CONSTRAINT "purchase_request_items_purchase_request_id_fkey" FOREIGN KEY ("purchase_request_id") REFERENCES "purchase_requests"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_attachments" ADD CONSTRAINT "purchase_attachments_purchase_request_id_fkey" FOREIGN KEY ("purchase_request_id") REFERENCES "purchase_requests"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "purchase_attachments" ADD CONSTRAINT "purchase_attachments_uploaded_by_fkey" FOREIGN KEY ("uploaded_by") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "workflow_instances" ADD CONSTRAINT "workflow_instances_purchase_request_id_fkey" FOREIGN KEY ("purchase_request_id") REFERENCES "purchase_requests"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 6526b412..52c755ee 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -44,6 +44,14 @@ enum LeaveRequestStatus { cancelled } +enum PurchaseRequestStatus { + draft + reviewing + approved + rejected + cancelled +} + enum LeaveDaySession { am pm @@ -162,6 +170,7 @@ model Department { employees Employee[] reimbursement_forms ReimbursementForm[] leave_requests LeaveRequest[] + purchase_requests PurchaseRequest[] workflow_instances WorkflowInstance[] budget_pools BudgetPool[] travel_orders TravelOrder[] @@ -217,9 +226,12 @@ model Employee { submitted_reimbursements ReimbursementForm[] @relation("ReimbursementSubmitter") owned_reimbursements ReimbursementForm[] @relation("ReimbursementOwner") paid_reimbursements ReimbursementForm[] @relation("ReimbursementPaidBy") - leave_requests LeaveRequest[] - leave_ledgers LeaveLedger[] - owned_cost_centers CostCenter[] @relation("CostCenterOwner") + leave_requests LeaveRequest[] + leave_ledgers LeaveLedger[] + owned_cost_centers CostCenter[] @relation("CostCenterOwner") + purchase_requests_applicant PurchaseRequest[] @relation("PurchaseRequestApplicant") + purchase_requests_submitted PurchaseRequest[] @relation("PurchaseRequestSubmitter") + purchase_attachments_uploaded PurchaseAttachment[] @relation("PurchaseAttachmentUploader") owned_invoices InvoicePool[] @relation("InvoiceOwner") uploaded_invoices InvoicePool[] @relation("InvoiceUploader") @@ -410,6 +422,7 @@ model LegalEntity { employees Employee[] holiday_calendars HolidayCalendar[] @relation("HolidayCalendarLegalEntity") suppliers Supplier[] + purchase_requests PurchaseRequest[] budget_pools BudgetPool[] @relation("BudgetPoolLegalEntity") @@index([enabled, archived_at], map: "idx_legal_entities_status") @@ -437,6 +450,7 @@ model CostCenter { children CostCenter[] @relation("CostCenterToParent") owner_employee Employee? @relation("CostCenterOwner", fields: [owner_employee_id], references: [id], onDelete: SetNull) budget_pools BudgetPool[] @relation("BudgetPoolCostCenter") + purchase_requests PurchaseRequest[] @@unique([legal_entity_id, code], map: "legal_entity_cost_center_code_ux") @@index([legal_entity_id, enabled, archived_at], map: "idx_cost_centers_legal_entity_status") @@ -462,6 +476,8 @@ model DocumentCategory { updated_at DateTime @updatedAt updated_by String? @db.Uuid + purchase_requests PurchaseRequest[] + @@index([entity_type, enabled, archived_at], map: "idx_document_categories_entity_status") @@map("document_categories") } @@ -526,13 +542,101 @@ model Supplier { updated_at DateTime @updatedAt updated_by String? @db.Uuid - legal_entity LegalEntity? @relation(fields: [legal_entity_id], references: [id], onDelete: SetNull) + legal_entity LegalEntity? @relation(fields: [legal_entity_id], references: [id], onDelete: SetNull) + purchase_requests PurchaseRequest[] @@index([legal_entity_id, enabled, archived_at], map: "idx_suppliers_legal_entity_status") @@index([name], map: "idx_suppliers_name") @@map("suppliers") } +model PurchaseRequest { + id String @id @default(uuid()) @db.Uuid + request_no String? @unique + applicant_id String @db.Uuid + submitter_id String? @db.Uuid + department_id String @db.Uuid + legal_entity_id String? @db.Uuid + cost_center_id String? @db.Uuid + supplier_id String? @db.Uuid + document_category_id String? @db.Uuid + budget_pool_id String? @db.Uuid + title String @default("") + reason String? + currency_code String @default("CNY") @db.VarChar(8) + total_amount Decimal @default(0) @db.Decimal(14, 2) + expected_delivery_date DateTime? @db.Date + status PurchaseRequestStatus @default(draft) + + created_at DateTime @default(now()) + created_by String? @db.Uuid + updated_at DateTime @updatedAt + updated_by String? @db.Uuid + + applicant Employee @relation("PurchaseRequestApplicant", fields: [applicant_id], references: [id], onDelete: Restrict) + submitter Employee? @relation("PurchaseRequestSubmitter", fields: [submitter_id], references: [id], onDelete: SetNull) + department Department @relation(fields: [department_id], references: [id], onDelete: Restrict) + legal_entity LegalEntity? @relation(fields: [legal_entity_id], references: [id], onDelete: SetNull) + cost_center CostCenter? @relation(fields: [cost_center_id], references: [id], onDelete: SetNull) + supplier Supplier? @relation(fields: [supplier_id], references: [id], onDelete: SetNull) + document_category DocumentCategory? @relation(fields: [document_category_id], references: [id], onDelete: SetNull) + budget_pool BudgetPool? @relation(fields: [budget_pool_id], references: [id], onDelete: SetNull) + + items PurchaseRequestItem[] + attachments PurchaseAttachment[] + workflow_instances WorkflowInstance[] + + @@index([applicant_id, status, updated_at], map: "idx_purchase_requests_applicant_status_updated") + @@index([submitter_id], map: "idx_purchase_requests_submitter") + @@index([department_id], map: "idx_purchase_requests_department") + @@index([legal_entity_id], map: "idx_purchase_requests_legal_entity") + @@index([cost_center_id], map: "idx_purchase_requests_cost_center") + @@index([supplier_id], map: "idx_purchase_requests_supplier") + @@index([document_category_id], map: "idx_purchase_requests_document_category") + @@index([budget_pool_id], map: "idx_purchase_requests_budget_pool") + @@index([status], map: "idx_purchase_requests_status") + @@map("purchase_requests") +} + +model PurchaseRequestItem { + id String @id @default(uuid()) @db.Uuid + purchase_request_id String @db.Uuid + description String + specification String? + quantity Decimal @db.Decimal(14, 2) + unit_price Decimal @db.Decimal(14, 2) + line_amount Decimal @db.Decimal(14, 2) + is_asset_candidate Boolean @default(false) + + created_at DateTime @default(now()) + created_by String? @db.Uuid + updated_at DateTime @updatedAt + updated_by String? @db.Uuid + + purchase_request PurchaseRequest @relation(fields: [purchase_request_id], references: [id], onDelete: Cascade) + + @@index([purchase_request_id], map: "idx_purchase_request_items_request") + @@map("purchase_request_items") +} + +model PurchaseAttachment { + id String @id @default(uuid()) @db.Uuid + purchase_request_id String @db.Uuid + storage_path String + original_name String + mime_type String + file_size Int + uploaded_by String @db.Uuid + created_at DateTime @default(now()) + + purchase_request PurchaseRequest @relation(fields: [purchase_request_id], references: [id], onDelete: Cascade) + uploader Employee @relation("PurchaseAttachmentUploader", fields: [uploaded_by], references: [id], onDelete: Restrict) + + @@index([purchase_request_id, created_at], map: "idx_purchase_attachments_request_created") + @@index([uploaded_by], map: "idx_purchase_attachments_uploader") + @@map("purchase_attachments") +} + model ReimbursementForm { id String @id @default(uuid()) @db.Uuid form_number String? @unique @@ -841,10 +945,11 @@ model BudgetPool { updated_at DateTime @updatedAt updated_by String? @db.Uuid - department Department? @relation(fields: [department_id], references: [id], onDelete: SetNull) - legal_entity LegalEntity? @relation("BudgetPoolLegalEntity", fields: [legal_entity_id], references: [id], onDelete: SetNull) - cost_center CostCenter? @relation("BudgetPoolCostCenter", fields: [cost_center_id], references: [id], onDelete: SetNull) - ledger_entries BudgetLedgerEntry[] + department Department? @relation(fields: [department_id], references: [id], onDelete: SetNull) + legal_entity LegalEntity? @relation("BudgetPoolLegalEntity", fields: [legal_entity_id], references: [id], onDelete: SetNull) + cost_center CostCenter? @relation("BudgetPoolCostCenter", fields: [cost_center_id], references: [id], onDelete: SetNull) + ledger_entries BudgetLedgerEntry[] + purchase_requests PurchaseRequest[] @@index([department_id, status], map: "idx_budget_pools_department_status") @@index([legal_entity_id, cost_center_id, status], map: "idx_budget_pools_scope_status") @@ -1001,6 +1106,7 @@ model WorkflowInstance { workflow_definition WorkflowDefinition @relation(fields: [workflow_id], references: [id], onDelete: Restrict) reimbursement_form ReimbursementForm? @relation(fields: [reimbursement_id], references: [id], onDelete: Cascade) leave_request LeaveRequest? @relation(fields: [leave_request_id], references: [id], onDelete: Cascade) + purchase_request PurchaseRequest? @relation(fields: [purchase_request_id], references: [id], onDelete: Cascade) submitter Employee @relation("WorkflowInstanceSubmitter", fields: [submitter_id], references: [id], onDelete: Restrict) department Department? @relation(fields: [context_department_id], references: [id], onDelete: SetNull) diff --git a/server/src/constants/permissions.js b/server/src/constants/permissions.js index 3765a4ac..b8e2ca23 100644 --- a/server/src/constants/permissions.js +++ b/server/src/constants/permissions.js @@ -16,6 +16,12 @@ const LEAVE_SELF_SERVICE_PERMISSIONS = [ "leave.submit", ]; +const PROCUREMENT_SELF_SERVICE_PERMISSIONS = [ + "procurement.create", + "procurement.view_own", + "procurement.submit", +]; + const MASTERDATA_VIEW_PERMISSIONS = ["masterdata.view"]; const MASTERDATA_MANAGE_PERMISSIONS = ["masterdata.manage"]; @@ -74,25 +80,34 @@ const ENTITY_PERMISSION_MAP = { }; const ROLE_PERMISSIONS = { - employee: EMPLOYEE_BASE_PERMISSIONS, + employee: [ + ...EMPLOYEE_BASE_PERMISSIONS, + ...PROCUREMENT_SELF_SERVICE_PERMISSIONS, + ], manager: [ ...EMPLOYEE_BASE_PERMISSIONS, + ...PROCUREMENT_SELF_SERVICE_PERMISSIONS, "expense.approve", "expense.transfer", "expense.view_all", "leave.approve", + "procurement.approve", + "procurement.view_all", "report.dashboard.view", "budget.view", "budget.manage", ], ceo: [ ...EMPLOYEE_BASE_PERMISSIONS, + ...PROCUREMENT_SELF_SERVICE_PERMISSIONS, ...MASTERDATA_VIEW_PERMISSIONS, "expense.approve", "expense.transfer", "expense.view_all", "leave.approve", "leave.view_all", + "procurement.approve", + "procurement.view_all", "report.dashboard.view", "budget.view", ], @@ -106,6 +121,7 @@ const ROLE_PERMISSIONS = { "expense.approve", "expense.finance.pay", "expense.view_all", + "procurement.view_all", ], finance_mgr: [ ...EMPLOYEE_BASE_PERMISSIONS, @@ -121,12 +137,16 @@ const ROLE_PERMISSIONS = { "expense.finance.pay", "expense.view_all", "expense.finance.override_duplicate", + "procurement.view_all", + "procurement.manage", "system.settings", "system.audit.view", ], administration: [ "expense.proxy_submit", ...EMPLOYEE_BASE_PERMISSIONS, + "procurement.view_all", + "procurement.manage", ], hr: [ ...LEAVE_SELF_SERVICE_PERMISSIONS, diff --git a/server/src/scripts/seed-config.js b/server/src/scripts/seed-config.js index 20068521..6ce3cb36 100644 --- a/server/src/scripts/seed-config.js +++ b/server/src/scripts/seed-config.js @@ -105,6 +105,18 @@ const DEFAULT_LEAVE_WORKFLOW_DEFINITION = { ], }; +const DEFAULT_PURCHASE_WORKFLOW_DEFINITION = { + steps: [ + { + id: "approval_1_manager", + name: "直属经理审批", + type: "approval", + assignee_rule: { + type: "department_manager", + }, + }, + ], +}; const DEFAULT_DOCUMENT_CATEGORIES = [ { @@ -128,8 +140,8 @@ const DEFAULT_DOCUMENT_CATEGORIES = [ enabled: true, }, { - code: "purchase_general", - name: "日常采购申请", + code: "PR_STANDARD", + name: "标准采购申请", entity_type: "purchase_request", sequence_prefix: "CG", number_length: 4, @@ -478,6 +490,38 @@ async function seedWorkflowDefinition(prisma) { created_by: null, }, }); + + await prisma.workflowDefinition.updateMany({ + where: { + entity_type: "purchase_request", + }, + data: { + is_active: false, + }, + }); + + await prisma.workflowDefinition.upsert({ + where: { + entity_type_version: { + entity_type: "purchase_request", + version: 1, + }, + }, + update: { + name: "采购申请审批流程(V1)", + definition_json: DEFAULT_PURCHASE_WORKFLOW_DEFINITION, + is_active: true, + created_by: null, + }, + create: { + name: "采购申请审批流程(V1)", + entity_type: "purchase_request", + definition_json: DEFAULT_PURCHASE_WORKFLOW_DEFINITION, + version: 1, + is_active: true, + created_by: null, + }, + }); } async function main() { @@ -508,6 +552,8 @@ async function main() { // eslint-disable-next-line no-console console.log("- workflow definition: leave v1 (1-step department_manager)"); // eslint-disable-next-line no-console + console.log("- workflow definition: purchase_request v1 (1-step department_manager)"); + // eslint-disable-next-line no-console console.log(`- leave types: ${DEFAULT_LEAVE_TYPES.length} seeded with default policies`); // eslint-disable-next-line no-console console.log(`- feature flags: ${DEFAULT_FEATURE_FLAGS.length} seeded`); diff --git a/server/src/services/documentSequence.js b/server/src/services/documentSequence.js index e15ee7dc..95f701a4 100644 --- a/server/src/services/documentSequence.js +++ b/server/src/services/documentSequence.js @@ -29,7 +29,7 @@ const DOCUMENT_TYPE_CONFIG = { }, }, purchase_request: { - defaultCategoryCode: "purchase_general", + defaultCategoryCode: "PR_STANDARD", preferLegacySequence: false, fallback: { prefix: "CG", diff --git a/server/tests/unit/constants/permissions.test.js b/server/tests/unit/constants/permissions.test.js index 4490ff2a..c1ce3c3a 100644 --- a/server/tests/unit/constants/permissions.test.js +++ b/server/tests/unit/constants/permissions.test.js @@ -35,6 +35,15 @@ describe("Permission Constants", () => { expect(hrPermissions).toEqual(expect.arrayContaining(["leave.view_all", "leave.manage"])); }); + it("grants procurement permissions to configured roles", () => { + expect(hasPermission(resolvePermissionsForRoles(["employee"]), "procurement.create")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["employee"]), "procurement.view_own")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["manager"]), "procurement.approve")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["finance"]), "procurement.approve")).toBe(false); + expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "procurement.manage")).toBe(true); + expect(hasPermission(resolvePermissionsForRoles(["administration"]), "procurement.manage")).toBe(true); + }); + it("maps workflow entities to correct permission codes", () => { expect(resolveEntityPermission("reimbursement", "approve")).toBe("expense.approve"); expect(resolveEntityPermission("reimbursement", "transfer")).toBe("expense.transfer"); diff --git a/server/tests/unit/documentSequence.service.test.js b/server/tests/unit/documentSequence.service.test.js index a3c6f47f..df2eb770 100644 --- a/server/tests/unit/documentSequence.service.test.js +++ b/server/tests/unit/documentSequence.service.test.js @@ -87,8 +87,8 @@ describe("documentSequence service", () => { it("falls back to default purchase category when sequence row does not exist", async () => { const prisma = buildPrisma({ categoryMap: { - purchase_general: { - code: "purchase_general", + PR_STANDARD: { + code: "PR_STANDARD", entity_type: "purchase_request", sequence_prefix: "CG", number_length: 4, @@ -102,15 +102,15 @@ describe("documentSequence service", () => { const config = await resolveDocumentSequenceConfig(prisma, { documentType: "purchase_request" }); expect(config.source).toBe("document_category"); - expect(config.documentCategoryCode).toBe("purchase_general"); + expect(config.documentCategoryCode).toBe("PR_STANDARD"); expect(config.prefix).toBe("CG"); }); it("issues a document number using category-driven config", async () => { const prisma = buildPrisma({ categoryMap: { - purchase_general: { - code: "purchase_general", + PR_STANDARD: { + code: "PR_STANDARD", entity_type: "purchase_request", sequence_prefix: "CG", number_length: 4,