erp-construccion/orchestration/directivas/DIRECTIVA-CONTROL-OBRA.md

12 KiB

Directiva: Control de Obra

Metadatos

Campo Valor
ID DIR-CONST-002
Versión 1.0.0
Ámbito Vertical Construcción
Módulos MAI-002, MAI-005, MAI-012
Estado Activa

Propósito

Define las reglas y patrones para el control de obra, gestión de proyectos de construcción, avances y recursos.

Alcance

Esta directiva aplica a:

  • Estructura jerárquica de proyectos
  • Control de avances de obra
  • Gestión de recursos y materiales
  • Presupuestos y costos
  • Subcontratos y contratos
  • Estimaciones de obra

Estructura Jerárquica de Proyectos

jerarquia:
  nivel_1:
    nombre: Proyecto/Desarrollo
    entidad: Project
    ejemplo: "Fraccionamiento Los Álamos"

  nivel_2:
    nombre: Etapa/Fase
    entidad: Phase
    ejemplo: "Etapa 3 - 120 viviendas"

  nivel_3:
    nombre: Manzana/Torre
    entidad: Block
    ejemplo: "Manzana 15"

  nivel_4:
    nombre: Lote/Vivienda/Departamento
    entidad: Unit
    ejemplo: "Lote 15-A Casa Modelo Roma"

tipos_proyecto:
  HORIZONTAL:
    niveles: [Desarrollo, Etapa, Manzana, Lote]
    ejemplo: Fraccionamiento de casas

  VERTICAL:
    niveles: [Desarrollo, Torre, Piso, Departamento]
    ejemplo: Edificio de departamentos

  MIXTO:
    niveles: [Desarrollo, Zona, Bloque, Unidad]
    ejemplo: Centro comercial con oficinas

Reglas Obligatorias

1. Entidades de Proyecto

// Proyecto base
interface ProjectEntity {
  id: string;
  tenantId: string;

  // Identificación
  code: string;              // Código único: PROJ-2025-001
  name: string;
  projectType: ProjectType;  // HORIZONTAL | VERTICAL | MIXTO

  // Ubicación
  address: string;
  city: string;
  state: string;
  coordinates: GeoPoint | null;

  // Estado y fechas
  status: ProjectStatus;
  plannedStartDate: Date;
  plannedEndDate: Date;
  actualStartDate: Date | null;
  actualEndDate: Date | null;

  // Métricas
  totalUnits: number;
  completedUnits: number;
  progressPercentage: number;

  // Presupuesto
  budgetAmount: number;
  actualCost: number;
  costVariance: number;

  // Auditoría
  createdAt: Date;
  updatedAt: Date;
  createdBy: string;
  updatedBy: string;
}

type ProjectStatus =
  | 'PLANEACION'
  | 'EN_CONSTRUCCION'
  | 'PAUSADO'
  | 'FINALIZADO'
  | 'CANCELADO';

type ProjectType = 'HORIZONTAL' | 'VERTICAL' | 'MIXTO';

2. Control de Avances

avances:
  tipos:
    - FISICO:        # Porcentaje de obra ejecutada
    - FINANCIERO:    # Porcentaje de presupuesto ejercido
    - PROGRAMATICO:  # Cumplimiento del programa

  granularidad:
    - Por unidad (casa/depto)
    - Por partida de presupuesto
    - Por concepto de obra
    - Por subcontrato

  frecuencia_registro:
    minima: Semanal
    recomendada: Diaria para conceptos críticos
// Registro de avance
interface ProgressEntry {
  id: string;
  tenantId: string;
  projectId: string;
  unitId: string;

  // Avance
  date: Date;
  conceptId: string;           // Concepto de obra
  previousProgress: number;    // % anterior
  currentProgress: number;     // % actual
  incrementProgress: number;   // Incremento

  // Cantidades
  quantity: number;
  unitOfMeasure: string;

  // Validación
  registeredBy: string;
  validatedBy: string | null;
  validatedAt: Date | null;

  // Evidencia
  photos: string[];            // URLs de fotos
  notes: string;
}

3. Gestión de Recursos

// Asignación de recursos
interface ResourceAssignment {
  id: string;
  tenantId: string;
  projectId: string;

  // Recurso
  resourceType: ResourceType;
  resourceId: string;

  // Asignación
  assignedFrom: Date;
  assignedTo: Date;
  quantity: number;

