GWM : locatin api parent id removed
This commit is contained in:
parent
67b2c32884
commit
8de3afedb3
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
5
scripts/patch-locations-drop-parent-id.sql
Normal file
5
scripts/patch-locations-drop-parent-id.sql
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user