feat(inventory)!: lock warehouses permanently to their parent project
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.
This commit is contained in:
@@ -271,12 +271,13 @@ async def update_warehouse(
|
|||||||
wh.name = payload.name
|
wh.name = payload.name
|
||||||
if payload.description is not None:
|
if payload.description is not None:
|
||||||
wh.description = payload.description
|
wh.description = payload.description
|
||||||
|
# پروژهی انبار تغییرناپذیر است — انبار برای همیشه به پروژهی والدش تعلق
|
||||||
|
# دارد و فقط با حذف و ساخت دوباره میتواند جابهجا شود.
|
||||||
if payload.project_id is not None and str(payload.project_id) != str(wh.project_id):
|
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 ValidationError(
|
||||||
raise NotFoundError("پروژه مقصد یافت نشد", field="project_id")
|
"پروژهی انبار قابل تغییر نیست — انبار برای همیشه به پروژهی والدش تعلق دارد",
|
||||||
if scope.role == ROLE_PM and payload.project_id != scope.project_id:
|
field="project_id",
|
||||||
raise ForbiddenError("PM فقط میتواند انبار را در پروژه خودش جابهجا کند", field="project_id")
|
)
|
||||||
wh.project_id = payload.project_id
|
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return {"data": warehouse_to_dict(wh, await db.get(Project, wh.project_id))}
|
return {"data": warehouse_to_dict(wh, await db.get(Project, wh.project_id))}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ class WarehouseCreate(APIModel):
|
|||||||
|
|
||||||
|
|
||||||
class WarehouseUpdate(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)
|
name: str | None = Field(default=None, min_length=1, max_length=255)
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
project_id: UUID | None = None
|
project_id: UUID | None = None
|
||||||
|
|||||||
@@ -1675,7 +1675,8 @@ export const apiV26 = {
|
|||||||
const d = await postData<Parameters<typeof rawWarehouse>[0]>(`/inventory/warehouses${projectQuery()}`, body);
|
const d = await postData<Parameters<typeof rawWarehouse>[0]>(`/inventory/warehouses${projectQuery()}`, body);
|
||||||
return rawWarehouse(d);
|
return rawWarehouse(d);
|
||||||
},
|
},
|
||||||
update: async (id: string, body: { name?: string; description?: string | null; project_id?: string }): Promise<Warehouse> => {
|
// project_id عمداً در تایپ نیست — انبار برای همیشه به پروژهی والدش تعلق دارد
|
||||||
|
update: async (id: string, body: { name?: string; description?: string | null }): Promise<Warehouse> => {
|
||||||
const d = await patchData<Parameters<typeof rawWarehouse>[0]>(`/inventory/warehouses/${id}${projectQuery()}`, body);
|
const d = await patchData<Parameters<typeof rawWarehouse>[0]>(`/inventory/warehouses/${id}${projectQuery()}`, body);
|
||||||
return rawWarehouse(d);
|
return rawWarehouse(d);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) => {
|
const removeWarehouse = async (id: string) => {
|
||||||
if (!confirm("انبار حذف شود؟ (انبارهای دارای سابقه تراکنش قابل حذف نیستند)")) return;
|
if (!confirm("انبار حذف شود؟ (انبارهای دارای سابقه تراکنش قابل حذف نیستند)")) return;
|
||||||
try {
|
try {
|
||||||
@@ -329,16 +321,6 @@ export function OrganizationPage() {
|
|||||||
>
|
>
|
||||||
📊 داشبورد
|
📊 داشبورد
|
||||||
</button>
|
</button>
|
||||||
<select
|
|
||||||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[11px]"
|
|
||||||
value={w.project_id}
|
|
||||||
onChange={(e) => void moveWarehouse(w.id, e.target.value)}
|
|
||||||
title="انتقال به پروژه دیگر"
|
|
||||||
>
|
|
||||||
{data.lookup.projects.map((p2) => (
|
|
||||||
<option key={p2.id} value={p2.id}>{p2.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button className="text-muted hover:text-red" onClick={() => void removeWarehouse(w.id)} title="حذف انبار">✕</button>
|
<button className="text-muted hover:text-red" onClick={() => void removeWarehouse(w.id)} title="حذف انبار">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user