  // Costo
  dailyRate: number;
  estimatedCost: number;
  actualCost: number;
}

type ResourceType =
  | 'MANO_OBRA'
  | 'EQUIPO'
  | 'MAQUINARIA'
  | 'MATERIAL';

4. Presupuesto y Costos

estructura_presupuesto:
  niveles:
    - Partida:      # Ej: "01 - Preliminares"
    - Subpartida:   # Ej: "01.01 - Limpieza"
    - Concepto:     # Ej: "01.01.001 - Trazo y nivelación"

  tipos_costo:
    - MANO_OBRA:
        directa: true
        indirecta: true
    - MATERIALES:
        locales: true
        importados: true
    - EQUIPOS:
        propios: true
        rentados: true
    - SUBCONTRATOS:
        mano_obra: true
        paquete: true
    - INDIRECTOS:
        administracion: true
        financiamiento: true
interface BudgetItem {
  id: string;
  tenantId: string;
  projectId: string;

  // Jerarquía
  partitionCode: string;      // "01.01.001"
  parentId: string | null;
  level: number;              // 1, 2, 3

  // Concepto
  description: string;
  unitOfMeasure: string;

  // Cantidades
  budgetedQuantity: number;
  executedQuantity: number;

  // Precios
  unitPrice: number;
  budgetedAmount: number;
  executedAmount: number;

  // Variación
  quantityVariance: number;
  amountVariance: number;
  variancePercentage: number;
}

5. Subcontratos

interface SubcontractEntity {
  id: string;
  tenantId: string;
  projectId: string;

  // Contratista
  contractorId: string;
  contractorName: string;

  // Contrato
  contractNumber: string;
  contractType: SubcontractType;
  description: string;

  // Alcance
  budgetItems: string[];      // IDs de conceptos

  // Montos
  contractAmount: number;
  amortizations: number;      // Anticipos amortizados
  deductions: number;         // Deducciones
  retentions: number;         // Retenciones
  netPayable: number;

  // Estado
  status: SubcontractStatus;
  startDate: Date;
  endDate: Date;

  // Estimaciones
  estimationsCount: number;
  estimatedAmount: number;
  paidAmount: number;
}

type SubcontractType =
  | 'MANO_OBRA'        // Solo mano de obra
  | 'SUMINISTRO'       // Solo materiales
  | 'PAQUETE'          // Todo incluido
  | 'PRECIO_UNITARIO'  // Por unidad ejecutada
  | 'PRECIO_ALZADO';   // Monto fijo

type SubcontractStatus =
  | 'BORRADOR'
  | 'ACTIVO'
  | 'PAUSADO'
  | 'FINIQUITADO'
  | 'CANCELADO';

Patrones de Implementación

Schema de Base de Datos

-- Schema de proyectos
CREATE SCHEMA IF NOT EXISTS project_management;

-- Proyectos principales
CREATE TABLE project_management.projects (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL,
    code VARCHAR(50) NOT NULL,
    name VARCHAR(200) NOT NULL,
    project_type VARCHAR(20) NOT NULL,
    status VARCHAR(30) NOT NULL DEFAULT 'PLANEACION',

    -- Ubicación
    address TEXT,
    city VARCHAR(100),
    state VARCHAR(100),
    coordinates POINT,

    -- Fechas
    planned_start_date DATE,
    planned_end_date DATE,
    actual_start_date DATE,
    actual_end_date DATE,

    -- Métricas
    total_units INTEGER DEFAULT 0,
    completed_units INTEGER DEFAULT 0,
    progress_percentage DECIMAL(5,2) DEFAULT 0,

    -- Presupuesto
    budget_amount DECIMAL(15,2) DEFAULT 0,
    actual_cost DECIMAL(15,2) DEFAULT 0,

    -- Auditoría
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID,
    updated_by UUID,
    is_active BOOLEAN DEFAULT TRUE,

    UNIQUE(tenant_id, code)
);

-- Índices obligatorios
CREATE INDEX idx_projects_tenant_id ON project_management.projects(tenant_id);
CREATE INDEX idx_projects_status ON project_management.projects(status);
CREATE INDEX idx_projects_type ON project_management.projects(project_type);

