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 | | [~] | Asset Subcategories | removed — use Item Subcategories |
| [x] | Departments | `/masters/departments` | | [x] | Departments | `/masters/departments` |
| [x] | Designations | `/masters/designations` | | [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] | Plants | `/masters/plants` | Alias — plant locations only |
| [x] | Warehouses | `/masters/warehouses` | Alias — warehouse 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` | | [x] | Document Series | `/masters/document-series` |
**Masters total:** 13 modules × 5 endpoints (+ plants/warehouses aliases) **Masters total:** 13 modules × 5 endpoints (+ plants/warehouses aliases)

View File

@ -592,7 +592,6 @@ model locations {
type String @db.VarChar(20) type String @db.VarChar(20)
code String @db.VarChar(30) code String @db.VarChar(30)
name String @db.VarChar(150) name String @db.VarChar(150)
parent_id BigInt?
gstin String? @db.VarChar(15) gstin String? @db.VarChar(15)
address String? address String?
city String? @db.VarChar(100) city String? @db.VarChar(100)
@ -610,8 +609,6 @@ model locations {
transfers_to asset_transfers[] @relation("TransferToLocation") transfers_to asset_transfers[] @relation("TransferToLocation")
assets_location assets[] @relation("AssetLocation") assets_location assets[] @relation("AssetLocation")
grn_location grn[] @relation("GRNLocation") 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_billing purchase_orders[] @relation("POBillingLocation")
po_shipping purchase_orders[] @relation("POShippingLocation") po_shipping purchase_orders[] @relation("POShippingLocation")
users_plant users[] @relation("UserPlantLocation") 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") 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([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. /// 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 } type: { type: string, enum: [plant, warehouse], example: plant }
code: { type: string, example: 'PLT01' } code: { type: string, example: 'PLT01' }
name: { type: string, example: 'Bharat Plant 1' } 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' } gstin: { type: string, example: '27ABCDE1234F1Z5' }
address: { type: string, example: 'MIDC, Nashik' } address: { type: string, example: 'MIDC, Nashik' }
city: { type: string, example: 'Nashik' } city: { type: string, example: 'Nashik' }
@ -373,7 +372,6 @@ components:
properties: properties:
code: { type: string, example: 'PLT01' } code: { type: string, example: 'PLT01' }
name: { type: string, example: 'Bharat Plant 1' } name: { type: string, example: 'Bharat Plant 1' }
parent_id: { type: integer, nullable: true, example: 1 }
gstin: { type: string, example: '27ABCDE1234F1Z5' } gstin: { type: string, example: '27ABCDE1234F1Z5' }
address: { type: string, example: 'MIDC, Nashik' } address: { type: string, example: 'MIDC, Nashik' }
city: { type: string, example: 'Nashik' } city: { type: string, example: 'Nashik' }
@ -384,11 +382,10 @@ components:
is_active: { type: boolean, example: true } is_active: { type: boolean, example: true }
WarehousesCreateBody: WarehousesCreateBody:
type: object type: object
required: [code, name, plant_id] required: [code, name]
properties: properties:
code: { type: string, example: 'WH01' } code: { type: string, example: 'WH01' }
name: { type: string, example: 'Main Warehouse' } name: { type: string, example: 'Main Warehouse' }
plant_id: { type: integer, example: 1 }
location: { type: string, example: 'Ground Floor' } location: { type: string, example: 'Ground Floor' }
is_active: { type: boolean, example: true } is_active: { type: boolean, example: true }
WarehousesUpdateBody: WarehousesUpdateBody:
@ -397,7 +394,6 @@ components:
properties: properties:
code: { type: string, example: 'WH01' } code: { type: string, example: 'WH01' }
name: { type: string, example: 'Main Warehouse' } name: { type: string, example: 'Main Warehouse' }
plant_id: { type: integer, example: 1 }
location: { type: string, example: 'Ground Floor' } location: { type: string, example: 'Ground Floor' }
is_active: { type: boolean, example: true } is_active: { type: boolean, example: true }
PlantsCreateBody: PlantsCreateBody:
@ -1527,10 +1523,6 @@ paths:
- in: query - in: query
name: type name: type
schema: { type: string, enum: [plant, warehouse] } schema: { type: string, enum: [plant, warehouse] }
- in: query
name: parent_id
schema: { type: integer }
description: Filter warehouses by plant id
responses: responses:
"200": "200":
description: List fetched description: List fetched

View File

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

View File

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

View File

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