erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-002-calculo-montos.md

263 lines
6.5 KiB
Markdown

# ET-EST-002: Cálculo Automático de Montos
**ID:** ET-EST-002
**Módulo:** MAI-008
**Relacionado con:** RF-EST-001, RF-EST-002
---
## 🧮 EstimationCalculator Service
```typescript
@Injectable()
export class EstimationCalculatorService {
/**
* Calcula monto bruto desde items
*/
calculateMontoBruto(items: EstimationItemDto[]): number {
return items.reduce((total, item) => {
return total + (item.cantidadEstimadaActual * item.precioUnitario);
}, 0);
}
/**
* Calcula amortización de anticipo
*/
async calculateAmortizacion(
montoBruto: number,
contractId: string,
estimacionNumero: number
): Promise<number> {
// Obtener anticipo del contrato
const anticipo = await this.getAnticipo(contractId);
if (!anticipo) return 0;
// Obtener amortizaciones previas
const amortizadoAnterior = await this.getTotalAmortizado(contractId, estimacionNumero - 1);
const saldoAnticipo = anticipo.monto - amortizadoAnterior;
if (saldoAnticipo <= 0) return 0;
// Calcular amortización actual
const porcentaje = anticipo.porcentajeAmortizacionPorEstimacion / 100;
const amortizacion = Math.min(
saldoAnticipo,
montoBruto * porcentaje
);
return Math.round(amortizacion);
}
/**
* Calcula retenciones
*/
calculateRetenciones(
montoBruto: number,
amortizacion: number,
contractConfig: RetentionConfig
): RetentionesDetalle {
const base = montoBruto - amortizacion;
const retenciones = {
fondoGarantia: Math.round(base * (contractConfig.porcentajeFondoGarantia / 100)),
isr: contractConfig.retieneISR ? Math.round(base * (contractConfig.tasaISR / 100)) : 0,
iva: contractConfig.retieneIVA ? Math.round(base * (contractConfig.tasaIVA / 100)) : 0,
otras: contractConfig.otrasRetenciones || 0
};
const total = Object.values(retenciones).reduce((sum, val) => sum + val, 0);
return {
...retenciones,
total
};
}
/**
* Calcula monto neto final
*/
calculateMontoNeto(
montoBruto: number,
amortizacion: number,
totalRetenciones: number
): number {
const neto = montoBruto - amortizacion - totalRetenciones;
if (neto < 0) {
throw new BadRequestException('Monto neto no puede ser negativo');
}
return neto;
}
/**
* Cálculo completo de estimación
*/
async calculateEstimationTotals(
items: EstimationItemDto[],
contractId: string,
estimacionNumero: number,
estimationType: EstimationType
): Promise<EstimationTotals> {
// 1. Monto bruto
const montoBruto = this.calculateMontoBruto(items);
// 2. Amortización
const amortizacion = await this.calculateAmortizacion(
montoBruto,
contractId,
estimacionNumero
);
// 3. Retenciones
const contractConfig = await this.getContractConfig(contractId, estimationType);
const retenciones = this.calculateRetenciones(montoBruto, amortizacion, contractConfig);
// 4. Monto neto
const montoNeto = this.calculateMontoNeto(montoBruto, amortizacion, retenciones.total);
return {
montoBruto,
amortizacion,
retenciones,
montoNeto
};
}
}
```
---
## 📊 Fórmulas de Cálculo
### Para Estimación a Cliente
```typescript
// Ejemplo real
const calculo = {
// Entrada
viviendas_terminadas: 25,
precio_unitario: 500_000_00, // $500K en centavos
// Paso 1: Monto bruto
monto_bruto: 25 * 500_000_00 = 12_500_000_00, // $12.5M
// Paso 2: Amortización
anticipo_inicial: 10_000_000_00, // $10M (20% del contrato)
porcentaje_amortizacion: 25,
amortizacion: Math.min(
10_000_000_00, // Saldo disponible
12_500_000_00 * 0.25 // 25% del bruto
) = 2_500_000_00, // $2.5M
// Paso 3: Base retenciones
base_retenciones: 12_500_000_00 - 2_500_000_00 = 10_000_000_00,
// Paso 4: Retenciones
retencion_fondo_garantia: 10_000_000_00 * 0.05 = 500_000_00, // 5%
retencion_isr: 0,
retencion_iva: 0,
total_retenciones: 500_000_00,
// Paso 5: Monto neto
monto_neto: 12_500_000_00 - 2_500_000_00 - 500_000_00 = 9_500_000_00 // $9.5M
};
```
### Para Estimación a Subcontratista
```typescript
const calculoSub = {
// Entrada
monto_subcontrato: 2_000_000_00, // $2M
porcentaje_avance: 30,
// Paso 1: Monto bruto
monto_bruto: 2_000_000_00 * 0.30 = 600_000_00, // $600K
// Paso 2: Amortización proporcional
anticipo: 2_000_000_00 * 0.10 = 200_000_00, // 10% anticipo
amortizacion: 200_000_00 * 0.30 = 60_000_00, // 30% del anticipo
// Paso 3: Retenciones
base: 600_000_00 - 60_000_00 = 540_000_00,
retencion: 540_000_00 * 0.10 = 54_000_00, // 10%
// Paso 4: Neto
monto_neto: 600_000_00 - 60_000_00 - 54_000_00 = 486_000_00 // $486K
};
```
---
## ✅ Validaciones
```typescript
@Injectable()
export class EstimationValidatorService {
/**
* Valida que no se exceda el monto del contrato
*/
async validateContractLimit(
contractId: string,
newMontoBruto: number
): Promise<void> {
const contract = await this.contractsRepo.findOne(contractId);
const estimatedTotal = await this.getTotalEstimated(contractId);
if (estimatedTotal + newMontoBruto > contract.montoTotal) {
throw new BadRequestException(
`Excede monto del contrato. Disponible: $${(contract.montoTotal - estimatedTotal) / 100}`
);
}
}
/**
* Valida que no se dupliquen conceptos
*/
async validateNoDuplicateItems(
projectId: string,
items: EstimationItemDto[]
): Promise<void> {
const previousItems = await this.getEstimatedItems(projectId);
for (const item of items) {
const alreadyEstimated = previousItems.find(
prev => prev.conceptCatalogId === item.conceptCatalogId
);
if (alreadyEstimated) {
throw new BadRequestException(
`Concepto "${item.descripcion}" ya fue estimado previamente`
);
}
}
}
/**
* Valida avances verificados
*/
async validateVerifiedProgress(items: EstimationItemDto[]): Promise<void> {
for (const item of items) {
if (!item.avanceObraId) {
throw new BadRequestException(
`Item "${item.descripcion}" no tiene avance de obra vinculado`
);
}
const avance = await this.avancesRepo.findOne(item.avanceObraId);
if (avance.status !== 'verified') {
throw new BadRequestException(
`Avance de "${item.descripcion}" no está verificado`
);
}
}
}
}
```
---
**Generado:** 2025-11-20
**Estado:** ✅ Completo