-- RLS
ALTER TABLE project_management.projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON project_management.projects
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Fases/Etapas
CREATE TABLE project_management.phases (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL REFERENCES project_management.projects(id),
    code VARCHAR(50) NOT NULL,
    name VARCHAR(200) NOT NULL,
    sequence INTEGER NOT NULL,
    status VARCHAR(30) NOT NULL DEFAULT 'PLANEACION',
    total_units INTEGER DEFAULT 0,
    completed_units INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Unidades (viviendas/departamentos)
CREATE TABLE project_management.units (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL REFERENCES project_management.projects(id),
    phase_id UUID REFERENCES project_management.phases(id),
    block_id UUID,

    code VARCHAR(50) NOT NULL,
    unit_type VARCHAR(50),      -- Casa, Depto, Local
    prototype_id UUID,          -- Modelo de vivienda

    status VARCHAR(30) NOT NULL DEFAULT 'PENDIENTE',
    progress_percentage DECIMAL(5,2) DEFAULT 0,

    -- Venta
    sale_status VARCHAR(30) DEFAULT 'DISPONIBLE',
    client_id UUID,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

Servicio de Avances

@Injectable()
export class ProgressService {
  constructor(
    @InjectRepository(ProgressEntryEntity)
    private progressRepo: Repository<ProgressEntryEntity>,
    @InjectRepository(UnitEntity)
    private unitRepo: Repository<UnitEntity>,
    private eventEmitter: EventEmitter2,
  ) {}

  async registerProgress(
    dto: RegisterProgressDto,
    tenantId: string,
    userId: string
  ): Promise<ProgressEntryEntity> {
    // Obtener avance anterior
    const lastProgress = await this.progressRepo.findOne({
      where: {
        unitId: dto.unitId,
        conceptId: dto.conceptId,
        tenantId
      },
      order: { date: 'DESC' }
    });

    const previousProgress = lastProgress?.currentProgress ?? 0;

    // Validar que no retroceda
    if (dto.currentProgress < previousProgress) {
      throw new BadRequestException(
        'El avance no puede ser menor al registrado anteriormente'
      );
    }

    // Crear registro
    const entry = this.progressRepo.create({
      ...dto,
      tenantId,
      previousProgress,
      incrementProgress: dto.currentProgress - previousProgress,
      registeredBy: userId,
    });

    const saved = await this.progressRepo.save(entry);

    // Actualizar avance de unidad
    await this.updateUnitProgress(dto.unitId, tenantId);

    // Emitir evento
    this.eventEmitter.emit('progress.registered', {
      tenantId,
      projectId: dto.projectId,
      unitId: dto.unitId,
      progress: dto.currentProgress
    });

    return saved;
  }

  private async updateUnitProgress(
    unitId: string,
    tenantId: string
  ): Promise<void> {
    // Calcular promedio ponderado de avances por concepto
    const result = await this.progressRepo
      .createQueryBuilder('p')
      .select('AVG(p.current_progress)', 'avgProgress')
      .where('p.unit_id = :unitId', { unitId })
      .andWhere('p.tenant_id = :tenantId', { tenantId })
      .getRawOne();

    await this.unitRepo.update(
      { id: unitId, tenantId },
      { progressPercentage: result.avgProgress ?? 0 }
    );
  }
}

Nomenclatura de Códigos

proyectos:
  formato: "PROJ-{YYYY}-{NNN}"
  ejemplo: "PROJ-2025-001"

fases:
  formato: "{PROJ_CODE}-E{NN}"
  ejemplo: "PROJ-2025-001-E03"

unidades:
  horizontal: "M{MZ}-L{LT}"   # M15-L07
  vertical: "T{TW}-P{PO}-D{DP}"  # T01-P05-D02

presupuesto:
  partida: "{NN}"             # 01
  subpartida: "{NN}.{NN}"     # 01.03
  concepto: "{NN}.{NN}.{NNN}" # 01.03.015

Validaciones Pre-Commit

  • Jerarquía de proyecto correctamente implementada
  • Control de avances con validación de retroceso
  • Presupuesto estructurado por niveles
  • RLS habilitado en todas las tablas
  • Eventos emitidos en operaciones críticas
  • Códigos generados según nomenclatura

Referencias

  • Documentación proyectos: /docs/01-fase-alcance-inicial/MAI-002-proyectos-estructura/
  • Documentación avances: /docs/01-fase-alcance-inicial/MAI-005-control-obra/
  • Core directivas: ../../erp-core/orchestration/directivas/

Directiva específica de Vertical Construcción