F5.1: jerarquía manual Departamento a Subinstalación

This commit is contained in:
2026-09-09 07:48:59 -03:00
parent 0b0484c5fe
commit d7557ef600
@@ -38,8 +38,10 @@ type ParentRow = {
typeCode: string; typeCode: string;
inventoryFamilyId: string | null; inventoryFamilyId: string | null;
}; };
type IdRow = { id: string };
const TYPE_CODE_BY_KIND: Record<Exclude<InventoryStructureKind, 'EMPRESA'>, string> = { const TYPE_CODE_BY_KIND: Record<Exclude<InventoryStructureKind, 'EMPRESA'>, string> = {
DEPARTAMENTO: 'departamento',
AREA: 'area', AREA: 'area',
YACIMIENTO: 'yacimiento', YACIMIENTO: 'yacimiento',
INSTALACION: 'instalacion', INSTALACION: 'instalacion',
@@ -47,7 +49,8 @@ const TYPE_CODE_BY_KIND: Record<Exclude<InventoryStructureKind, 'EMPRESA'>, stri
}; };
const PARENT_TYPE_BY_KIND: Record<InventoryStructureKind, string | null> = { const PARENT_TYPE_BY_KIND: Record<InventoryStructureKind, string | null> = {
EMPRESA: null, EMPRESA: null,
AREA: null, DEPARTAMENTO: null,
AREA: 'departamento',
YACIMIENTO: 'area', YACIMIENTO: 'area',
INSTALACION: 'yacimiento', INSTALACION: 'yacimiento',
SUBINSTALACION: 'instalacion', SUBINSTALACION: 'instalacion',
@@ -70,22 +73,24 @@ export class InventoryStructureService {
SELECT id,code,name SELECT id,code,name
FROM asset_types FROM asset_types
WHERE ( WHERE (
lower(code) IN ('area','yacimiento','instalacion','subinstalacion') lower(code) IN ('departamento','area','yacimiento','instalacion','subinstalacion')
OR operational_role='COMPANY' OR operational_role='COMPANY'
) AND is_active=true ) AND is_active=true
ORDER BY CASE ORDER BY CASE
WHEN operational_role='COMPANY' THEN 0 WHEN operational_role='COMPANY' THEN 0
WHEN lower(code)='area' THEN 1 WHEN lower(code)='departamento' THEN 1
WHEN lower(code)='yacimiento' THEN 2 WHEN lower(code)='area' THEN 2
WHEN lower(code)='instalacion' THEN 3 WHEN lower(code)='yacimiento' THEN 3
WHEN lower(code)='subinstalacion' THEN 4 ELSE 9 END WHEN lower(code)='instalacion' THEN 4
WHEN lower(code)='subinstalacion' THEN 5 ELSE 9 END
`)) as StructureTypeRow[]; `)) as StructureTypeRow[];
const company = types.find((item) => ['empresa','organizacion'].includes(item.code.toLowerCase())); const company = types.find((item) => ['empresa','organizacion'].includes(item.code.toLowerCase()));
const departamento = types.find((item) => item.code.toLowerCase()==='departamento');
const area = types.find((item) => item.code.toLowerCase()==='area'); const area = types.find((item) => item.code.toLowerCase()==='area');
const yacimiento = types.find((item) => item.code.toLowerCase()==='yacimiento'); const yacimiento = types.find((item) => item.code.toLowerCase()==='yacimiento');
const instalacion = types.find((item) => item.code.toLowerCase()==='instalacion'); const instalacion = types.find((item) => item.code.toLowerCase()==='instalacion');
const subinstalacion = types.find((item) => item.code.toLowerCase()==='subinstalacion'); const subinstalacion = types.find((item) => item.code.toLowerCase()==='subinstalacion');
if (!company || !area || !yacimiento || !instalacion || !subinstalacion) { if (!company || !departamento || !area || !yacimiento || !instalacion || !subinstalacion) {
throw new ConflictException({ throw new ConflictException({
code: 'INVENTORY_STRUCTURE_TYPES_INCOMPLETE', code: 'INVENTORY_STRUCTURE_TYPES_INCOMPLETE',
message: 'La configuración maestra de Empresa e Inventario todavía no está completa', message: 'La configuración maestra de Empresa e Inventario todavía no está completa',
@@ -109,7 +114,8 @@ export class InventoryStructureService {
{ kind: 'EMPRESA', label: 'Empresa', type: company, parentKind: null, requiresFamily: false }, { kind: 'EMPRESA', label: 'Empresa', type: company, parentKind: null, requiresFamily: false },
], ],
levels: [ levels: [
{ kind: 'AREA', label: 'Área', type: area, parentKind: null, requiresFamily: false }, { kind: 'DEPARTAMENTO', label: 'Departamento', type: departamento, parentKind: null, requiresFamily: false },
{ kind: 'AREA', label: 'Área', type: area, parentKind: 'DEPARTAMENTO', requiresFamily: false },
{ kind: 'YACIMIENTO', label: 'Yacimiento', type: yacimiento, parentKind: 'AREA', requiresFamily: false }, { kind: 'YACIMIENTO', label: 'Yacimiento', type: yacimiento, parentKind: 'AREA', requiresFamily: false },
{ kind: 'INSTALACION', label: 'Instalación', type: instalacion, parentKind: 'YACIMIENTO', requiresFamily: true }, { kind: 'INSTALACION', label: 'Instalación', type: instalacion, parentKind: 'YACIMIENTO', requiresFamily: true },
{ kind: 'SUBINSTALACION', label: 'Subinstalación', type: subinstalacion, parentKind: 'INSTALACION', requiresFamily: true }, { kind: 'SUBINSTALACION', label: 'Subinstalación', type: subinstalacion, parentKind: 'INSTALACION', requiresFamily: true },
@@ -121,7 +127,7 @@ export class InventoryStructureService {
async parents(kindValue: string, search?: string) { async parents(kindValue: string, search?: string) {
const kind = kindValue.toUpperCase() as InventoryStructureKind; const kind = kindValue.toUpperCase() as InventoryStructureKind;
if (!(kind in PARENT_TYPE_BY_KIND) || kind === 'AREA' || kind === 'EMPRESA') { if (!(kind in PARENT_TYPE_BY_KIND) || kind === 'DEPARTAMENTO' || kind === 'EMPRESA') {
throw new BadRequestException({ throw new BadRequestException({
code: 'INVENTORY_STRUCTURE_PARENT_KIND_INVALID', code: 'INVENTORY_STRUCTURE_PARENT_KIND_INVALID',
message: 'El nivel indicado no requiere un registro padre', message: 'El nivel indicado no requiere un registro padre',
@@ -153,7 +159,7 @@ export class InventoryStructureService {
AND asset.information_status<>'INACTIVE' AND asset.information_status<>'INACTIVE'
${searchSql} ${searchSql}
ORDER BY asset.name,asset.code ORDER BY asset.name,asset.code
LIMIT 80 LIMIT 100
`, parameters); `, parameters);
return { data: rows }; return { data: rows };
} }
@@ -169,7 +175,9 @@ export class InventoryStructureService {
const parent = await this.requireParent(manager, dto.kind, dto.parentId ?? null); const parent = await this.requireParent(manager, dto.kind, dto.parentId ?? null);
const family = await this.requireFamily(manager, dto.kind, dto.familyId ?? null, parent); const family = await this.requireFamily(manager, dto.kind, dto.familyId ?? null, parent);
const generatedCode = dto.code?.trim().toUpperCase() || this.generatedCode(dto.kind, dto.name); const generatedCode = dto.code?.trim().toUpperCase() || this.generatedCode(dto.kind, dto.name);
const operationalAreaId = parent ? await this.resolveAreaId(manager, parent) : null; const operationalAreaId = parent && ['YACIMIENTO','INSTALACION','SUBINSTALACION'].includes(dto.kind)
? await this.resolveAreaId(manager, parent)
: null;
const inserted = (await manager.query(` const inserted = (await manager.query(`
INSERT INTO assets ( INSERT INTO assets (
@@ -193,9 +201,9 @@ export class InventoryStructureService {
AssetInformationStatus.DRAFT, AssetInformationStatus.DRAFT,
AssetOperationalStatus.UNKNOWN, AssetOperationalStatus.UNKNOWN,
AssetDataOrigin.MANUAL, AssetDataOrigin.MANUAL,
dto.kind === 'EMPRESA' ? 'Maestro de Empresas F5' : 'Estructura de Inventario F5', dto.kind === 'EMPRESA' ? 'Maestro manual de Empresas F5.1' : 'Estructura manual de Inventario F5.1',
dto.kind === 'EMPRESA' ? 'inventory-master:empresa' : `inventory-structure:${dto.kind.toLowerCase()}`, dto.kind === 'EMPRESA' ? 'inventory-master:empresa' : `inventory-structure:${dto.kind.toLowerCase()}`,
family ? `Familia técnica: ${family.code} · ${family.name}` : null, family ? `Clasificación técnica: ${family.code} · ${family.name}` : null,
principal.userId, principal.userId,
])) as Array<{ id: string }>; ])) as Array<{ id: string }>;
const id = inserted[0]?.id; const id = inserted[0]?.id;
@@ -225,7 +233,7 @@ export class InventoryStructureService {
id, id,
parent?.id ?? null, parent?.id ?? null,
operationalAreaId, operationalAreaId,
dto.kind === 'EMPRESA' ? 'Alta guiada de Empresa independiente F5' : 'Alta guiada de estructura de Inventario F5', dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F5.1' : 'Alta manual de estructura de Inventario F5.1',
versionNumber, versionNumber,
request.requestId, request.requestId,
principal.userId, principal.userId,
@@ -285,7 +293,7 @@ export class InventoryStructureService {
code: 'INVENTORY_ROOT_MUST_NOT_HAVE_PARENT', code: 'INVENTORY_ROOT_MUST_NOT_HAVE_PARENT',
message: kind === 'EMPRESA' message: kind === 'EMPRESA'
? 'Una Empresa es un maestro independiente y no puede tener padre' ? 'Una Empresa es un maestro independiente y no puede tener padre'
: 'Un Área es un registro raíz y no puede tener padre', : 'Un Departamento es un registro raíz y no puede tener padre',
}); });
} }
return null; return null;
@@ -309,7 +317,7 @@ export class InventoryStructureService {
if (parent.typeCode.toLowerCase() !== expectedType) { if (parent.typeCode.toLowerCase() !== expectedType) {
throw new BadRequestException({ throw new BadRequestException({
code: 'INVENTORY_STRUCTURE_PARENT_INVALID', code: 'INVENTORY_STRUCTURE_PARENT_INVALID',
message: 'La jerarquía requerida es Área → Yacimiento → Instalación → Subinstalación', message: 'La jerarquía requerida es Departamento → Área → Yacimiento → Instalación → Subinstalación',
}); });
} }
return parent; return parent;
@@ -349,13 +357,13 @@ export class InventoryStructureService {
if (!expectedLevel) { if (!expectedLevel) {
if (familyId) throw new BadRequestException({ if (familyId) throw new BadRequestException({
code: 'INVENTORY_STRUCTURE_FAMILY_NOT_ALLOWED', code: 'INVENTORY_STRUCTURE_FAMILY_NOT_ALLOWED',
message: 'Empresa, Área y Yacimiento no llevan familia técnica', message: 'Empresa, Departamento, Área y Yacimiento no llevan clasificación técnica',
}); });
return null; return null;
} }
if (!familyId) throw new BadRequestException({ if (!familyId) throw new BadRequestException({
code: 'INVENTORY_STRUCTURE_FAMILY_REQUIRED', code: 'INVENTORY_STRUCTURE_FAMILY_REQUIRED',
message: `Elegí la familia técnica de la ${kind.toLowerCase()}`, message: `Elegí la clasificación técnica de la ${kind.toLowerCase()}`,
}); });
const rows = (await manager.query(` const rows = (await manager.query(`
SELECT family.id,family.code,family.name,family.level, SELECT family.id,family.code,family.name,family.level,
@@ -369,22 +377,27 @@ export class InventoryStructureService {
LIMIT 1 LIMIT 1
`, [familyId])) as FamilyRow[]; `, [familyId])) as FamilyRow[];
const family = rows[0]; const family = rows[0];
if (!family) throw new NotFoundException({ code: 'INVENTORY_FAMILY_NOT_FOUND', message: 'La familia técnica no existe' }); if (!family) throw new NotFoundException({ code: 'INVENTORY_FAMILY_NOT_FOUND', message: 'La clasificación técnica no existe' });
if (family.level !== expectedLevel) throw new BadRequestException({ if (family.level !== expectedLevel) throw new BadRequestException({
code: 'INVENTORY_FAMILY_LEVEL_INVALID', code: 'INVENTORY_FAMILY_LEVEL_INVALID',
message: 'La familia técnica no corresponde al nivel seleccionado', message: 'La clasificación técnica no corresponde al nivel seleccionado',
}); });
if (kind === 'SUBINSTALACION' && family.parentFamilyId !== parent?.inventoryFamilyId) { if (kind === 'SUBINSTALACION' && family.parentFamilyId !== parent?.inventoryFamilyId) {
throw new BadRequestException({ throw new BadRequestException({
code: 'INVENTORY_SUBINSTALLATION_FAMILY_PARENT_INVALID', code: 'INVENTORY_SUBINSTALLATION_FAMILY_PARENT_INVALID',
message: 'La Subinstalación elegida no pertenece a la familia de la Instalación seleccionada', message: 'La Subinstalación elegida no pertenece a la clasificación de la Instalación seleccionada',
}); });
} }
return family; return family;
} }
private generatedCode(kind: InventoryStructureKind, name: string): string { private generatedCode(kind: InventoryStructureKind, name: string): string {
const prefix = kind === 'EMPRESA' ? 'EMP' : kind === 'YACIMIENTO' ? 'YAC' : kind === 'INSTALACION' ? 'INST' : kind === 'SUBINSTALACION' ? 'SUB' : 'AREA'; const prefix = kind === 'EMPRESA' ? 'EMP'
: kind === 'DEPARTAMENTO' ? 'DEP'
: kind === 'AREA' ? 'AREA'
: kind === 'YACIMIENTO' ? 'YAC'
: kind === 'INSTALACION' ? 'INST'
: 'SUB';
const readable = name const readable = name
.normalize('NFD') .normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') .replace(/[\u0300-\u036f]/g, '')
@@ -417,5 +430,3 @@ export class InventoryStructureService {
return rows[0]; return rows[0];
} }
} }
type IdRow = { id: string };