GWM : plan and warehouse merged as locations

This commit is contained in:
Gowtham M 2026-06-23 14:34:36 +05:30
parent 58d281c6b9
commit f04b917b76
19 changed files with 817 additions and 276 deletions

View File

@ -171,8 +171,9 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
| [x] | Asset Categories | `/masters/asset-categories` | | [x] | Asset Categories | `/masters/asset-categories` |
| [x] | Departments | `/masters/departments` | | [x] | Departments | `/masters/departments` |
| [x] | Designations | `/masters/designations` | | [x] | Designations | `/masters/designations` |
| [x] | Plants | `/masters/plants` | | [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`) |
| [x] | Warehouses | `/masters/warehouses` | | [x] | Plants | `/masters/plants` | Alias — plant locations only |
| [x] | Warehouses | `/masters/warehouses` | Alias — warehouse locations only |
| [x] | Document Series | `/masters/document-series` | | [x] | Document Series | `/masters/document-series` |
**Masters total:** 14 modules × 5 endpoints = **70 APIs** [x] **Masters total:** 14 modules × 5 endpoints = **70 APIs** [x]

View File

@ -171,13 +171,13 @@ model asset_transfers {
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction) assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
departments_asset_transfers_from_department_idTodepartments departments? @relation("asset_transfers_from_department_idTodepartments", fields: [from_department_id], references: [id], onUpdate: NoAction) departments_asset_transfers_from_department_idTodepartments departments? @relation("asset_transfers_from_department_idTodepartments", fields: [from_department_id], references: [id], onUpdate: NoAction)
plants_asset_transfers_from_plant_idToplants plants? @relation("asset_transfers_from_plant_idToplants", fields: [from_plant_id], references: [id], onUpdate: NoAction) from_plant locations? @relation("TransferFromPlant", fields: [from_plant_id], references: [id], onUpdate: NoAction)
users_asset_transfers_from_user_idTousers users? @relation("asset_transfers_from_user_idTousers", fields: [from_user_id], references: [id], onUpdate: NoAction) users_asset_transfers_from_user_idTousers users? @relation("asset_transfers_from_user_idTousers", fields: [from_user_id], references: [id], onUpdate: NoAction)
warehouses_asset_transfers_from_warehouse_idTowarehouses warehouses? @relation("asset_transfers_from_warehouse_idTowarehouses", fields: [from_warehouse_id], references: [id], onUpdate: NoAction) from_warehouse locations? @relation("TransferFromWarehouse", fields: [from_warehouse_id], references: [id], onUpdate: NoAction)
departments_asset_transfers_to_department_idTodepartments departments? @relation("asset_transfers_to_department_idTodepartments", fields: [to_department_id], references: [id], onUpdate: NoAction) departments_asset_transfers_to_department_idTodepartments departments? @relation("asset_transfers_to_department_idTodepartments", fields: [to_department_id], references: [id], onUpdate: NoAction)
plants_asset_transfers_to_plant_idToplants plants? @relation("asset_transfers_to_plant_idToplants", fields: [to_plant_id], references: [id], onUpdate: NoAction) to_plant locations? @relation("TransferToPlant", fields: [to_plant_id], references: [id], onUpdate: NoAction)
users_asset_transfers_to_user_idTousers users? @relation("asset_transfers_to_user_idTousers", fields: [to_user_id], references: [id], onUpdate: NoAction) users_asset_transfers_to_user_idTousers users? @relation("asset_transfers_to_user_idTousers", fields: [to_user_id], references: [id], onUpdate: NoAction)
warehouses_asset_transfers_to_warehouse_idTowarehouses warehouses? @relation("asset_transfers_to_warehouse_idTowarehouses", fields: [to_warehouse_id], references: [id], onUpdate: NoAction) to_warehouse locations? @relation("TransferToWarehouse", fields: [to_warehouse_id], references: [id], onUpdate: NoAction)
users_asset_transfers_transferred_byTousers users? @relation("asset_transfers_transferred_byTousers", fields: [transferred_by], references: [id], onUpdate: NoAction) users_asset_transfers_transferred_byTousers users? @relation("asset_transfers_transferred_byTousers", fields: [transferred_by], references: [id], onUpdate: NoAction)
@@index([asset_id], map: "idx_asset_transfers_asset_id") @@index([asset_id], map: "idx_asset_transfers_asset_id")
@ -232,11 +232,11 @@ model assets {
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction) departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
grn grn? @relation(fields: [grn_id], references: [id], onUpdate: NoAction) grn grn? @relation(fields: [grn_id], references: [id], onUpdate: NoAction)
grn_items grn_items? @relation(fields: [grn_item_id], references: [id], onUpdate: NoAction) grn_items grn_items? @relation(fields: [grn_item_id], references: [id], onUpdate: NoAction)
plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction) plant locations @relation("AssetPlantLocation", fields: [plant_id], references: [id], onUpdate: NoAction)
purchase_orders purchase_orders? @relation(fields: [po_id], references: [id], onUpdate: NoAction) purchase_orders purchase_orders? @relation(fields: [po_id], references: [id], onUpdate: NoAction)
users_assets_updated_byTousers users? @relation("assets_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) users_assets_updated_byTousers users? @relation("assets_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
vendors_assets_vendor_idTovendors vendors? @relation("assets_vendor_idTovendors", fields: [vendor_id], references: [id], onUpdate: NoAction) vendors_assets_vendor_idTovendors vendors? @relation("assets_vendor_idTovendors", fields: [vendor_id], references: [id], onUpdate: NoAction)
warehouses warehouses? @relation(fields: [warehouse_id], references: [id], onUpdate: NoAction) warehouse locations? @relation("AssetWarehouseLocation", fields: [warehouse_id], references: [id], onUpdate: NoAction)
@@index([asset_category_id], map: "idx_assets_category_id") @@index([asset_category_id], map: "idx_assets_category_id")
@@index([department_id], map: "idx_assets_dept_id") @@index([department_id], map: "idx_assets_dept_id")
@ -375,7 +375,7 @@ model grn {
users_grn_received_byTousers users? @relation("grn_received_byTousers", fields: [received_by], references: [id], onUpdate: NoAction) users_grn_received_byTousers users? @relation("grn_received_byTousers", fields: [received_by], references: [id], onUpdate: NoAction)
users_grn_updated_byTousers users? @relation("grn_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) users_grn_updated_byTousers users? @relation("grn_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction) vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction)
warehouses warehouses @relation(fields: [warehouse_id], references: [id], onUpdate: NoAction) warehouse locations @relation("GRNWarehouseLocation", fields: [warehouse_id], references: [id], onUpdate: NoAction)
grn_attachments grn_attachments[] grn_attachments grn_attachments[]
grn_items grn_items[] grn_items grn_items[]
@ -576,30 +576,42 @@ model permissions {
@@unique([module_id, action]) @@unique([module_id, action])
} }
model plants { model locations {
id BigInt @id @default(autoincrement()) id BigInt @id @default(autoincrement())
code String @unique @db.VarChar(20) type String @db.VarChar(20)
name String @db.VarChar(150) code String @db.VarChar(30)
gstin String? @db.VarChar(15) name String @db.VarChar(150)
address String? parent_id BigInt?
city String? @db.VarChar(100) gstin String? @db.VarChar(15)
state String? @db.VarChar(100) address String?
pincode String? @db.VarChar(10) city String? @db.VarChar(100)
phone String? @db.VarChar(15) state String? @db.VarChar(100)
is_active Boolean @default(true) pincode String? @db.VarChar(10)
created_by BigInt? phone String? @db.VarChar(15)
updated_by BigInt? location String? @db.VarChar(200)
created_at DateTime @default(now()) @db.Timestamptz(6) is_active Boolean @default(true)
updated_at DateTime @default(now()) @db.Timestamptz(6) created_by BigInt?
deleted_at DateTime? @db.Timestamptz(6) updated_by BigInt?
asset_transfers_asset_transfers_from_plant_idToplants asset_transfers[] @relation("asset_transfers_from_plant_idToplants") created_at DateTime @default(now()) @db.Timestamptz(6)
asset_transfers_asset_transfers_to_plant_idToplants asset_transfers[] @relation("asset_transfers_to_plant_idToplants") updated_at DateTime @default(now()) @db.Timestamptz(6)
assets assets[] deleted_at DateTime? @db.Timestamptz(6)
users_plants_created_byTousers users? @relation("plants_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_plants_created_by") transfers_from_plant asset_transfers[] @relation("TransferFromPlant")
users_plants_updated_byTousers users? @relation("plants_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_plants_updated_by") transfers_to_plant asset_transfers[] @relation("TransferToPlant")
purchase_orders purchase_orders[] transfers_from_wh asset_transfers[] @relation("TransferFromWarehouse")
users_users_plant_idToplants users[] @relation("users_plant_idToplants") transfers_to_wh asset_transfers[] @relation("TransferToWarehouse")
warehouses warehouses[] assets_plant assets[] @relation("AssetPlantLocation")
assets_warehouse assets[] @relation("AssetWarehouseLocation")
grn_warehouse grn[] @relation("GRNWarehouseLocation")
parent locations? @relation("LocationHierarchy", fields: [parent_id], references: [id], onUpdate: NoAction)
children locations[] @relation("LocationHierarchy")
po_plant purchase_orders[] @relation("POPlantLocation")
po_warehouse purchase_orders[] @relation("POWarehouseLocation")
users_plant users[] @relation("UserPlantLocation")
users_created users? @relation("locations_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_locations_created_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([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.
@ -706,10 +718,10 @@ model purchase_orders {
purchase_orders purchase_orders? @relation("purchase_ordersTopurchase_orders", fields: [parent_po_id], references: [id], onUpdate: NoAction) purchase_orders purchase_orders? @relation("purchase_ordersTopurchase_orders", fields: [parent_po_id], references: [id], onUpdate: NoAction)
other_purchase_orders purchase_orders[] @relation("purchase_ordersTopurchase_orders") other_purchase_orders purchase_orders[] @relation("purchase_ordersTopurchase_orders")
payment_terms payment_terms? @relation(fields: [payment_term_id], references: [id], onUpdate: NoAction) payment_terms payment_terms? @relation(fields: [payment_term_id], references: [id], onUpdate: NoAction)
plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction) plant locations @relation("POPlantLocation", fields: [plant_id], references: [id], onUpdate: NoAction)
users_purchase_orders_updated_byTousers users? @relation("purchase_orders_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) users_purchase_orders_updated_byTousers users? @relation("purchase_orders_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction) vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction)
warehouses warehouses? @relation(fields: [warehouse_id], references: [id], onDelete: Restrict, onUpdate: NoAction) warehouse locations? @relation("POWarehouseLocation", fields: [warehouse_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
@@index([created_at], map: "idx_po_created_at") @@index([created_at], map: "idx_po_created_at")
@@index([po_date], map: "idx_po_po_date") @@index([po_date], map: "idx_po_po_date")
@ -846,8 +858,8 @@ model users {
password_reset_tokens password_reset_tokens[] password_reset_tokens password_reset_tokens[]
payment_terms_payment_terms_created_byTousers payment_terms[] @relation("payment_terms_created_byTousers") payment_terms_payment_terms_created_byTousers payment_terms[] @relation("payment_terms_created_byTousers")
payment_terms_payment_terms_updated_byTousers payment_terms[] @relation("payment_terms_updated_byTousers") payment_terms_payment_terms_updated_byTousers payment_terms[] @relation("payment_terms_updated_byTousers")
plants_plants_created_byTousers plants[] @relation("plants_created_byTousers") locations_locations_created_byTousers locations[] @relation("locations_created_byTousers")
plants_plants_updated_byTousers plants[] @relation("plants_updated_byTousers") locations_locations_updated_byTousers locations[] @relation("locations_updated_byTousers")
po_approvals po_approvals[] po_approvals po_approvals[]
po_attachments po_attachments[] po_attachments po_attachments[]
purchase_orders_purchase_orders_created_byTousers purchase_orders[] @relation("purchase_orders_created_byTousers") purchase_orders_purchase_orders_created_byTousers purchase_orders[] @relation("purchase_orders_created_byTousers")
@ -862,7 +874,7 @@ model users {
other_users_users_created_byTousers users[] @relation("users_created_byTousers") other_users_users_created_byTousers users[] @relation("users_created_byTousers")
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction) departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
designations designations? @relation(fields: [designation_id], references: [id], onUpdate: NoAction) designations designations? @relation(fields: [designation_id], references: [id], onUpdate: NoAction)
plants_users_plant_idToplants plants? @relation("users_plant_idToplants", fields: [plant_id], references: [id], onUpdate: NoAction) plant_location locations? @relation("UserPlantLocation", fields: [plant_id], references: [id], onUpdate: NoAction)
users_users_reporting_toTousers users? @relation("users_reporting_toTousers", fields: [reporting_to], references: [id], onUpdate: NoAction) users_users_reporting_toTousers users? @relation("users_reporting_toTousers", fields: [reporting_to], references: [id], onUpdate: NoAction)
other_users_users_reporting_toTousers users[] @relation("users_reporting_toTousers") other_users_users_reporting_toTousers users[] @relation("users_reporting_toTousers")
users_users_updated_byTousers users? @relation("users_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) users_users_updated_byTousers users? @relation("users_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
@ -877,8 +889,6 @@ model users {
vendor_item_mapping_vendor_item_mapping_updated_byTousers vendor_item_mapping[] @relation("vendor_item_mapping_updated_byTousers") vendor_item_mapping_vendor_item_mapping_updated_byTousers vendor_item_mapping[] @relation("vendor_item_mapping_updated_byTousers")
vendors_vendors_created_byTousers vendors[] @relation("vendors_created_byTousers") vendors_vendors_created_byTousers vendors[] @relation("vendors_created_byTousers")
vendors_vendors_updated_byTousers vendors[] @relation("vendors_updated_byTousers") vendors_vendors_updated_byTousers vendors[] @relation("vendors_updated_byTousers")
warehouses_warehouses_created_byTousers warehouses[] @relation("warehouses_created_byTousers")
warehouses_warehouses_updated_byTousers warehouses[] @relation("warehouses_updated_byTousers")
} }
/// 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.
@ -995,25 +1005,3 @@ model vendors {
@@index([vendor_name(ops: raw("gin_trgm_ops"))], map: "idx_vendors_name_trgm", type: Gin) @@index([vendor_name(ops: raw("gin_trgm_ops"))], map: "idx_vendors_name_trgm", type: Gin)
} }
model warehouses {
id BigInt @id @default(autoincrement())
code String @unique @db.VarChar(30)
name String @db.VarChar(150)
plant_id BigInt
location String? @db.VarChar(200)
is_active Boolean @default(true)
created_by BigInt?
updated_by BigInt?
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
deleted_at DateTime? @db.Timestamptz(6)
asset_transfers_asset_transfers_from_warehouse_idTowarehouses asset_transfers[] @relation("asset_transfers_from_warehouse_idTowarehouses")
asset_transfers_asset_transfers_to_warehouse_idTowarehouses asset_transfers[] @relation("asset_transfers_to_warehouse_idTowarehouses")
assets assets[]
grn grn[]
purchase_orders purchase_orders[]
users_warehouses_created_byTousers users? @relation("warehouses_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction)
users_warehouses_updated_byTousers users? @relation("warehouses_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
}

View File

@ -27,7 +27,7 @@ CREATE VIEW v_asset_expiry_alerts AS
FROM asset_amc_contracts amc FROM asset_amc_contracts amc
JOIN assets a ON a.id = amc.asset_id JOIN assets a ON a.id = amc.asset_id
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
JOIN plants p ON p.id = a.plant_id JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
LEFT JOIN departments d ON d.id = a.department_id LEFT JOIN departments d ON d.id = a.department_id
LEFT JOIN vendors v ON v.id = amc.vendor_id LEFT JOIN vendors v ON v.id = amc.vendor_id
WHERE amc.is_active = TRUE AND amc.deleted_at IS NULL AND a.deleted_at IS NULL WHERE amc.is_active = TRUE AND amc.deleted_at IS NULL AND a.deleted_at IS NULL
@ -48,7 +48,7 @@ UNION ALL
FROM asset_insurance_policies ins FROM asset_insurance_policies ins
JOIN assets a ON a.id = ins.asset_id JOIN assets a ON a.id = ins.asset_id
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
JOIN plants p ON p.id = a.plant_id JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
LEFT JOIN departments d ON d.id = a.department_id LEFT JOIN departments d ON d.id = a.department_id
WHERE ins.is_active = TRUE AND ins.deleted_at IS NULL AND a.deleted_at IS NULL WHERE ins.is_active = TRUE AND ins.deleted_at IS NULL AND a.deleted_at IS NULL
AND (ins.policy_end_date - CURRENT_DATE) <= 90 AND (ins.policy_end_date - CURRENT_DATE) <= 90
@ -67,7 +67,7 @@ UNION ALL
END END
FROM assets a FROM assets a
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
JOIN plants p ON p.id = a.plant_id JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
LEFT JOIN departments d ON d.id = a.department_id LEFT JOIN departments d ON d.id = a.department_id
WHERE a.warranty_expiry_date IS NOT NULL AND a.deleted_at IS NULL WHERE a.warranty_expiry_date IS NOT NULL AND a.deleted_at IS NULL
AND (a.warranty_expiry_date - CURRENT_DATE) <= 90; AND (a.warranty_expiry_date - CURRENT_DATE) <= 90;
@ -96,7 +96,7 @@ SELECT
amc.end_date AS amc_end_date amc.end_date AS amc_end_date
FROM assets a FROM assets a
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
JOIN plants p ON p.id = a.plant_id JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
LEFT JOIN departments d ON d.id = a.department_id LEFT JOIN departments d ON d.id = a.department_id
JOIN LATERAL ( JOIN LATERAL (
SELECT * FROM asset_service_visits SELECT * FROM asset_service_visits
@ -145,9 +145,9 @@ SELECT
a.created_at a.created_at
FROM assets a FROM assets a
JOIN asset_categories ac ON ac.id = a.asset_category_id JOIN asset_categories ac ON ac.id = a.asset_category_id
JOIN plants p ON p.id = a.plant_id JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
LEFT JOIN departments d ON d.id = a.department_id LEFT JOIN departments d ON d.id = a.department_id
LEFT JOIN warehouses w ON w.id = a.warehouse_id LEFT JOIN locations w ON w.id = a.warehouse_id AND w.type = 'warehouse'
LEFT JOIN users u ON u.id = a.assigned_to_user_id LEFT JOIN users u ON u.id = a.assigned_to_user_id
LEFT JOIN vendors v ON v.id = a.vendor_id LEFT JOIN vendors v ON v.id = a.vendor_id
LEFT JOIN asset_amc_contracts amc LEFT JOIN asset_amc_contracts amc

View File

@ -0,0 +1,148 @@
-- Merge plants + warehouses into locations (type: plant | warehouse)
BEGIN;
CREATE TABLE IF NOT EXISTS locations (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(20) NOT NULL CHECK (type IN ('plant', 'warehouse')),
code VARCHAR(30) NOT NULL,
name VARCHAR(150) NOT NULL,
parent_id BIGINT REFERENCES locations(id),
gstin VARCHAR(15),
address TEXT,
city VARCHAR(100),
state VARCHAR(100),
pincode VARCHAR(10),
phone VARCHAR(15),
location VARCHAR(200),
is_active BOOLEAN NOT NULL DEFAULT true,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_locations_code_active
ON locations (code)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_locations_type ON locations (type);
CREATE INDEX IF NOT EXISTS idx_locations_parent_id ON locations (parent_id);
-- Migrate plants (preserve ids) when legacy table exists
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'plants'
) THEN
INSERT INTO locations (
id, type, code, name, gstin, address, city, state, pincode, phone,
is_active, created_by, updated_by, created_at, updated_at, deleted_at
)
OVERRIDING SYSTEM VALUE
SELECT
id, 'plant', code, name, gstin, address, city, state, pincode, phone,
is_active, created_by, updated_by, created_at, updated_at, deleted_at
FROM plants
ON CONFLICT (id) DO NOTHING;
END IF;
END $$;
-- Migrate warehouses (preserve ids; plant_id -> parent_id)
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'warehouses'
) THEN
INSERT INTO locations (
id, type, code, name, parent_id, location,
is_active, created_by, updated_by, created_at, updated_at, deleted_at
)
OVERRIDING SYSTEM VALUE
SELECT
id, 'warehouse', code, name, plant_id, location,
is_active, created_by, updated_by, created_at, updated_at, deleted_at
FROM warehouses
ON CONFLICT (id) DO NOTHING;
END IF;
END $$;
SELECT setval(
pg_get_serial_sequence('locations', 'id'),
GREATEST(COALESCE((SELECT MAX(id) FROM locations), 1), 1)
);
-- Drop FKs referencing plants / warehouses
DO $$
DECLARE r RECORD;
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'plants') THEN
FOR r IN
SELECT c.conname, c.conrelid::regclass AS tbl
FROM pg_constraint c
WHERE c.contype = 'f' AND c.confrelid = 'public.plants'::regclass
LOOP
EXECUTE format('ALTER TABLE %s DROP CONSTRAINT IF EXISTS %I', r.tbl, r.conname);
END LOOP;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'warehouses') THEN
FOR r IN
SELECT c.conname, c.conrelid::regclass AS tbl
FROM pg_constraint c
WHERE c.contype = 'f' AND c.confrelid = 'public.warehouses'::regclass
LOOP
EXECUTE format('ALTER TABLE %s DROP CONSTRAINT IF EXISTS %I', r.tbl, r.conname);
END LOOP;
END IF;
END $$;
-- Point existing FK columns to locations (idempotent)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_plant_id_fkey') THEN
ALTER TABLE users ADD CONSTRAINT users_plant_id_fkey FOREIGN KEY (plant_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'assets_plant_id_fkey') THEN
ALTER TABLE assets ADD CONSTRAINT assets_plant_id_fkey FOREIGN KEY (plant_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'assets_warehouse_id_fkey') THEN
ALTER TABLE assets ADD CONSTRAINT assets_warehouse_id_fkey FOREIGN KEY (warehouse_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'purchase_orders_plant_id_fkey') THEN
ALTER TABLE purchase_orders ADD CONSTRAINT purchase_orders_plant_id_fkey FOREIGN KEY (plant_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'purchase_orders_warehouse_id_fkey') THEN
ALTER TABLE purchase_orders ADD CONSTRAINT purchase_orders_warehouse_id_fkey FOREIGN KEY (warehouse_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'grn_warehouse_id_fkey') THEN
ALTER TABLE grn ADD CONSTRAINT grn_warehouse_id_fkey FOREIGN KEY (warehouse_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'asset_transfers_from_plant_id_fkey') THEN
ALTER TABLE asset_transfers ADD CONSTRAINT asset_transfers_from_plant_id_fkey FOREIGN KEY (from_plant_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'asset_transfers_to_plant_id_fkey') THEN
ALTER TABLE asset_transfers ADD CONSTRAINT asset_transfers_to_plant_id_fkey FOREIGN KEY (to_plant_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'asset_transfers_from_warehouse_id_fkey') THEN
ALTER TABLE asset_transfers ADD CONSTRAINT asset_transfers_from_warehouse_id_fkey FOREIGN KEY (from_warehouse_id) REFERENCES locations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'asset_transfers_to_warehouse_id_fkey') THEN
ALTER TABLE asset_transfers ADD CONSTRAINT asset_transfers_to_warehouse_id_fkey FOREIGN KEY (to_warehouse_id) REFERENCES locations(id);
END IF;
END $$;
-- Drop views that depend on plants/warehouses, then drop legacy tables
DROP VIEW IF EXISTS v_purchase_orders CASCADE;
DROP VIEW IF EXISTS v_grn CASCADE;
DROP VIEW IF EXISTS v_assets CASCADE;
DROP VIEW IF EXISTS v_asset_expiry_alerts CASCADE;
DROP VIEW IF EXISTS v_asset_next_service CASCADE;
DROP TABLE IF EXISTS warehouses;
DROP TABLE IF EXISTS plants;
COMMIT;

View File

@ -0,0 +1,33 @@
-- Recreate PO/GRN views after locations merge
DROP VIEW IF EXISTS v_purchase_orders CASCADE;
DROP VIEW IF EXISTS v_grn CASCADE;
CREATE VIEW v_purchase_orders AS
SELECT
po.*,
pl.code AS plant_code,
pl.name AS plant_name,
wh.code AS warehouse_code,
wh.name AS warehouse_name,
v.vendor_code,
v.vendor_name
FROM purchase_orders po
JOIN locations pl ON pl.id = po.plant_id AND pl.type = 'plant'
LEFT JOIN locations wh ON wh.id = po.warehouse_id AND wh.type = 'warehouse'
JOIN vendors v ON v.id = po.vendor_id
WHERE po.deleted_at IS NULL;
CREATE VIEW v_grn AS
SELECT
g.*,
po.po_number,
v.vendor_code,
v.vendor_name,
wh.code AS warehouse_code,
wh.name AS warehouse_name
FROM grn g
JOIN purchase_orders po ON po.id = g.po_id
JOIN vendors v ON v.id = g.vendor_id
JOIN locations wh ON wh.id = g.warehouse_id AND wh.type = 'warehouse'
WHERE g.deleted_at IS NULL;

View File

@ -3,8 +3,7 @@ const ApiError = require('../../utils/ApiError');
const SOFT_DELETE_TABLES = new Set([ const SOFT_DELETE_TABLES = new Set([
'vendors', 'vendors',
'plants', 'locations',
'warehouses',
'departments', 'departments',
'users', 'users',
'purchase_orders', 'purchase_orders',

View File

@ -4,12 +4,12 @@ const auditLog = require('../../utils/auditLog');
const { getPagination } = require('../../utils/pagination'); const { getPagination } = require('../../utils/pagination');
const { nextDocumentNumber } = require('../../utils/generateCode'); const { nextDocumentNumber } = require('../../utils/generateCode');
const { DISPOSAL_STATUSES } = require('./assets.constants'); const { DISPOSAL_STATUSES } = require('./assets.constants');
const { assertPlant, assertWarehouse } = require('../../utils/locations');
const repository = require('./assets.repository'); const repository = require('./assets.repository');
const SOFT_DELETE_TABLES = new Set([ const SOFT_DELETE_TABLES = new Set([
'vendors', 'vendors',
'plants', 'locations',
'warehouses',
'departments', 'departments',
'users', 'users',
'purchase_orders', 'purchase_orders',
@ -20,9 +20,9 @@ const SOFT_DELETE_TABLES = new Set([
const assetInclude = { const assetInclude = {
asset_categories: { select: { id: true, code: true, name: true, code_prefix: true } }, asset_categories: { select: { id: true, code: true, name: true, code_prefix: true } },
plants: { select: { id: true, code: true, name: true } }, plant: { select: { id: true, code: true, name: true } },
departments: { select: { id: true, name: true } }, departments: { select: { id: true, name: true } },
warehouses: { select: { id: true, code: true, name: true } }, warehouse: { select: { id: true, code: true, name: true } },
users_assets_assigned_to_user_idTousers: { users_assets_assigned_to_user_idTousers: {
select: { id: true, full_name: true, employee_code: true }, select: { id: true, full_name: true, employee_code: true },
}, },
@ -38,18 +38,14 @@ const assetDetailInclude = {
}; };
const transferInclude = { const transferInclude = {
plants_asset_transfers_from_plant_idToplants: { select: { id: true, code: true, name: true } }, from_plant: { select: { id: true, code: true, name: true } },
plants_asset_transfers_to_plant_idToplants: { select: { id: true, code: true, name: true } }, to_plant: { select: { id: true, code: true, name: true } },
departments_asset_transfers_from_department_idTodepartments: { select: { id: true, name: true } }, departments_asset_transfers_from_department_idTodepartments: { select: { id: true, name: true } },
departments_asset_transfers_to_department_idTodepartments: { select: { id: true, name: true } }, departments_asset_transfers_to_department_idTodepartments: { select: { id: true, name: true } },
users_asset_transfers_from_user_idTousers: { select: { id: true, full_name: true } }, users_asset_transfers_from_user_idTousers: { select: { id: true, full_name: true } },
users_asset_transfers_to_user_idTousers: { select: { id: true, full_name: true } }, users_asset_transfers_to_user_idTousers: { select: { id: true, full_name: true } },
warehouses_asset_transfers_from_warehouse_idTowarehouses: { from_warehouse: { select: { id: true, code: true, name: true } },
select: { id: true, code: true, name: true }, to_warehouse: { select: { id: true, code: true, name: true } },
},
warehouses_asset_transfers_to_warehouse_idTowarehouses: {
select: { id: true, code: true, name: true },
},
users_asset_transfers_transferred_byTousers: { select: { id: true, full_name: true } }, users_asset_transfers_transferred_byTousers: { select: { id: true, full_name: true } },
}; };
@ -65,9 +61,9 @@ const sanitizeAsset = (asset) => {
if (!asset) return null; if (!asset) return null;
const { const {
asset_categories, asset_categories,
plants, plant,
departments, departments,
warehouses, warehouse,
users_assets_assigned_to_user_idTousers, users_assets_assigned_to_user_idTousers,
vendors_assets_vendor_idTovendors, vendors_assets_vendor_idTovendors,
purchase_orders, purchase_orders,
@ -80,9 +76,9 @@ const sanitizeAsset = (asset) => {
return { return {
...rest, ...rest,
asset_category: asset_categories || null, asset_category: asset_categories || null,
plant: plants || null, plant: plant || null,
department: departments || null, department: departments || null,
warehouse: warehouses || null, warehouse: warehouse || null,
assigned_to_user: users_assets_assigned_to_user_idTousers || null, assigned_to_user: users_assets_assigned_to_user_idTousers || null,
vendor: vendors_assets_vendor_idTovendors || null, vendor: vendors_assets_vendor_idTovendors || null,
purchase_order: purchase_orders || null, purchase_order: purchase_orders || null,
@ -90,9 +86,7 @@ const sanitizeAsset = (asset) => {
created_by_user: users_assets_created_byTousers || null, created_by_user: users_assets_created_byTousers || null,
updated_by_user: users_assets_updated_byTousers || null, updated_by_user: users_assets_updated_byTousers || null,
asset_categories: undefined, asset_categories: undefined,
plants: undefined,
departments: undefined, departments: undefined,
warehouses: undefined,
users_assets_assigned_to_user_idTousers: undefined, users_assets_assigned_to_user_idTousers: undefined,
vendors_assets_vendor_idTovendors: undefined, vendors_assets_vendor_idTovendors: undefined,
purchase_orders: undefined, purchase_orders: undefined,
@ -104,28 +98,28 @@ const sanitizeAsset = (asset) => {
const sanitizeTransfer = (row) => { const sanitizeTransfer = (row) => {
if (!row) return null; if (!row) return null;
const { const {
plants_asset_transfers_from_plant_idToplants, from_plant,
plants_asset_transfers_to_plant_idToplants, to_plant,
departments_asset_transfers_from_department_idTodepartments, departments_asset_transfers_from_department_idTodepartments,
departments_asset_transfers_to_department_idTodepartments, departments_asset_transfers_to_department_idTodepartments,
users_asset_transfers_from_user_idTousers, users_asset_transfers_from_user_idTousers,
users_asset_transfers_to_user_idTousers, users_asset_transfers_to_user_idTousers,
warehouses_asset_transfers_from_warehouse_idTowarehouses, from_warehouse,
warehouses_asset_transfers_to_warehouse_idTowarehouses, to_warehouse,
users_asset_transfers_transferred_byTousers, users_asset_transfers_transferred_byTousers,
...rest ...rest
} = row; } = row;
return { return {
...rest, ...rest,
from_plant: plants_asset_transfers_from_plant_idToplants || null, from_plant: from_plant || null,
to_plant: plants_asset_transfers_to_plant_idToplants || null, to_plant: to_plant || null,
from_department: departments_asset_transfers_from_department_idTodepartments || null, from_department: departments_asset_transfers_from_department_idTodepartments || null,
to_department: departments_asset_transfers_to_department_idTodepartments || null, to_department: departments_asset_transfers_to_department_idTodepartments || null,
from_user: users_asset_transfers_from_user_idTousers || null, from_user: users_asset_transfers_from_user_idTousers || null,
to_user: users_asset_transfers_to_user_idTousers || null, to_user: users_asset_transfers_to_user_idTousers || null,
from_warehouse: warehouses_asset_transfers_from_warehouse_idTowarehouses || null, from_warehouse: from_warehouse || null,
to_warehouse: warehouses_asset_transfers_to_warehouse_idTowarehouses || null, to_warehouse: to_warehouse || null,
transferred_by_user: users_asset_transfers_transferred_byTousers || null, transferred_by_user: users_asset_transfers_transferred_byTousers || null,
}; };
}; };
@ -155,11 +149,10 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
payload.asset_category_id, payload.asset_category_id,
'asset_category_id' 'asset_category_id'
); );
await assertReference('plants', payload.plant_id, 'plant_id'); await assertPlant(payload.plant_id, 'plant_id');
if (payload.department_id) if (payload.department_id)
await assertReference('departments', payload.department_id, 'department_id'); await assertReference('departments', payload.department_id, 'department_id');
if (payload.warehouse_id) if (payload.warehouse_id) await assertWarehouse(payload.warehouse_id, 'warehouse_id');
await assertReference('warehouses', payload.warehouse_id, 'warehouse_id');
if (payload.assigned_to_user_id) if (payload.assigned_to_user_id)
await assertReference('users', payload.assigned_to_user_id, 'assigned_to_user_id'); await assertReference('users', payload.assigned_to_user_id, 'assigned_to_user_id');
if (payload.vendor_id) await assertReference('vendors', payload.vendor_id, 'vendor_id'); if (payload.vendor_id) await assertReference('vendors', payload.vendor_id, 'vendor_id');
@ -402,14 +395,12 @@ const transferAsset = async (id, payload, userId, requestId) => {
throw new ApiError(409, 'Cannot transfer disposed or scrapped assets'); throw new ApiError(409, 'Cannot transfer disposed or scrapped assets');
} }
if (payload.to_plant_id) await assertReference('plants', payload.to_plant_id, 'to_plant_id'); if (payload.to_plant_id) await assertPlant(payload.to_plant_id, 'to_plant_id');
if (payload.to_department_id) { if (payload.to_department_id) {
await assertReference('departments', payload.to_department_id, 'to_department_id'); await assertReference('departments', payload.to_department_id, 'to_department_id');
} }
if (payload.to_user_id) await assertReference('users', payload.to_user_id, 'to_user_id'); if (payload.to_user_id) await assertReference('users', payload.to_user_id, 'to_user_id');
if (payload.to_warehouse_id) { if (payload.to_warehouse_id) await assertWarehouse(payload.to_warehouse_id, 'to_warehouse_id');
await assertReference('warehouses', payload.to_warehouse_id, 'to_warehouse_id');
}
const transfer = { const transfer = {
transfer_date: toDateOnly(payload.transfer_date), transfer_date: toDateOnly(payload.transfer_date),

View File

@ -110,7 +110,7 @@ const getMe = async (userId) => {
}, },
departments: { select: { id: true, name: true } }, departments: { select: { id: true, name: true } },
designations: { select: { id: true, name: true } }, designations: { select: { id: true, name: true } },
plants_users_plant_idToplants: { select: { id: true, code: true, name: true } }, plant_location: { select: { id: true, code: true, name: true } },
}, },
}); });
@ -138,7 +138,7 @@ const getMe = async (userId) => {
role: roles[0] || null, role: roles[0] || null,
department: user.departments, department: user.departments,
designation: user.designations, designation: user.designations,
plant: user.plants_users_plant_idToplants, plant: user.plant_location,
permissions, permissions,
}; };
}; };

View File

@ -5,11 +5,12 @@ const { getPagination } = require('../../utils/pagination');
const { nextDocumentNumber } = require('../../utils/generateCode'); const { nextDocumentNumber } = require('../../utils/generateCode');
const { buildSimplePdf } = require('../../utils/simplePdf'); const { buildSimplePdf } = require('../../utils/simplePdf');
const repository = require('./grn.repository'); const repository = require('./grn.repository');
const { assertWarehouse } = require('../../utils/locations');
const grnListInclude = { const grnListInclude = {
purchase_orders: { select: { id: true, po_number: true, status: true } }, purchase_orders: { select: { id: true, po_number: true, status: true } },
vendors: { select: { id: true, vendor_code: true, vendor_name: true } }, vendors: { select: { id: true, vendor_code: true, vendor_name: true } },
warehouses: { select: { id: true, code: true, name: true } }, warehouse: { select: { id: true, code: true, name: true } },
users_grn_received_byTousers: { select: { id: true, full_name: true } }, users_grn_received_byTousers: { select: { id: true, full_name: true } },
users_grn_created_byTousers: { select: { id: true, full_name: true } }, users_grn_created_byTousers: { select: { id: true, full_name: true } },
}; };
@ -46,7 +47,7 @@ const sanitizeGrn = (row) => {
const { const {
purchase_orders, purchase_orders,
vendors, vendors,
warehouses, warehouse,
users_grn_received_byTousers, users_grn_received_byTousers,
users_grn_created_byTousers, users_grn_created_byTousers,
users_grn_quality_checked_byTousers, users_grn_quality_checked_byTousers,
@ -60,7 +61,7 @@ const sanitizeGrn = (row) => {
...rest, ...rest,
purchase_order: purchase_orders || null, purchase_order: purchase_orders || null,
vendor: vendors || null, vendor: vendors || null,
warehouse: warehouses || null, warehouse: warehouse || null,
received_by_user: users_grn_received_byTousers || null, received_by_user: users_grn_received_byTousers || null,
created_by_user: users_grn_created_byTousers || null, created_by_user: users_grn_created_byTousers || null,
quality_checked_by_user: users_grn_quality_checked_byTousers || null, quality_checked_by_user: users_grn_quality_checked_byTousers || null,
@ -78,6 +79,7 @@ const sanitizeGrn = (row) => {
purchase_orders: undefined, purchase_orders: undefined,
vendors: undefined, vendors: undefined,
warehouses: undefined, warehouses: undefined,
warehouse: undefined,
users_grn_received_byTousers: undefined, users_grn_received_byTousers: undefined,
users_grn_created_byTousers: undefined, users_grn_created_byTousers: undefined,
users_grn_quality_checked_byTousers: undefined, users_grn_quality_checked_byTousers: undefined,
@ -242,10 +244,7 @@ const buildHeaderData = (payload, po, userId) => ({
const createGrn = async (payload, userId, requestId) => { const createGrn = async (payload, userId, requestId) => {
const po = await getReceivablePoOrThrow(payload.po_id); const po = await getReceivablePoOrThrow(payload.po_id);
const warehouse = await prisma.warehouses.findFirst({ await assertWarehouse(payload.warehouse_id, 'warehouse_id');
where: { id: BigInt(payload.warehouse_id), deleted_at: null, is_active: true },
});
if (!warehouse) throw new ApiError(422, 'Invalid warehouse_id');
const { builtItems, assetPlans } = await validateAndBuildItems(po, payload.items); const { builtItems, assetPlans } = await validateAndBuildItems(po, payload.items);
const header = buildHeaderData(payload, po, userId); const header = buildHeaderData(payload, po, userId);
@ -316,10 +315,7 @@ const updateGrn = async (id, payload, userId, requestId) => {
} }
if (payload.warehouse_id) { if (payload.warehouse_id) {
const warehouse = await prisma.warehouses.findFirst({ await assertWarehouse(payload.warehouse_id, 'warehouse_id');
where: { id: BigInt(payload.warehouse_id), deleted_at: null, is_active: true },
});
if (!warehouse) throw new ApiError(422, 'Invalid warehouse_id');
} }
const data = { const data = {

View File

@ -14,6 +14,7 @@ router.use('/designations', require('./designations/designations.routes'));
router.use('/document-series', require('./document-series/document-series.routes')); router.use('/document-series', require('./document-series/document-series.routes'));
router.use('/item-subcategories', require('./item-subcategories/item-subcategories.routes')); router.use('/item-subcategories', require('./item-subcategories/item-subcategories.routes'));
router.use('/items', require('./items/items.routes')); router.use('/items', require('./items/items.routes'));
router.use('/locations', require('./locations/locations.routes'));
router.use('/warehouses', require('./warehouses/warehouses.routes')); router.use('/warehouses', require('./warehouses/warehouses.routes'));
router.use('/plants', require('./plants/plants.routes')); router.use('/plants', require('./plants/plants.routes'));

View File

@ -0,0 +1,30 @@
const asyncHandler = require('../../../utils/asyncHandler');
const ApiResponse = require('../../../utils/ApiResponse');
const service = require('./locations.service');
const create = asyncHandler(async (req, res) => {
const data = await service.createLocation(req.body, req.user?.id, req.id);
res.status(201).json(new ApiResponse(201, data, 'Location created successfully'));
});
const list = asyncHandler(async (req, res) => {
const result = await service.listLocations(req.query);
res.json(new ApiResponse(200, result.data, 'Locations list fetched', result.meta));
});
const getOne = asyncHandler(async (req, res) => {
const data = await service.getLocationById(req.params.id);
res.json(new ApiResponse(200, data, 'Location fetched'));
});
const update = asyncHandler(async (req, res) => {
const data = await service.updateLocationById(req.params.id, req.body, req.user?.id, req.id);
res.json(new ApiResponse(200, data, 'Location updated successfully'));
});
const remove = asyncHandler(async (req, res) => {
await service.deleteLocationById(req.params.id, req.user?.id, req.id);
res.json(new ApiResponse(200, null, 'Location deleted successfully'));
});
module.exports = { create, list, getOne, update, remove };

View File

@ -0,0 +1,21 @@
const express = require('express');
const authenticate = require('../../../middlewares/auth.middleware');
const authorize = require('../../../middlewares/rbac.middleware');
const validate = require('../../../middlewares/validate.middleware');
const controller = require('./locations.controller');
const {
createLocationSchema,
updateLocationSchema,
listQuerySchema,
} = require('./locations.validation');
const router = express.Router();
router.use(authenticate);
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
router.post('/', authorize('MASTERS', 'create'), validate(createLocationSchema), controller.create);
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateLocationSchema), controller.update);
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
module.exports = router;

View File

@ -0,0 +1,274 @@
const prisma = require('../../../config/prisma');
const ApiError = require('../../../utils/ApiError');
const auditLog = require('../../../utils/auditLog');
const { getPagination } = require('../../../utils/pagination');
const {
LOCATION_TYPES,
assertPlant,
toPlantResponse,
toWarehouseResponse,
toLocationResponse,
} = require('../../../utils/locations');
const TABLE_NAME = 'locations';
const normalizeString = (value) => {
if (value === null || value === undefined) return value;
return String(value).trim();
};
const buildPlantData = (payload) => ({
type: LOCATION_TYPES.PLANT,
code: payload.code ? String(payload.code).trim().toUpperCase() : undefined,
name: normalizeString(payload.name),
gstin: normalizeString(payload.gstin),
address: normalizeString(payload.address),
city: normalizeString(payload.city),
state: normalizeString(payload.state),
pincode: normalizeString(payload.pincode),
phone: normalizeString(payload.phone),
is_active: payload.is_active,
parent_id: null,
location: null,
});
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,
address: null,
city: null,
state: null,
pincode: null,
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({
where: {
code,
deleted_at: null,
...(excludeId ? { id: { not: BigInt(excludeId) } } : {}),
},
});
if (existing) throw new ApiError(409, 'Location code already exists');
};
const createLocation = async (type, payload, userId, requestId) => {
const data =
type === LOCATION_TYPES.PLANT ? buildPlantData(payload) : buildWarehouseData(payload);
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 response =
type === LOCATION_TYPES.PLANT ? toPlantResponse(created) : toWarehouseResponse(created);
await auditLog({
tableName: TABLE_NAME,
recordId: created.id,
action: 'CREATE',
oldValue: null,
newValue: response,
userId,
requestId,
});
return response;
};
const listLocations = async (query, type = null) => {
const { page, limit, skip } = getPagination(query);
const where = {
deleted_at: null,
...(type ? { type } : {}),
...(query.parent_id ? { parent_id: BigInt(query.parent_id) } : {}),
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
...(query.search
? {
OR: [
{ code: { contains: query.search, mode: 'insensitive' } },
{ name: { contains: query.search, mode: 'insensitive' } },
],
}
: {}),
};
const [rows, total] = await Promise.all([
prisma.locations.findMany({
where,
include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined,
orderBy: { created_at: 'desc' },
skip,
take: limit,
}),
prisma.locations.count({ where }),
]);
const mapRow = (row) => {
if (type === LOCATION_TYPES.PLANT) return toPlantResponse(row);
if (type === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
return toLocationResponse(row);
};
return { data: rows.map(mapRow), meta: { page, limit, total } };
};
const getLocationById = async (id, type = null) => {
const row = await prisma.locations.findFirst({
where: {
id: BigInt(id),
deleted_at: null,
...(type ? { type } : {}),
},
include: warehouseInclude,
});
if (!row) throw new ApiError(404, type ? `${type} not found` : 'Location not found');
if (type === LOCATION_TYPES.PLANT) return toPlantResponse(row);
if (type === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
return toLocationResponse(row);
};
const updateLocation = async (id, type, payload, userId, requestId) => {
const existing = await prisma.locations.findFirst({
where: { id: BigInt(id), deleted_at: null, type },
include: warehouseInclude,
});
if (!existing) throw new ApiError(404, `${type} not found`);
const data =
type === LOCATION_TYPES.PLANT ? buildPlantData(payload) : buildWarehouseData(payload);
Object.keys(data).forEach((key) => {
if (data[key] === undefined) delete data[key];
});
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: BigInt(id) },
data,
include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined,
});
const oldValue =
type === LOCATION_TYPES.PLANT ? toPlantResponse(existing) : toWarehouseResponse(existing);
const newValue =
type === LOCATION_TYPES.PLANT ? toPlantResponse(updated) : toWarehouseResponse(updated);
await auditLog({
tableName: TABLE_NAME,
recordId: id,
action: 'UPDATE',
oldValue,
newValue,
userId,
requestId,
});
return newValue;
};
const deleteLocation = async (id, type, userId, requestId) => {
const existing = await prisma.locations.findFirst({
where: { id: BigInt(id), deleted_at: null, type },
include: warehouseInclude,
});
if (!existing) throw new ApiError(404, `${type} not found`);
await prisma.locations.update({
where: { id: BigInt(id) },
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null },
});
const oldValue =
type === LOCATION_TYPES.PLANT ? toPlantResponse(existing) : toWarehouseResponse(existing);
await auditLog({
tableName: TABLE_NAME,
recordId: id,
action: 'DELETE',
oldValue,
newValue: { deleted_at: new Date() },
userId,
requestId,
});
};
const deleteLocationById = async (id, userId, requestId) => {
const existing = await prisma.locations.findFirst({
where: { id: BigInt(id), deleted_at: null },
include: warehouseInclude,
});
if (!existing) throw new ApiError(404, 'Location not found');
return deleteLocation(id, existing.type, userId, requestId);
};
const updateLocationById = async (id, payload, userId, requestId) => {
const existing = await prisma.locations.findFirst({
where: { id: BigInt(id), deleted_at: null },
});
if (!existing) throw new ApiError(404, 'Location not found');
return updateLocation(id, existing.type, payload, userId, requestId);
};
module.exports = {
LOCATION_TYPES,
createPlant: (payload, userId, requestId) =>
createLocation(LOCATION_TYPES.PLANT, payload, userId, requestId),
listPlants: (query) => listLocations(query, LOCATION_TYPES.PLANT),
getPlantById: (id) => getLocationById(id, LOCATION_TYPES.PLANT),
updatePlant: (id, payload, userId, requestId) =>
updateLocation(id, LOCATION_TYPES.PLANT, payload, userId, requestId),
deletePlant: (id, userId, requestId) =>
deleteLocation(id, LOCATION_TYPES.PLANT, userId, requestId),
createWarehouse: (payload, userId, requestId) =>
createLocation(LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
listWarehouses: (query) => listLocations(query, LOCATION_TYPES.WAREHOUSE),
getWarehouseById: (id) => getLocationById(id, LOCATION_TYPES.WAREHOUSE),
updateWarehouse: (id, payload, userId, requestId) =>
updateLocation(id, LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
deleteWarehouse: (id, userId, requestId) =>
deleteLocation(id, LOCATION_TYPES.WAREHOUSE, userId, requestId),
createLocation: (payload, userId, requestId) => {
if (!payload.type) throw new ApiError(422, 'type is required');
if (![LOCATION_TYPES.PLANT, LOCATION_TYPES.WAREHOUSE].includes(payload.type)) {
throw new ApiError(422, 'type must be plant or warehouse');
}
return createLocation(payload.type, payload, userId, requestId);
},
listLocations: (query) => listLocations(query),
getLocationById: (id) => getLocationById(id),
updateLocationById,
deleteLocationById,
};

View File

@ -0,0 +1,94 @@
const Joi = require('joi');
const locationFields = {
code: Joi.string().max(30).optional(),
name: Joi.string().max(150).optional(),
gstin: Joi.string().max(15).allow(null, '').optional(),
address: Joi.string().allow(null, '').optional(),
city: Joi.string().max(100).allow(null, '').optional(),
state: Joi.string().max(100).allow(null, '').optional(),
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(),
};
const createPlantSchema = Joi.object({
code: Joi.string().max(30).required(),
name: Joi.string().max(150).required(),
gstin: locationFields.gstin,
address: locationFields.address,
city: locationFields.city,
state: locationFields.state,
pincode: locationFields.pincode,
phone: locationFields.phone,
is_active: Joi.boolean().default(true),
});
const updatePlantSchema = createPlantSchema.fork(['code', 'name'], (schema) => schema.optional()).min(1);
const createWarehouseSchema = Joi.object({
code: Joi.string().max(30).required(),
name: Joi.string().max(150).required(),
plant_id: Joi.number().integer().positive().required(),
location: locationFields.location,
is_active: Joi.boolean().default(true),
});
const updateWarehouseSchema = createWarehouseSchema
.fork(['code', 'name', 'plant_id'], (schema) => schema.optional())
.min(1);
const createLocationSchema = Joi.object({
type: Joi.string().valid('plant', 'warehouse').required(),
code: Joi.string().max(30).required(),
name: Joi.string().max(150).required(),
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,
state: locationFields.state,
pincode: locationFields.pincode,
phone: locationFields.phone,
location: locationFields.location,
is_active: Joi.boolean().default(true),
});
const updateLocationSchema = Joi.object({
code: Joi.string().max(30).optional(),
name: Joi.string().max(150).optional(),
parent_id: Joi.number().integer().positive().optional(),
gstin: locationFields.gstin,
address: locationFields.address,
city: locationFields.city,
state: locationFields.state,
pincode: locationFields.pincode,
phone: locationFields.phone,
location: locationFields.location,
is_active: Joi.boolean().optional(),
}).min(1);
const listQuerySchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(20),
search: Joi.string().allow('').optional(),
is_active: Joi.boolean().optional(),
type: Joi.string().valid('plant', 'warehouse').optional(),
parent_id: Joi.number().integer().positive().optional(),
});
module.exports = {
createPlantSchema,
updatePlantSchema,
createWarehouseSchema,
updateWarehouseSchema,
createLocationSchema,
updateLocationSchema,
listQuerySchema,
};

View File

@ -1,86 +1,9 @@
const { buildMasterService } = require('../_shared/master.factory'); const service = require('../locations/locations.service');
const config = {
modelName: 'plants',
tableName: 'plants',
uniqueField: 'code',
softDelete: true,
fields: [
{
name: 'code',
type: 'string',
uppercase: true,
searchable: true,
},
{
name: 'name',
type: 'string',
uppercase: false,
searchable: true,
},
{
name: 'gstin',
type: 'string',
uppercase: false,
searchable: false,
},
{
name: 'address',
type: 'string',
uppercase: false,
searchable: false,
},
{
name: 'city',
type: 'string',
uppercase: false,
searchable: false,
},
{
name: 'state',
type: 'string',
uppercase: false,
searchable: false,
},
{
name: 'pincode',
type: 'string',
uppercase: false,
searchable: false,
},
{
name: 'phone',
type: 'string',
uppercase: false,
searchable: false,
},
{
name: 'is_active',
type: 'boolean',
uppercase: false,
searchable: false,
},
{
name: 'created_by',
type: 'int',
uppercase: false,
searchable: false,
},
{
name: 'updated_by',
type: 'int',
uppercase: false,
searchable: false,
},
],
};
const service = buildMasterService(config);
module.exports = { module.exports = {
createPlants: service.createOne, createPlants: service.createPlant,
listPlants: service.list, listPlants: service.listPlants,
getPlantsById: service.getOne, getPlantsById: service.getPlantById,
updatePlants: service.updateOne, updatePlants: service.updatePlant,
deletePlants: service.removeOne, deletePlants: service.deletePlant,
}; };

View File

@ -1,62 +1,9 @@
const { buildMasterService } = require('../_shared/master.factory'); const service = require('../locations/locations.service');
const config = {
modelName: 'warehouses',
tableName: 'warehouses',
uniqueField: 'code',
softDelete: true,
fields: [
{
name: 'code',
type: 'string',
uppercase: true,
searchable: true,
},
{
name: 'name',
type: 'string',
uppercase: false,
searchable: true,
},
{
name: 'plant_id',
type: 'int',
uppercase: false,
searchable: false,
},
{
name: 'location',
type: 'string',
uppercase: false,
searchable: false,
},
{
name: 'is_active',
type: 'boolean',
uppercase: false,
searchable: false,
},
{
name: 'created_by',
type: 'int',
uppercase: false,
searchable: false,
},
{
name: 'updated_by',
type: 'int',
uppercase: false,
searchable: false,
},
],
};
const service = buildMasterService(config);
module.exports = { module.exports = {
createWarehouses: service.createOne, createWarehouses: service.createWarehouse,
listWarehouses: service.list, listWarehouses: service.listWarehouses,
getWarehousesById: service.getOne, getWarehousesById: service.getWarehouseById,
updateWarehouses: service.updateOne, updateWarehouses: service.updateWarehouse,
deleteWarehouses: service.removeOne, deleteWarehouses: service.deleteWarehouse,
}; };

View File

@ -14,11 +14,12 @@ const {
} = require('./purchase-orders.constants'); } = require('./purchase-orders.constants');
const { computeLineAmounts, computeHeaderTotals } = require('./purchase-orders.calculations'); const { computeLineAmounts, computeHeaderTotals } = require('./purchase-orders.calculations');
const repository = require('./purchase-orders.repository'); const repository = require('./purchase-orders.repository');
const { assertPlant, assertWarehouse } = require('../../utils/locations');
const poListInclude = { const poListInclude = {
vendors: { select: { id: true, vendor_code: true, vendor_name: true } }, vendors: { select: { id: true, vendor_code: true, vendor_name: true } },
plants: { select: { id: true, code: true, name: true } }, plant: { select: { id: true, code: true, name: true } },
warehouses: { select: { id: true, code: true, name: true } }, warehouse: { select: { id: true, code: true, name: true } },
brands: { select: { id: true, code: true, name: true } }, brands: { select: { id: true, code: true, name: true } },
users_purchase_orders_created_byTousers: { select: { id: true, full_name: true } }, users_purchase_orders_created_byTousers: { select: { id: true, full_name: true } },
}; };
@ -57,8 +58,8 @@ const sanitizePo = (po) => {
if (!po) return null; if (!po) return null;
const { const {
vendors, vendors,
plants, plant,
warehouses, warehouse,
brands, brands,
payment_terms, payment_terms,
delivery_terms, delivery_terms,
@ -73,8 +74,8 @@ const sanitizePo = (po) => {
return { return {
...rest, ...rest,
vendor: vendors || null, vendor: vendors || null,
plant: plants || null, plant: plant || null,
warehouse: warehouses || null, warehouse: warehouse || null,
brand: brands || null, brand: brands || null,
payment_term: payment_terms || null, payment_term: payment_terms || null,
delivery_term: delivery_terms || null, delivery_term: delivery_terms || null,
@ -102,7 +103,9 @@ const sanitizePo = (po) => {
po_approvals: undefined, po_approvals: undefined,
vendors: undefined, vendors: undefined,
plants: undefined, plants: undefined,
plant: undefined,
warehouses: undefined, warehouses: undefined,
warehouse: undefined,
brands: undefined, brands: undefined,
payment_terms: undefined, payment_terms: undefined,
delivery_terms: undefined, delivery_terms: undefined,
@ -120,8 +123,7 @@ const assertStatus = (po, allowedStatuses, action) => {
const SOFT_DELETE_TABLES = new Set([ const SOFT_DELETE_TABLES = new Set([
'vendors', 'vendors',
'plants', 'locations',
'warehouses',
'payment_terms', 'payment_terms',
'delivery_terms', 'delivery_terms',
'items', 'items',
@ -206,9 +208,8 @@ const validateAndBuildItems = async (items) => {
const buildHeaderData = async (payload, builtItems, userId) => { const buildHeaderData = async (payload, builtItems, userId) => {
await assertReference('vendors', payload.vendor_id, 'vendor_id'); await assertReference('vendors', payload.vendor_id, 'vendor_id');
await assertReference('plants', payload.plant_id, 'plant_id'); await assertPlant(payload.plant_id, 'plant_id');
if (payload.warehouse_id) if (payload.warehouse_id) await assertWarehouse(payload.warehouse_id, 'warehouse_id');
await assertReference('warehouses', payload.warehouse_id, 'warehouse_id');
if (payload.brand_id) if (payload.brand_id)
await assertReference('brands', payload.brand_id, 'brand_id', { requireActive: false }); await assertReference('brands', payload.brand_id, 'brand_id', { requireActive: false });
if (payload.payment_term_id) if (payload.payment_term_id)

View File

@ -5,6 +5,7 @@ const ApiError = require('../../utils/ApiError');
const auditLog = require('../../utils/auditLog'); const auditLog = require('../../utils/auditLog');
const { getPagination } = require('../../utils/pagination'); const { getPagination } = require('../../utils/pagination');
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption'); const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
const { assertPlant } = require('../../utils/locations');
const { extractUserRoles } = require('../../utils/userPermissions'); const { extractUserRoles } = require('../../utils/userPermissions');
const userInclude = { const userInclude = {
@ -13,7 +14,7 @@ const userInclude = {
}, },
departments: { select: { id: true, name: true } }, departments: { select: { id: true, name: true } },
designations: { select: { id: true, name: true } }, designations: { select: { id: true, name: true } },
plants_users_plant_idToplants: { select: { id: true, code: true, name: true } }, plant_location: { select: { id: true, code: true, name: true } },
users_users_reporting_toTousers: { select: { id: true, full_name: true, employee_code: true } }, users_users_reporting_toTousers: { select: { id: true, full_name: true, employee_code: true } },
}; };
@ -26,7 +27,7 @@ const sanitizeUser = (user) => {
if (!user) return null; if (!user) return null;
const { const {
user_roles, user_roles,
plants_users_plant_idToplants, plant_location,
users_users_reporting_toTousers, users_users_reporting_toTousers,
departments, departments,
designations, designations,
@ -44,7 +45,7 @@ const sanitizeUser = (user) => {
role: roles[0] || null, role: roles[0] || null,
department: departments || null, department: departments || null,
designation: designations || null, designation: designations || null,
plant: plants_users_plant_idToplants || null, plant: plant_location || null,
reporting_manager: users_users_reporting_toTousers || null, reporting_manager: users_users_reporting_toTousers || null,
}; };
}; };
@ -129,10 +130,7 @@ const assertFk = async (payload) => {
if (!row) throw new ApiError(422, 'Invalid designation_id'); if (!row) throw new ApiError(422, 'Invalid designation_id');
} }
if (payload.plant_id) { if (payload.plant_id) {
const row = await prisma.plants.findFirst({ await assertPlant(payload.plant_id, 'plant_id');
where: { id: BigInt(payload.plant_id), deleted_at: null },
});
if (!row) throw new ApiError(422, 'Invalid plant_id');
} }
if (payload.reporting_to) { if (payload.reporting_to) {
const row = await prisma.users.findFirst({ const row = await prisma.users.findFirst({

96
src/utils/locations.js Normal file
View File

@ -0,0 +1,96 @@
const prisma = require('../config/prisma');
const ApiError = require('./ApiError');
const LOCATION_TYPES = Object.freeze({
PLANT: 'plant',
WAREHOUSE: 'warehouse',
});
const assertLocation = async (id, expectedType, label, { requireActive = true } = {}) => {
if (!id) return null;
const row = await prisma.locations.findFirst({
where: {
id: BigInt(id),
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}`);
if (requireActive && !row.is_active) throw new ApiError(422, `${label} is inactive`);
return row;
};
const assertPlant = (id, label = 'plant_id') =>
assertLocation(id, LOCATION_TYPES.PLANT, label);
const assertWarehouse = (id, label = 'warehouse_id') =>
assertLocation(id, LOCATION_TYPES.WAREHOUSE, label);
const locationListSelect = { id: true, code: true, name: true, type: true };
const plantListSelect = { id: true, code: true, name: true };
const warehouseListSelect = {
id: true,
code: true,
name: true,
parent_id: true,
location: true,
parent: { select: plantListSelect },
};
const toPlantResponse = (row) => ({
id: row.id,
code: row.code,
name: row.name,
gstin: row.gstin,
address: row.address,
city: row.city,
state: row.state,
pincode: row.pincode,
phone: row.phone,
is_active: row.is_active,
created_by: row.created_by,
updated_by: row.updated_by,
created_at: row.created_at,
updated_at: row.updated_at,
deleted_at: row.deleted_at,
});
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,
updated_by: row.updated_by,
created_at: row.created_at,
updated_at: row.updated_at,
deleted_at: row.deleted_at,
});
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 };
};
module.exports = {
LOCATION_TYPES,
assertLocation,
assertPlant,
assertWarehouse,
locationListSelect,
plantListSelect,
warehouseListSelect,
toPlantResponse,
toWarehouseResponse,
toLocationResponse,
};