From b1edf6eed3bd61f926f0eb406bd9927e926caf7b Mon Sep 17 00:00:00 2001 From: "reza.g" Date: Sat, 19 Sep 2026 14:32:46 +0330 Subject: [PATCH] feat(inventory)!: lock warehouses permanently to their parent project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Warehouses now belong strictly and exclusively to the project they were created under; cross-project reassignment is removed end-to-end. Backend: PATCH /inventory/warehouses/{id} no longer applies project_id — any attempt to move a warehouse to a different project returns 400 with «پروژه‌ی انبار قابل تغییر نیست — انبار برای همیشه به پروژه‌ی والدش تعلق دارد» (sending the current project stays a harmless no-op). The WarehouseUpdate schema documents project_id as accept-only-to-reject; creation still requires project_id and delete is unchanged. Frontend: the Organization page's per-warehouse project dropdown («انتقال به پروژه دیگر») and its moveWarehouse handler are removed — warehouses render statically inside their project card with only the dashboard link and delete; the warehouses.update API type no longer accepts project_id. WarehousesPage already had no move UI (rename/describe only). Verified end-to-end against the running stack: rename 200; same-project project_id no-op 200; cross-project reassignment 400 with the guard message; warehouse remains in its original project; test data cleaned up. python compile + api/frontend builds clean. --- backend/app/routers/inventory.py | 11 +++++----- backend/app/schemas/inventory.py | 7 ++++++ frontend/src/lib/api.ts | 3 ++- .../src/pages/Inventory/OrganizationPage.tsx | 22 ++----------------- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/backend/app/routers/inventory.py b/backend/app/routers/inventory.py index c75f210..813d334 100644 --- a/backend/app/routers/inventory.py +++ b/backend/app/routers/inventory.py @@ -271,12 +271,13 @@ async def update_warehouse( wh.name = payload.name if payload.description is not None: wh.description = payload.description + # پروژه‌ی انبار تغییرناپذیر است — انبار برای همیشه به پروژه‌ی والدش تعلق + # دارد و فقط با حذف و ساخت دوباره می‌تواند جابه‌جا شود. if payload.project_id is not None and str(payload.project_id) != str(wh.project_id): - if await db.get(Project, payload.project_id) is None: - raise NotFoundError("پروژه مقصد یافت نشد", field="project_id") - if scope.role == ROLE_PM and payload.project_id != scope.project_id: - raise ForbiddenError("PM فقط می‌تواند انبار را در پروژه خودش جابه‌جا کند", field="project_id") - wh.project_id = payload.project_id + raise ValidationError( + "پروژه‌ی انبار قابل تغییر نیست — انبار برای همیشه به پروژه‌ی والدش تعلق دارد", + field="project_id", + ) await db.flush() return {"data": warehouse_to_dict(wh, await db.get(Project, wh.project_id))} diff --git a/backend/app/schemas/inventory.py b/backend/app/schemas/inventory.py index 086cf43..57e69b0 100644 --- a/backend/app/schemas/inventory.py +++ b/backend/app/schemas/inventory.py @@ -30,6 +30,13 @@ class WarehouseCreate(APIModel): class WarehouseUpdate(APIModel): + """Rename/re-describe a warehouse. + + `project_id` is accepted only to be rejected: a warehouse belongs + permanently to its parent project and cannot be reassigned (the router + answers 400 unless the value equals the current project). + """ + name: str | None = Field(default=None, min_length=1, max_length=255) description: str | None = None project_id: UUID | None = None diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 10eecc9..a1da178 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1675,7 +1675,8 @@ export const apiV26 = { const d = await postData[0]>(`/inventory/warehouses${projectQuery()}`, body); return rawWarehouse(d); }, - update: async (id: string, body: { name?: string; description?: string | null; project_id?: string }): Promise => { + // project_id عمداً در تایپ نیست — انبار برای همیشه به پروژه‌ی والدش تعلق دارد + update: async (id: string, body: { name?: string; description?: string | null }): Promise => { const d = await patchData[0]>(`/inventory/warehouses/${id}${projectQuery()}`, body); return rawWarehouse(d); }, diff --git a/frontend/src/pages/Inventory/OrganizationPage.tsx b/frontend/src/pages/Inventory/OrganizationPage.tsx index b1cfb90..0146aa8 100644 --- a/frontend/src/pages/Inventory/OrganizationPage.tsx +++ b/frontend/src/pages/Inventory/OrganizationPage.tsx @@ -148,16 +148,8 @@ export function OrganizationPage() { } }; - const moveWarehouse = async (id: string, projectId: string) => { - try { - await apiV26.warehouses.update(id, { project_id: projectId }); - toast.success("انبار منتقل شد ✓"); - await data.refresh(); - } catch (err) { - toast.error(err instanceof Error ? err.message : "خطا در انتقال انبار"); - } - }; - + // انبار برای همیشه به پروژه‌ی والدش تعلق دارد — انتقال به پروژه‌ی دیگر وجود ندارد؛ + // انبار فقط با حذف و ساخت دوباره زیر پروژه‌ی موردنظر جابه‌جا می‌شود. const removeWarehouse = async (id: string) => { if (!confirm("انبار حذف شود؟ (انبارهای دارای سابقه تراکنش قابل حذف نیستند)")) return; try { @@ -329,16 +321,6 @@ export function OrganizationPage() { > 📊 داشبورد -