# US-HR-003: Costeo de Mano de Obra por Obra
**Epic:** MAI-007 - RRHH, Asistencias y Nómina
**RF:** RF-HR-003
**ET:** ET-HR-003
**Tipo:** Historia de Usuario
**Prioridad:** Alta
**Story Points:** 10
**Sprint:** 10
**Estado:** 📋 Pendiente
**Última actualización:** 2025-11-17
---
## 📖 Historia de Usuario
**Como** Director de Constructora o Ingeniero Residente
**Quiero** un sistema automático que calcule y registre el costo real de mano de obra por obra usando el FSR
**Para** comparar presupuesto vs real, detectar desviaciones temprano y proyectar el costo final al 100% de avance
---
## 🎯 Criterios de Aceptación
### CA-1: Configuración del FSR (Factor de Salario Real) ⚙️
**Dado que** soy Director de Constructora
**Cuando** accedo a "Configuración" > "RRHH" > "Factor de Salario Real"
**Entonces** puedo:
1. **Ver FSR Actual:**
- Ver tarjeta con FSR total: **1.58**
- Ver desglose de componentes:
- IMSS: 23.00%
- INFONAVIT: 5.00%
- Aguinaldo: 4.17%
- Vacaciones: 1.67%
- Prima Vacacional: 0.42%
- Domingos: 14.28%
- Días Festivos: 2.19%
- Ausentismo: 5.00%
- Otros: 3.00%
- **Total: 58%** → FSR = 1 + 0.58 = **1.58**
2. **Editar Componentes:**
- Poder modificar cada porcentaje individualmente
- Ver actualización automática del FSR total
- Ejemplo: Si cambio IMSS a 25%, FSR se recalcula a 1.60
- Validación: Cada componente debe ser ≥ 0% y ≤ 50%
- Validación: FSR total debe ser entre 1.0 y 3.0
3. **Guardar Configuración:**
- Al guardar, se registra fecha de cambio
- FSR aplica a **nuevos** cálculos (no retroactivo)
- Mensaje: "FSR actualizado a 1.60. Aplicará a registros posteriores al 2025-11-17"
- Log de auditoría: quién cambió, cuándo y valores anteriores vs nuevos
**Y** solo el rol Director puede modificar el FSR
### CA-2: Cálculo Automático de Costo al Aprobar Asistencia 🤖
**Dado que** se aprueba un registro de asistencia
**Cuando** el evento `attendance.approved` se dispara
**Entonces** el sistema debe automáticamente:
1. **Obtener Datos del Empleado:**
- Empleado: Juan Pérez García
- Salario diario base: $450.00
- Obra asignada: Casa Modelo Norte
- Salario específico de obra (si existe): $500.00
- **Usar:** $500.00 (prioridad a salario específico)
2. **Obtener FSR de la Constructora:**
- Buscar FSR configurado: 1.58
- Fecha efectiva: 2025-11-15
3. **Calcular Días Trabajados:**
- Basado en check-in y check-out:
- Check-in: 07:15 AM
- Check-out: 05:30 PM
- Horas trabajadas: 10h 15min
- **Si ≥ 8 horas:** 1.0 día
- **Si 4-8 horas:** 0.5 días
- **Si < 4 horas:** 0.25 días
- En este caso: **1.0 día**
4. **Calcular Costo Real:**
```
Costo Real = Salario Diario × Días Trabajados × FSR
Costo Real = $500.00 × 1.0 × 1.58 = $790.00
```
5. **Determinar Partida Presupuestal:**
- Buscar si el empleado pertenece a una cuadrilla
- Buscar si la cuadrilla está asignada a una partida presupuestal en esa obra
- Cuadrilla: Albañilería A
- Partida asignada: "03.02 - Muro de Block"
- Si no hay asignación: marcar como "Indirecto"
6. **Guardar Registro de Costo:**
```json
{
"attendanceId": "uuid-attendance",
"employeeId": "uuid-employee",
"workId": "uuid-work",
"budgetItemId": "uuid-budget-item", // o null
"workDate": "2025-11-17",
"daysWorked": 1.0,
"dailySalary": 500.00,
"fsr": 1.58,
"realCost": 790.00 // Calculado automáticamente por BD
}
```
**Y** este proceso debe ocurrir en segundo plano sin intervención del usuario
### CA-3: Dashboard de Costeo por Obra 📊
**Dado que** soy Director, Ingeniero o Residente
**Cuando** accedo a una obra > "Costeo de Mano de Obra"
**Entonces** veo un dashboard con:
1. **Resumen en Tarjetas:**
```
┌─────────────────────┐ ┌─────────────────────┐
│ Presupuesto MO │ │ Real Gastado │
│ $1,250,000 │ │ $875,342 │
└─────────────────────┘ └─────────────────────┘
┌─────────────────────┐ ┌─────────────────────┐
│ Proyección 100% │ │ Desviación │
│ $1,312,500 │ │ +5.0% [AMARILLO] │
└─────────────────────┘ └─────────────────────┘
```
2. **Indicadores de Color:**
- **Verde:** Desviación < 10% (dentro de presupuesto)
- **Amarillo:** Desviación 10-20% (advertencia)
- **Rojo:** Desviación > 20% (crítico)
3. **Avance Físico:**
- Porcentaje de avance de la obra: 66.7%
- Integrado desde módulo de Control de Obra
- Proyección calculada como: `Real Gastado / Avance Físico × 100`
- Ejemplo: `$875,342 / 0.667 × 100 = $1,312,500`
4. **Fórmula de Desviación:**
```
Desviación = (Proyección - Presupuesto) / Presupuesto × 100
Desviación = ($1,312,500 - $1,250,000) / $1,250,000 × 100 = +5.0%
```
### CA-4: Detalle por Partida Presupuestal 📋
**Dado que** veo el dashboard de costeo
**Cuando** bajo a la sección "Costo por Partida"
**Entonces** veo una tabla con:
| Partida | Presupuestado | Real Gastado | Días-Hombre | Desviación | Estado |
|---------|---------------|--------------|-------------|------------|--------|
| 02.01 - Excavación | $85,000 | $82,500 | 165 | -2.9% | 🟢 |
| 03.02 - Muro de Block | $320,000 | $285,000 | 570 | -10.9% | 🟢 |
| 04.01 - Castillos | $180,000 | $195,000 | 390 | +8.3% | 🟡 |
| 05.03 - Losa | $425,000 | $312,842 | 625 | (proyección) | 🟢 |
| **Indirecto** | $240,000 | $0 | 0 | - | - |
| **TOTAL** | **$1,250,000** | **$875,342** | **1,750** | **+5.0%** | **🟡** |
**Características de la tabla:**
- Ordenable por cualquier columna
- Filtrable por estado (verde, amarillo, rojo)
- Exportable a Excel
- Clic en partida: ver detalle de empleados
**Y** la fila "Indirecto" agrupa costos sin partida asignada (supervisión, logística, etc.)
### CA-5: Detalle de Empleados por Partida 👷
**Dado que** hago clic en una partida (ej: "03.02 - Muro de Block")
**Cuando** se abre el modal de detalle
**Entonces** veo:
1. **Header:**
- Partida: 03.02 - Muro de Block
- Presupuestado: $320,000
- Real gastado: $285,000
- Días-hombre: 570
2. **Lista de Empleados:**
| Empleado | Cuadrilla | Días Trabajados | Costo Total |
|----------|-----------|-----------------|-------------|
| Juan Pérez | Albañilería A | 45 | $35,550 |
| María López | Albañilería A | 43 | $34,002 |
| Carlos Ruiz | Albañilería B | 38 | $30,020 |
| ... | ... | ... | ... |
3. **Gráfica de Tendencia:**
- Gráfica de línea mostrando costo acumulado por semana
- Comparación con curva de presupuesto
- Detectar si hay aceleración o desaceleración de gasto
### CA-6: Asignación de Cuadrillas a Partidas 🔧
**Dado que** soy Ingeniero o Residente
**Cuando** accedo a "Cuadrillas" en una obra
**Entonces** puedo:
1. **Ver Cuadrillas de la Obra:**
- Lista de cuadrillas activas
- Por cada cuadrilla: nombre, tipo, supervisor, # miembros
2. **Asignar a Partida:**
- Hacer clic en "Asignar a Partida"
- Seleccionar partida del presupuesto (dropdown)
- Seleccionar fecha de inicio: 2025-11-10
- Fecha de fin: opcional (abierta si es indefinido)
- Guardar asignación
3. **Registro de Asignación:**
```json
{
"crewId": "uuid-crew",
"workId": "uuid-work",
"budgetItemId": "uuid-budget-item",
"startDate": "2025-11-10",
"endDate": null, // Abierta
"isActive": true
}
```
4. **Validación:**
- Una cuadrilla puede estar asignada a múltiples partidas en diferentes periodos
- No puede estar en dos partidas **simultáneamente** (fechas traslapadas)
- Si se intenta: error "La cuadrilla ya está asignada a '04.01 - Castillos' desde el 2025-11-08"
5. **Historial de Asignaciones:**
- Ver tabla con todas las asignaciones pasadas y presentes
- Filtrar por cuadrilla, partida, fecha
**Y** a partir de la fecha de asignación, todos los costos de esa cuadrilla se imputan a la partida correspondiente
### CA-7: Alertas de Desviación 🚨
**Dado que** el sistema calcula costos diariamente
**Cuando** detecta una desviación significativa
**Entonces** debe:
1. **Generar Alerta Automática:**
- **Condición:** Desviación de una partida > 15%
- Crear notificación para:
- Ingeniero Residente de la obra
- Director de Constructora
- Contenido de notificación:
```
⚠️ Alerta de Desviación de Costo
Obra: Casa Modelo Norte
Partida: 04.01 - Castillos
Desviación: +18.5%
Real: $195,000 vs Presupuesto: $164,620 (a la fecha)
Acción recomendada: Revisar rendimientos y asignación de personal
```
2. **Dashboard de Alertas:**
- Sección en home del usuario
- Lista de alertas activas
- Filtros: por obra, por criticidad (amarillo, rojo)
- Acción: "Marcar como revisado"
3. **Email Semanal:**
- Cada lunes a las 8 AM
- Resumen de alertas de la semana anterior
- Top 3 partidas con mayor desviación
- Solo si hay alertas activas
### CA-8: Comparación Histórica de Obras 📈
**Dado que** soy Director
**Cuando** accedo a "Reportes" > "Análisis Comparativo de Obras"
**Entonces** puedo:
1. **Seleccionar Obras:**
- Seleccionar hasta 5 obras para comparar
- Filtrar por: estado (activa, terminada), año, tipo de obra
2. **Ver Tabla Comparativa:**
| Obra | Presup. MO | Real MO | Desv. | m² | Costo/m² | Eficiencia |
|------|------------|---------|-------|-----|----------|------------|
| Casa Norte | $1.25M | $1.31M | +5% | 250 | $5,240 | 95% |
| Casa Sur | $980K | $920K | -6% | 200 | $4,600 | 106% |
| Edificio A | $3.5M | $3.8M | +8% | 800 | $4,750 | 92% |
3. **Fórmulas:**
- Costo/m² = Real MO / Metros cuadrados
- Eficiencia = (Presupuesto / Real) × 100
- Benchmark: Identificar obra con mejor costo/m²
4. **Gráfica de Dispersión:**
- Eje X: Metros cuadrados
- Eje Y: Costo/m²
- Cada punto: una obra
- Detectar outliers
**Y** poder exportar el análisis a PDF para presentaciones
### CA-9: Proyección y Escenarios 🔮
**Dado que** veo el dashboard de una obra en progreso
**Cuando** accedo a "Proyecciones"
**Entonces** puedo:
1. **Ver Proyección Base:**
- Basada en % de avance físico actual
- Costo final proyectado: $1,312,500
- Desviación proyectada: +5.0%
2. **Simular Escenarios:**
- **Escenario Optimista:** Si mejoramos rendimiento 10%
- Costo proyectado: $1,181,250 (-5.5%)
- **Escenario Pesimista:** Si rendimiento empeora 10%
- Costo proyectado: $1,443,750 (+15.5%)
- **Escenario Realista:** Con tendencia actual
- Mantiene proyección base
3. **Ajuste de Variables:**
- Slider de % de mejora/empeoramiento: -20% a +20%
- Cambio de FSR futuro (si se espera cambio legal)
- Cambio de salario promedio (aumentos programados)
- Ver impacto en tiempo real
4. **Exportar Escenario:**
- Guardar escenario con nombre
- Ejemplo: "Escenario Post-Aumento Salarial Diciembre"
- Compartir con equipo vía link
### CA-10: Permisos por Rol 🔐
**Roles y Permisos:**
| Acción | Director | Engineer | Resident | HR | Finance |
|--------|----------|----------|----------|-----|---------|
| Ver dashboard costeo | ✅ | ✅ | ✅ | ❌ | ✅ |
| Configurar FSR | ✅ | ❌ | ❌ | ❌ | ❌ |
| Asignar cuadrillas a partidas | ✅ | ✅ | ✅ | ❌ | ❌ |
| Ver detalle de empleados | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ver salarios individuales | ✅ | ❌ | ❌ | ✅ | ✅ |
| Exportar reportes | ✅ | ✅ | ✅ | ❌ | ✅ |
| Crear proyecciones | ✅ | ✅ | ❌ | ❌ | ❌ |
---
## 🔧 Detalles Técnicos
### Arquitectura Event-Driven
```typescript
// labor-costs.service.ts
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class LaborCostsService {
/**
* Event listener que se ejecuta cuando se aprueba asistencia
*/
@OnEvent('attendance.approved')
async handleAttendanceApproved(attendance: AttendanceRecord) {
// 1. Obtener empleado y salario
const employee = await this.getEmployeeWithSalary(attendance.employeeId);
// 2. Obtener FSR de la constructora
const fsrConfig = await this.getFSRConfig(employee.constructoraId);
// 3. Calcular días trabajados
const daysWorked = await this.calculateDaysWorked(attendance);
// 4. Determinar partida presupuestal
const budgetItemId = await this.determineBudgetItem(
attendance.employeeId,
attendance.workId,
attendance.workDate
);
// 5. Crear registro de costo
const laborCost = this.laborCostRepo.create({
attendanceId: attendance.id,
employeeId: attendance.employeeId,
workId: attendance.workId,
budgetItemId,
workDate: attendance.workDate,
daysWorked,
dailySalary: employee.workSpecificSalary || employee.currentSalary,
fsr: fsrConfig.totalFsr,
// realCost se calcula automáticamente en BD con GENERATED column
});
await this.laborCostRepo.save(laborCost);
// 6. Verificar desviaciones y emitir alertas si es necesario
await this.checkDeviations(attendance.workId);
}
}
```
### Columna Calculada en PostgreSQL
```sql
-- labor_costs table
CREATE TABLE hr.labor_costs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
attendance_id UUID UNIQUE NOT NULL REFERENCES hr.attendance_records(id),
employee_id UUID NOT NULL,
work_id UUID NOT NULL,
budget_item_id UUID REFERENCES budgets.budget_items(id),
work_date DATE NOT NULL,
days_worked DECIMAL(3,2) NOT NULL CHECK(days_worked > 0 AND days_worked <= 1),
daily_salary DECIMAL(10,2) NOT NULL,
fsr DECIMAL(4,2) NOT NULL,
-- Columna generada automáticamente
real_cost DECIMAL(10,2) GENERATED ALWAYS AS (daily_salary * days_worked * fsr) STORED,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_labor_costs_work ON hr.labor_costs(work_id, work_date);
CREATE INDEX idx_labor_costs_budget_item ON hr.labor_costs(budget_item_id);
```
### Query de Dashboard
```typescript
// Obtener resumen de costeo por obra
async getCostSummary(workId: string) {
// 1. Total presupuestado de MO
const budgetedLabor = await this.db.query(`
SELECT SUM(labor_cost) as total
FROM budgets.budget_items
WHERE work_id = $1
`, [workId]);
// 2. Total real gastado
const realLabor = await this.db.query(`
SELECT
SUM(real_cost) as total,
COUNT(*) as total_records,
SUM(days_worked) as total_days
FROM hr.labor_costs
WHERE work_id = $1
`, [workId]);
// 3. Avance físico (de control de obra)
const physicalProgress = await this.getPhysicalProgress(workId);
// 4. Proyección
const projected = physicalProgress > 10
? (realLabor.total / physicalProgress) * 100
: null;
// 5. Desviación
const deviation = projected
? ((projected - budgetedLabor.total) / budgetedLabor.total) * 100
: null;
return {
budgeted: budgetedLabor.total,
real: realLabor.total,
totalDays: realLabor.total_days,
physicalProgress,
projected,
deviation,
status: this.getDeviationStatus(deviation),
};
}
```
### Componente React del Dashboard
```typescript
// CostDashboard.tsx
import { useQuery } from '@tanstack/react-query';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
export function CostDashboard({ workId }: { workId: string }) {
const { data, isLoading } = useQuery({
queryKey: ['labor-costs', 'summary', workId],
queryFn: () => apiService.get(`/hr/labor-costs/summary/${workId}`),
refetchInterval: 60000, // Actualizar cada minuto
});
if (isLoading) return
Presupuesto MO
${data.budgeted.toLocaleString('es-MX')}
Real Gastado
${data.real.toLocaleString('es-MX')}
{data.totalDays} días-hombre
Proyección 100%
${data.projected?.toLocaleString('es-MX') || 'N/A'}
Avance: {data.physicalProgress}%
Desviación
{data.deviation > 0 ? '+' : ''} {data.deviation?.toFixed(1)}%