feat(f6): aplicar compatibilidad múltiple al alta de Inventarios
This commit is contained in:
@@ -20,6 +20,7 @@ import type { CreateInventoryStructureDto, InventoryStructureKind } from './dto/
|
|||||||
import { AssetHistoryService } from './asset-history.service';
|
import { AssetHistoryService } from './asset-history.service';
|
||||||
|
|
||||||
type StructureTypeRow = { id: string; code: string; name: string };
|
type StructureTypeRow = { id: string; code: string; name: string };
|
||||||
|
type FamilyParent = { id: string; code: string; name: string };
|
||||||
type FamilyRow = {
|
type FamilyRow = {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -27,9 +28,8 @@ type FamilyRow = {
|
|||||||
level: 'INSTALLATION' | 'SUBINSTALLATION';
|
level: 'INSTALLATION' | 'SUBINSTALLATION';
|
||||||
legacyTypeCode: string | null;
|
legacyTypeCode: string | null;
|
||||||
informationLabels: string[];
|
informationLabels: string[];
|
||||||
parentFamilyId: string | null;
|
parentFamilyIds: string[];
|
||||||
parentFamilyCode: string | null;
|
parentFamilies: FamilyParent[];
|
||||||
parentFamilyName: string | null;
|
|
||||||
};
|
};
|
||||||
type ParentRow = {
|
type ParentRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -101,10 +101,15 @@ export class InventoryStructureService {
|
|||||||
SELECT family.id,family.code,family.name,family.level,
|
SELECT family.id,family.code,family.name,family.level,
|
||||||
family.legacy_type_code AS "legacyTypeCode",
|
family.legacy_type_code AS "legacyTypeCode",
|
||||||
family.information_labels AS "informationLabels",
|
family.information_labels AS "informationLabels",
|
||||||
parent.id AS "parentFamilyId",parent.code AS "parentFamilyCode",parent.name AS "parentFamilyName"
|
COALESCE((SELECT JSONB_AGG(rule.parent_family_id ORDER BY parent.name,parent.code)
|
||||||
|
FROM inventory_family_parent_rules rule
|
||||||
|
JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
||||||
|
WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilyIds",
|
||||||
|
COALESCE((SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) ORDER BY parent.name,parent.code)
|
||||||
|
FROM inventory_family_parent_rules rule
|
||||||
|
JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
||||||
|
WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilies"
|
||||||
FROM inventory_families family
|
FROM inventory_families family
|
||||||
LEFT JOIN inventory_family_parent_rules rule ON rule.child_family_id=family.id
|
|
||||||
LEFT JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
|
||||||
WHERE family.is_active=true
|
WHERE family.is_active=true
|
||||||
ORDER BY family.level,family.name,family.code
|
ORDER BY family.level,family.name,family.code
|
||||||
`)) as FamilyRow[];
|
`)) as FamilyRow[];
|
||||||
@@ -201,7 +206,7 @@ export class InventoryStructureService {
|
|||||||
AssetInformationStatus.DRAFT,
|
AssetInformationStatus.DRAFT,
|
||||||
AssetOperationalStatus.UNKNOWN,
|
AssetOperationalStatus.UNKNOWN,
|
||||||
AssetDataOrigin.MANUAL,
|
AssetDataOrigin.MANUAL,
|
||||||
dto.kind === 'EMPRESA' ? 'Maestro manual de Empresas F5.1' : 'Estructura manual de Inventario F5.1',
|
dto.kind === 'EMPRESA' ? 'Maestro manual de Empresas F6' : 'Estructura manual de Inventario F6',
|
||||||
dto.kind === 'EMPRESA' ? 'inventory-master:empresa' : `inventory-structure:${dto.kind.toLowerCase()}`,
|
dto.kind === 'EMPRESA' ? 'inventory-master:empresa' : `inventory-structure:${dto.kind.toLowerCase()}`,
|
||||||
family ? `Clasificación técnica: ${family.code} · ${family.name}` : null,
|
family ? `Clasificación técnica: ${family.code} · ${family.name}` : null,
|
||||||
principal.userId,
|
principal.userId,
|
||||||
@@ -218,11 +223,7 @@ export class InventoryStructureService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const versionNumber = await this.history.capture(
|
const versionNumber = await this.history.capture(
|
||||||
manager,
|
manager,id,AssetVersionChangeType.CREATED,principal,request,
|
||||||
id,
|
|
||||||
AssetVersionChangeType.CREATED,
|
|
||||||
principal,
|
|
||||||
request,
|
|
||||||
);
|
);
|
||||||
await manager.query(`
|
await manager.query(`
|
||||||
INSERT INTO asset_context_history (
|
INSERT INTO asset_context_history (
|
||||||
@@ -230,38 +231,28 @@ export class InventoryStructureService {
|
|||||||
change_reason,asset_version_number,source,request_id,created_by
|
change_reason,asset_version_number,source,request_id,created_by
|
||||||
) VALUES ($1,$2,$3,NULL,CURRENT_TIMESTAMP,$4,$5,'WEB',$6,$7)
|
) VALUES ($1,$2,$3,NULL,CURRENT_TIMESTAMP,$4,$5,'WEB',$6,$7)
|
||||||
`, [
|
`, [
|
||||||
id,
|
id,parent?.id ?? null,operationalAreaId,
|
||||||
parent?.id ?? null,
|
dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F6' : 'Alta manual de estructura de Inventario F6',
|
||||||
operationalAreaId,
|
versionNumber,request.requestId,principal.userId,
|
||||||
dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F5.1' : 'Alta manual de estructura de Inventario F5.1',
|
|
||||||
versionNumber,
|
|
||||||
request.requestId,
|
|
||||||
principal.userId,
|
|
||||||
]);
|
]);
|
||||||
const created = await this.loadView(manager, id);
|
const created = await this.loadView(manager, id);
|
||||||
await this.audit.record({
|
await this.audit.record({
|
||||||
...administrationAuditContext(principal, request),
|
...administrationAuditContext(principal, request),
|
||||||
action: AuditAction.ASSET_CREATED,
|
action: AuditAction.ASSET_CREATED,
|
||||||
entityType: 'asset',
|
entityType: 'asset',entityId: id,
|
||||||
entityId: id,
|
|
||||||
afterData: created as unknown as Record<string, unknown>,
|
afterData: created as unknown as Record<string, unknown>,
|
||||||
metadata: {
|
metadata: {
|
||||||
versionNumber,
|
versionNumber,inventoryStructureKind: dto.kind,
|
||||||
inventoryStructureKind: dto.kind,
|
inventoryFamilyId: family?.id ?? null,inventoryFamilyCode: family?.code ?? null,
|
||||||
inventoryFamilyId: family?.id ?? null,
|
|
||||||
inventoryFamilyCode: family?.code ?? null,
|
|
||||||
operatorOwnership: false,
|
operatorOwnership: false,
|
||||||
},
|
},
|
||||||
}, manager);
|
}, manager);
|
||||||
return created;
|
return created;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isUniqueViolation(error)) {
|
if (isUniqueViolation(error)) throw new ConflictException({
|
||||||
throw new ConflictException({
|
code: 'ASSET_CODE_ALREADY_EXISTS',message: 'Ya existe un registro con ese código',
|
||||||
code: 'ASSET_CODE_ALREADY_EXISTS',
|
});
|
||||||
message: 'Ya existe un registro con ese código',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,12 +263,9 @@ export class InventoryStructureService {
|
|||||||
: `SELECT id,code,name FROM asset_types WHERE lower(code)=lower($1::text) AND is_active=true LIMIT 1`;
|
: `SELECT id,code,name FROM asset_types WHERE lower(code)=lower($1::text) AND is_active=true LIMIT 1`;
|
||||||
const params = kind === 'EMPRESA' ? [] : [TYPE_CODE_BY_KIND[kind as Exclude<InventoryStructureKind,'EMPRESA'>]];
|
const params = kind === 'EMPRESA' ? [] : [TYPE_CODE_BY_KIND[kind as Exclude<InventoryStructureKind,'EMPRESA'>]];
|
||||||
const rows = (await manager.query(sql,params)) as StructureTypeRow[];
|
const rows = (await manager.query(sql,params)) as StructureTypeRow[];
|
||||||
if (!rows[0]) {
|
if (!rows[0]) throw new ConflictException({
|
||||||
throw new ConflictException({
|
code: 'INVENTORY_STRUCTURE_TYPE_NOT_CONFIGURED',message: `El nivel ${kind} no está configurado`,
|
||||||
code: 'INVENTORY_STRUCTURE_TYPE_NOT_CONFIGURED',
|
});
|
||||||
message: `El nivel ${kind} no está configurado`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,22 +276,18 @@ export class InventoryStructureService {
|
|||||||
): Promise<ParentRow | null> {
|
): Promise<ParentRow | null> {
|
||||||
const expectedType = PARENT_TYPE_BY_KIND[kind];
|
const expectedType = PARENT_TYPE_BY_KIND[kind];
|
||||||
if (!expectedType) {
|
if (!expectedType) {
|
||||||
if (parentId) {
|
if (parentId) throw new BadRequestException({
|
||||||
throw new BadRequestException({
|
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 Departamento 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;
|
||||||
}
|
}
|
||||||
if (!parentId) {
|
if (!parentId) throw new BadRequestException({
|
||||||
throw new BadRequestException({
|
code: 'INVENTORY_STRUCTURE_PARENT_REQUIRED',
|
||||||
code: 'INVENTORY_STRUCTURE_PARENT_REQUIRED',
|
message: `Para crear ${kind.toLowerCase()} primero tenés que elegir su ${expectedType}`,
|
||||||
message: `Para crear ${kind.toLowerCase()} primero tenés que elegir su ${expectedType}`,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
const rows = (await manager.query(`
|
const rows = (await manager.query(`
|
||||||
SELECT asset.id,asset.code,asset.name,type.code AS "typeCode",
|
SELECT asset.id,asset.code,asset.name,type.code AS "typeCode",
|
||||||
asset.inventory_family_id AS "inventoryFamilyId"
|
asset.inventory_family_id AS "inventoryFamilyId"
|
||||||
@@ -314,12 +298,10 @@ export class InventoryStructureService {
|
|||||||
`, [parentId])) as ParentRow[];
|
`, [parentId])) as ParentRow[];
|
||||||
const parent = rows[0];
|
const parent = rows[0];
|
||||||
if (!parent) throw new NotFoundException({ code: 'INVENTORY_STRUCTURE_PARENT_NOT_FOUND', message: 'El registro padre no existe' });
|
if (!parent) throw new NotFoundException({ code: 'INVENTORY_STRUCTURE_PARENT_NOT_FOUND', message: 'El registro padre no existe' });
|
||||||
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 Departamento → Área → Yacimiento → Instalación → Subinstalación',
|
||||||
message: 'La jerarquía requerida es Departamento → Área → Yacimiento → Instalación → Subinstalación',
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,12 +320,10 @@ export class InventoryStructureService {
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
`,[parent.id])) as IdRow[];
|
`,[parent.id])) as IdRow[];
|
||||||
const areaId=rows[0]?.id;
|
const areaId=rows[0]?.id;
|
||||||
if (!areaId) {
|
if (!areaId) throw new ConflictException({
|
||||||
throw new ConflictException({
|
code:'INVENTORY_STRUCTURE_AREA_ANCESTOR_MISSING',
|
||||||
code:'INVENTORY_STRUCTURE_AREA_ANCESTOR_MISSING',
|
message:'La ubicación seleccionada no pertenece a un Área válida',
|
||||||
message:'La ubicación seleccionada no pertenece a un Área válida',
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
return areaId;
|
return areaId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,12 +347,13 @@ export class InventoryStructureService {
|
|||||||
});
|
});
|
||||||
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,
|
||||||
family.legacy_type_code AS "legacyTypeCode",
|
family.legacy_type_code AS "legacyTypeCode",family.information_labels AS "informationLabels",
|
||||||
family.information_labels AS "informationLabels",
|
COALESCE((SELECT JSONB_AGG(rule.parent_family_id ORDER BY rule.parent_family_id)
|
||||||
parent.id AS "parentFamilyId",parent.code AS "parentFamilyCode",parent.name AS "parentFamilyName"
|
FROM inventory_family_parent_rules rule WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilyIds",
|
||||||
|
COALESCE((SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) ORDER BY parent.name,parent.code)
|
||||||
|
FROM inventory_family_parent_rules rule JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
||||||
|
WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilies"
|
||||||
FROM inventory_families family
|
FROM inventory_families family
|
||||||
LEFT JOIN inventory_family_parent_rules rule ON rule.child_family_id=family.id
|
|
||||||
LEFT JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
|
||||||
WHERE family.id=$1::uuid AND family.is_active=true
|
WHERE family.id=$1::uuid AND family.is_active=true
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, [familyId])) as FamilyRow[];
|
`, [familyId])) as FamilyRow[];
|
||||||
@@ -382,10 +363,19 @@ export class InventoryStructureService {
|
|||||||
code: 'INVENTORY_FAMILY_LEVEL_INVALID',
|
code: 'INVENTORY_FAMILY_LEVEL_INVALID',
|
||||||
message: 'La clasificación 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') {
|
||||||
throw new BadRequestException({
|
if (!parent?.inventoryFamilyId) throw new BadRequestException({
|
||||||
|
code:'INVENTORY_PARENT_FAMILY_REQUIRED',
|
||||||
|
message:'La Instalación padre debe tener una clasificación técnica válida',
|
||||||
|
});
|
||||||
|
const [compatible]=(await manager.query(`
|
||||||
|
SELECT 1 AS ok FROM inventory_family_parent_rules
|
||||||
|
WHERE child_family_id=$1::uuid AND parent_family_id=$2::uuid
|
||||||
|
LIMIT 1
|
||||||
|
`,[family.id,parent.inventoryFamilyId])) as Array<{ok:number}>;
|
||||||
|
if (!compatible) throw new BadRequestException({
|
||||||
code: 'INVENTORY_SUBINSTALLATION_FAMILY_PARENT_INVALID',
|
code: 'INVENTORY_SUBINSTALLATION_FAMILY_PARENT_INVALID',
|
||||||
message: 'La Subinstalación elegida no pertenece a la clasificación de la Instalación seleccionada',
|
message: 'Ese tipo de Subinstalación no es compatible con la clasificación de la Instalación seleccionada',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return family;
|
return family;
|
||||||
@@ -398,13 +388,8 @@ export class InventoryStructureService {
|
|||||||
: kind === 'YACIMIENTO' ? 'YAC'
|
: kind === 'YACIMIENTO' ? 'YAC'
|
||||||
: kind === 'INSTALACION' ? 'INST'
|
: kind === 'INSTALACION' ? 'INST'
|
||||||
: 'SUB';
|
: 'SUB';
|
||||||
const readable = name
|
const readable = name.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toUpperCase()
|
||||||
.normalize('NFD')
|
.replace(/[^A-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'REGISTRO';
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
.toUpperCase()
|
|
||||||
.replace(/[^A-Z0-9]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '')
|
|
||||||
.slice(0, 48) || 'REGISTRO';
|
|
||||||
return `${prefix}-${readable}-${randomUUID().slice(0, 8).toUpperCase()}`.slice(0, 120);
|
return `${prefix}-${readable}-${randomUUID().slice(0, 8).toUpperCase()}`.slice(0, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user