GWM : locatin api parent id removed

This commit is contained in:
Gowtham M 2026-07-20 11:55:39 +05:30
parent 67b2c32884
commit 8de3afedb3
7 changed files with 12 additions and 73 deletions

View File

@ -182,9 +182,11 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
| [~] | Asset Subcategories | removed — use Item Subcategories |
| [x] | Departments | `/masters/departments` |
| [x] | Designations | `/masters/designations` |
| [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`) |
| [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`); no plant↔warehouse hierarchy |
| [x] | Plants | `/masters/plants` | Alias — plant locations only |
| [x] | Warehouses | `/masters/warehouses` | Alias — warehouse locations only |
**DB patch:** `scripts/patch-locations-drop-parent-id.sql` — drops `locations.parent_id` (flat locations).
| [x] | Document Series | `/masters/document-series` |
**Masters total:** 13 modules × 5 endpoints (+ plants/warehouses aliases)

View File

@ -592,7 +592,6 @@ model locations {
type String @db.VarChar(20)
code String @db.VarChar(30)
name String @db.VarChar(150)
parent_id BigInt?
gstin String? @db.VarChar(15)
address String?
city String? @db.VarChar(100)
@ -610,8 +609,6 @@ model locations {
transfers_to asset_transfers[] @relation("TransferToLocation")
assets_location assets[] @relation("AssetLocation")
grn_location grn[] @relation("GRNLocation")
parent locations? @relation("LocationHierarchy", fields: [parent_id], references: [id], onUpdate: NoAction)
children locations[] @relation("LocationHierarchy")
po_billing purchase_orders[] @relation("POBillingLocation")
po_shipping purchase_orders[] @relation("POShippingLocation")
users_plant users[] @relation("UserPlantLocation")
@ -619,7 +616,6 @@ model locations {
users_updated users? @relation("locations_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_locations_updated_by")
@@index([type], map: "idx_locations_type")
@@index([parent_id], map: "idx_locations_parent_id")
}
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.

View File

@ -0,0 +1,5 @@
-- Remove location hierarchy (parent_id). Plants and warehouses are flat locations.
ALTER TABLE locations DROP CONSTRAINT IF EXISTS locations_parent_id_fkey;
DROP INDEX IF EXISTS idx_locations_parent_id;
ALTER TABLE locations DROP COLUMN IF EXISTS parent_id;

View File

@ -358,7 +358,6 @@ components:
type: { type: string, enum: [plant, warehouse], example: plant }
code: { type: string, example: 'PLT01' }
name: { type: string, example: 'Bharat Plant 1' }
parent_id: { type: integer, nullable: true, example: 1, description: Required when type is warehouse (plant id) }
gstin: { type: string, example: '27ABCDE1234F1Z5' }
address: { type: string, example: 'MIDC, Nashik' }
city: { type: string, example: 'Nashik' }
@ -373,7 +372,6 @@ components:
properties:
code: { type: string, example: 'PLT01' }
name: { type: string, example: 'Bharat Plant 1' }
parent_id: { type: integer, nullable: true, example: 1 }
gstin: { type: string, example: '27ABCDE1234F1Z5' }
address: { type: string, example: 'MIDC, Nashik' }
city: { type: string, example: 'Nashik' }
@ -384,11 +382,10 @@ components:
is_active: { type: boolean, example: true }
WarehousesCreateBody:
type: object
required: [code, name, plant_id]
required: [code, name]
properties:
code: { type: string, example: 'WH01' }
name: { type: string, example: 'Main Warehouse' }
plant_id: { type: integer, example: 1 }
location: { type: string, example: 'Ground Floor' }
is_active: { type: boolean, example: true }
WarehousesUpdateBody:
@ -397,7 +394,6 @@ components:
properties:
code: { type: string, example: 'WH01' }
name: { type: string, example: 'Main Warehouse' }
plant_id: { type: integer, example: 1 }
location: { type: string, example: 'Ground Floor' }
is_active: { type: boolean, example: true }
PlantsCreateBody:
@ -1527,10 +1523,6 @@ paths:
- in: query
name: type
schema: { type: string, enum: [plant, warehouse] }
- in: query
name: parent_id
schema: { type: integer }
description: Filter warehouses by plant id
responses:
"200":
description: List fetched

View File

@ -6,7 +6,6 @@ const { parseId } = require('../../../utils/parseId');
const { rowsToCsv } = require('../../../utils/csv');
const {
LOCATION_TYPES,
assertPlant,
toPlantResponse,
toWarehouseResponse,
toLocationResponse,
@ -31,7 +30,6 @@ const buildPlantData = (payload) => ({
pincode: normalizeString(payload.pincode),
phone: normalizeString(payload.phone),
is_active: payload.is_active,
parent_id: null,
location: null,
});
@ -39,7 +37,6 @@ const buildWarehouseData = (payload) => ({
type: LOCATION_TYPES.WAREHOUSE,
code: payload.code ? String(payload.code).trim().toUpperCase() : undefined,
name: normalizeString(payload.name),
parent_id: payload.parent_id ?? payload.plant_id ?? null,
location: normalizeString(payload.location),
is_active: payload.is_active,
gstin: null,
@ -50,8 +47,6 @@ const buildWarehouseData = (payload) => ({
phone: null,
});
const warehouseInclude = { parent: { select: { id: true, code: true, name: true } } };
const assertUniqueCode = async (code, excludeId = null) => {
if (!code) return;
const existing = await prisma.locations.findFirst({
@ -71,19 +66,10 @@ const createLocation = async (type, payload, userId, requestId) => {
if (!data.code || !data.name) throw new ApiError(422, 'code and name are required');
await assertUniqueCode(data.code);
if (type === LOCATION_TYPES.WAREHOUSE) {
if (!data.parent_id) throw new ApiError(422, 'parent_id (plant) is required for warehouse');
await assertPlant(data.parent_id, 'parent_id');
data.parent_id = BigInt(data.parent_id);
}
data.created_by = userId ? BigInt(userId) : null;
data.updated_by = userId ? BigInt(userId) : null;
const created = await prisma.locations.create({
data,
include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined,
});
const created = await prisma.locations.create({ data });
const response =
type === LOCATION_TYPES.PLANT ? toPlantResponse(created) : toWarehouseResponse(created);
@ -106,7 +92,6 @@ const buildLocationsWhere = (query, type = null) => {
const where = {
deleted_at: null,
...(effectiveType ? { type: effectiveType } : {}),
...(query.parent_id ? { parent_id: BigInt(query.parent_id) } : {}),
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
...(query.search
? {
@ -123,9 +108,6 @@ const buildLocationsWhere = (query, type = null) => {
const listLocations = async (query, type = null) => {
const { where, effectiveType } = buildLocationsWhere(query, type);
const locationsInclude =
effectiveType === LOCATION_TYPES.WAREHOUSE || !effectiveType ? warehouseInclude : undefined;
const mapRow = (row) => {
if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row);
if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
@ -136,7 +118,6 @@ const listLocations = async (query, type = null) => {
where.is_active = true;
const rows = await prisma.locations.findMany({
where,
include: locationsInclude,
orderBy: { created_at: 'desc' },
});
const data = rows.map(mapRow);
@ -148,7 +129,6 @@ const listLocations = async (query, type = null) => {
const [rows, total] = await Promise.all([
prisma.locations.findMany({
where,
include: locationsInclude,
orderBy: { created_at: 'desc' },
skip,
take: limit,
@ -163,10 +143,6 @@ const exportLocations = async (query, type = null) => {
const { where, effectiveType } = buildLocationsWhere(query, type);
const rows = await prisma.locations.findMany({
where,
include:
effectiveType === LOCATION_TYPES.WAREHOUSE || !effectiveType
? warehouseInclude
: undefined,
orderBy: { created_at: 'desc' },
});
@ -181,8 +157,6 @@ const exportLocations = async (query, type = null) => {
? [
{ key: 'code', header: 'Code' },
{ key: 'name', header: 'Name' },
{ key: (row) => row.parent?.code || row.plant?.code || '', header: 'Plant Code' },
{ key: (row) => row.parent?.name || row.plant?.name || '', header: 'Plant Name' },
{ key: 'location', header: 'Location' },
{ key: 'is_active', header: 'Is Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
@ -215,7 +189,6 @@ const getLocationById = async (id, type = null) => {
deleted_at: null,
...(type ? { type } : {}),
},
include: warehouseInclude,
});
if (!row) throw new ApiError(404, type ? `${type} not found` : 'Location not found');
@ -228,7 +201,6 @@ const updateLocation = async (id, type, payload, userId, requestId) => {
const idBigInt = parseId(id);
const existing = await prisma.locations.findFirst({
where: { id: idBigInt, deleted_at: null, type },
include: warehouseInclude,
});
if (!existing) throw new ApiError(404, `${type} not found`);
@ -240,18 +212,11 @@ const updateLocation = async (id, type, payload, userId, requestId) => {
if (data.code) await assertUniqueCode(data.code, id);
if (type === LOCATION_TYPES.WAREHOUSE && data.parent_id !== undefined) {
if (!data.parent_id) throw new ApiError(422, 'parent_id (plant) is required for warehouse');
await assertPlant(data.parent_id, 'parent_id');
data.parent_id = BigInt(data.parent_id);
}
data.updated_by = userId ? BigInt(userId) : null;
const updated = await prisma.locations.update({
where: { id: idBigInt },
data,
include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined,
});
const oldValue =
@ -276,7 +241,6 @@ const deleteLocation = async (id, type, userId, requestId) => {
const idBigInt = parseId(id);
const existing = await prisma.locations.findFirst({
where: { id: idBigInt, deleted_at: null, type },
include: warehouseInclude,
});
if (!existing) throw new ApiError(404, `${type} not found`);
@ -302,7 +266,6 @@ const deleteLocation = async (id, type, userId, requestId) => {
const deleteLocationById = async (id, userId, requestId) => {
const existing = await prisma.locations.findFirst({
where: { id: parseId(id), deleted_at: null },
include: warehouseInclude,
});
if (!existing) throw new ApiError(404, 'Location not found');
return deleteLocation(id, existing.type, userId, requestId);

View File

@ -13,8 +13,6 @@ const locationFields = {
pincode: Joi.string().max(10).allow(null, '').optional(),
phone: Joi.string().max(15).allow(null, '').optional(),
location: Joi.string().max(200).allow(null, '').optional(),
parent_id: Joi.number().integer().positive().optional(),
plant_id: Joi.number().integer().positive().optional(),
is_active: Joi.boolean().optional(),
};
@ -45,7 +43,6 @@ const updatePlantSchema = Joi.object({
const createWarehouseSchema = Joi.object({
code: masterCode({ max: 30, required: true }),
name: masterName({ max: 150, required: true }),
plant_id: Joi.number().integer().positive().required(),
location: locationFields.location,
is_active: Joi.boolean().default(true),
});
@ -53,7 +50,6 @@ const createWarehouseSchema = Joi.object({
const updateWarehouseSchema = Joi.object({
code: masterCode({ max: 30 }),
name: masterName({ max: 150 }),
plant_id: Joi.number().integer().positive().optional(),
location: locationFields.location,
is_active: Joi.boolean().optional(),
}).min(1);
@ -62,11 +58,6 @@ const createLocationSchema = Joi.object({
type: Joi.string().valid('plant', 'warehouse').required(),
code: masterCode({ max: 30, required: true }),
name: masterName({ max: 150, required: true }),
parent_id: Joi.when('type', {
is: 'warehouse',
then: Joi.number().integer().positive().required(),
otherwise: Joi.number().integer().positive().allow(null).optional(),
}),
gstin: locationFields.gstin,
address: locationFields.address,
city: locationFields.city,
@ -80,7 +71,6 @@ const createLocationSchema = Joi.object({
const updateLocationSchema = Joi.object({
code: masterCode({ max: 30 }),
name: masterName({ max: 150 }),
parent_id: Joi.number().integer().positive().optional(),
gstin: locationFields.gstin,
address: locationFields.address,
city: locationFields.city,
@ -93,7 +83,6 @@ const updateLocationSchema = Joi.object({
const listLocationsQuerySchema = listQuerySchema.keys({
type: Joi.string().valid('plant', 'warehouse').optional(),
parent_id: Joi.number().integer().positive().optional(),
});
const exportQuerySchema = toExportQuerySchema(listLocationsQuerySchema);

View File

@ -15,9 +15,6 @@ const assertLocation = async (id, expectedType, label, { requireActive = true }
type: expectedType,
deleted_at: null,
},
include: expectedType === LOCATION_TYPES.WAREHOUSE
? { parent: { select: { id: true, code: true, name: true } } }
: undefined,
});
if (!row) throw new ApiError(422, `Invalid ${label}`);
@ -31,13 +28,12 @@ const assertPlant = (id, label = 'plant_id') =>
const assertWarehouse = (id, label = 'warehouse_id') =>
assertLocation(id, LOCATION_TYPES.WAREHOUSE, label);
/** Assert a location of any type (plant or warehouse). Returns the row (with parent). */
/** Assert a location of any type (plant or warehouse). */
const assertAnyLocation = async (id, label, { requireActive = true } = {}) => {
if (!id) return null;
const row = await prisma.locations.findFirst({
where: { id: BigInt(id), deleted_at: null },
include: { parent: { select: { id: true, code: true, name: true } } },
});
if (!row) throw new ApiError(422, `Invalid ${label}`);
@ -53,9 +49,7 @@ const warehouseListSelect = {
id: true,
code: true,
name: true,
parent_id: true,
location: true,
parent: { select: plantListSelect },
};
const toPlantResponse = (row) => ({
@ -80,8 +74,6 @@ const toWarehouseResponse = (row) => ({
id: row.id,
code: row.code,
name: row.name,
plant_id: row.parent_id,
plant: row.parent || null,
location: row.location,
is_active: row.is_active,
created_by: row.created_by,
@ -93,7 +85,7 @@ const toWarehouseResponse = (row) => ({
const toLocationResponse = (row) => {
if (row.type === LOCATION_TYPES.PLANT) return { ...toPlantResponse(row), type: row.type };
return { ...toWarehouseResponse(row), type: row.type, parent_id: row.parent_id };
return { ...toWarehouseResponse(row), type: row.type };
};
module.exports = {