erp-construccion/orchestration/directivas/DIRECTIVA-ESTIMACIONES.md

14 KiB

Directiva: Estimaciones y Facturación

Metadatos

Campo Valor
ID DIR-CONST-003
Versión 1.0.0
Ámbito Vertical Construcción
Módulo MAI-008
Estado Activa

Propósito

Define las reglas y patrones para el proceso de estimaciones de obra, aplicación de anticipos, retenciones y generación de documentos de cobro.

Alcance

Esta directiva aplica a:

  • Estimaciones a clientes
  • Estimaciones a subcontratistas
  • Anticipos y amortizaciones
  • Retenciones (fondo de garantía, IMSS, etc.)
  • Generación de reportes y documentos

Tipos de Estimación

tipos:
  CLIENTE:
    descripcion: Cobro al cliente/desarrollador
    flujo: Contratista → Cliente

  SUBCONTRATISTA:
    descripcion: Pago a subcontratistas
    flujo: Subcontratista → Contratista

  DESTAJO:
    descripcion: Pago a cuadrillas por obra ejecutada
    flujo: Trabajadores → Contratista

Reglas Obligatorias

1. Estructura de Estimación

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

  // Identificación
  number: number;                    // Número consecutivo
  code: string;                      // EST-{PROJ}-{NNN}
  type: EstimationType;              // CLIENTE | SUBCONTRATISTA | DESTAJO
  period: EstimationPeriod;          // Período de corte

  // Relaciones
  contractId: string | null;         // Contrato origen
  subcontractId: string | null;      // Subcontrato (si aplica)

  // Fechas
  cutoffDate: Date;                  // Fecha de corte
  createdAt: Date;
  submittedAt: Date | null;
  approvedAt: Date | null;

  // Estado
  status: EstimationStatus;

  // Montos (todos calculados)
  grossAmount: number;               // Importe bruto
  previousAmount: number;            // Estimado anteriormente
  currentAmount: number;             // Este período

  // Deducciones
  advanceAmortization: number;       // Amortización de anticipo
  retentionGuarantee: number;        // Fondo de garantía (5-10%)
  retentionImss: number;             // Retención IMSS (5%)
  retentionIsr: number;              // Retención ISR (1.25%)
  otherDeductions: number;           // Otras deducciones

  // Neto
  netAmount: number;                 // Importe neto a pagar

  // IVA
  subtotal: number;
  iva: number;                       // IVA 16%
  total: number;                     // Total con IVA

  // Aprobaciones
  preparedBy: string;
  reviewedBy: string | null;
  approvedBy: string | null;
}

type EstimationType = 'CLIENTE' | 'SUBCONTRATISTA' | 'DESTAJO';

type EstimationStatus =
  | 'BORRADOR'
  | 'EN_REVISION'
  | 'OBSERVACIONES'
  | 'APROBADA'
  | 'RECHAZADA'
  | 'FACTURADA'
  | 'PAGADA'
  | 'CANCELADA';

2. Detalle de Estimación

interface EstimationDetail {
  id: string;
  tenantId: string;
  estimationId: string;

  // Concepto
  budgetItemId: string;              // Concepto de presupuesto
  budgetItemCode: string;            // Código (01.03.015)
  description: string;
  unitOfMeasure: string;

  // Cantidades
  contractedQuantity: number;        // Cantidad contratada
  previousQuantity: number;          // Estimado anterior
  currentQuantity: number;           // Este período
  accumulatedQuantity: number;       // Acumulado
  remainingQuantity: number;         // Pendiente

  // Precios
  unitPrice: number;
  previousAmount: number;
  currentAmount: number;
  accumulatedAmount: number;

  // % de avance
  progressPercentage: number;
}

3. Anticipos y Amortización

anticipo:
  porcentaje_maximo: 30%
  amortizacion:
    metodo: PROPORCIONAL
    formula: "(monto_estimacion / monto_contrato) * monto_anticipo"

  reglas:
    - Anticipo se registra al inicio del contrato
    - Amortización automática en cada estimación
    - No puede exceder el anticipo otorgado
    - Saldo de anticipo visible en cada estimación
interface AdvancePayment {
  id: string;
  tenantId: string;
  contractId: string;

  // Monto
  amount: number;
  percentage: number;               // % sobre contrato

  // Estado
  paidDate: Date;
  status: AdvanceStatus;

  // Amortización
  amortizedAmount: number;
  pendingAmount: number;
}

// Cálculo de amortización
function calculateAmortization(
  estimationAmount: number,
  contractAmount: number,
  advanceAmount: number,
  alreadyAmortized: number
): number {
  const proportionalAmortization =
    (estimationAmount / contractAmount) * advanceAmount;

  const pendingAdvance = advanceAmount - alreadyAmortized;

  // No puede exceder el pendiente
  return Math.min(proportionalAmortization, pendingAdvance);
}

4. Retenciones

retenciones:
  fondo_garantia:
    porcentaje: 5% - 10%
    descripcion: Garantía de vicios ocultos
    liberacion: 6-12 meses después de finiquito

  imss:
    porcentaje: 5%
    descripcion: Retención IMSS subcontratistas
    aplica_a: SUBCONTRATISTA

  isr:
    porcentaje: 1.25%
    descripcion: Retención ISR por servicios
    aplica_a: SUBCONTRATISTA

  otros:
    - Penalizaciones
    - Daños a terceros
    - Cargos por atraso
interface RetentionConfig {
  tenantId: string;
  contractId: string;

  // Fondo de garantía
  guaranteeFundPercentage: number;
  guaranteeFundAccumulated: number;
  guaranteeFundReleaseDate: Date | null;

  // IMSS (solo subcontratos)
  imssRetentionPercentage: number;

  // ISR (solo subcontratos)
  isrRetentionPercentage: number;

  // Otros
  otherRetentions: OtherRetention[];
}

// Cálculo de retenciones
function calculateRetentions(
  amount: number,
  config: RetentionConfig,
  type: EstimationType
): RetentionBreakdown {
  const guaranteeFund = amount * (config.guaranteeFundPercentage / 100);

  let imss = 0;
  let isr = 0;

  if (type === 'SUBCONTRATISTA') {
    imss = amount * (config.imssRetentionPercentage / 100);
    isr = amount * (config.isrRetentionPercentage / 100);
  }

  return {
    guaranteeFund,
    imss,
    isr,
    total: guaranteeFund + imss + isr
  };
}

5. Workflow de Aprobación

workflow:
  estados:
    BORRADOR:
      acciones: [editar, eliminar, enviar_revision]
      permisos: [preparador]

    EN_REVISION:
      acciones: [aprobar, rechazar, devolver_observaciones]
      permisos: [revisor, supervisor]

    OBSERVACIONES:
      acciones: [editar, enviar_revision]
      permisos: [preparador]

    APROBADA:
      acciones: [generar_factura, cancelar]
      permisos: [autorizador]

    FACTURADA:
      acciones: [registrar_pago]
      permisos: [tesoreria]

    PAGADA:
      acciones: []  # Estado final
      permisos: []

niveles_aprobacion:
  - nivel: 1
    monto_maximo: 100000
    aprobador: supervisor_obra

  - nivel: 2
    monto_maximo: 500000
    aprobador: gerente_proyecto

  - nivel: 3
    monto_maximo: null  # Sin límite
    aprobador: director_operaciones

Patrones de Implementación

Schema de Base de Datos

-- Schema de estimaciones
CREATE SCHEMA IF NOT EXISTS financial_management;

-- Estimaciones
CREATE TABLE financial_management.estimations (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL,

    -- Identificación
    number INTEGER NOT NULL,
    code VARCHAR(50) NOT NULL,
    type VARCHAR(20) NOT NULL,

    -- Relaciones
    contract_id UUID,
    subcontract_id UUID,

    -- Período
    period_start DATE NOT NULL,
    period_end DATE NOT NULL,
    cutoff_date DATE NOT NULL,

    -- Estado
    status VARCHAR(30) NOT NULL DEFAULT 'BORRADOR',

    -- Montos
    gross_amount DECIMAL(15,2) DEFAULT 0,
    previous_amount DECIMAL(15,2) DEFAULT 0,
    current_amount DECIMAL(15,2) DEFAULT 0,

    -- Deducciones
    advance_amortization DECIMAL(15,2) DEFAULT 0,
    retention_guarantee DECIMAL(15,2) DEFAULT 0,
    retention_imss DECIMAL(15,2) DEFAULT 0,
    retention_isr DECIMAL(15,2) DEFAULT 0,
    other_deductions DECIMAL(15,2) DEFAULT 0,

    -- Totales
    net_amount DECIMAL(15,2) DEFAULT 0,
    subtotal DECIMAL(15,2) DEFAULT 0,
    iva DECIMAL(15,2) DEFAULT 0,
    total DECIMAL(15,2) DEFAULT 0,

    -- Fechas de flujo
    submitted_at TIMESTAMPTZ,
    approved_at TIMESTAMPTZ,
    invoiced_at TIMESTAMPTZ,
    paid_at TIMESTAMPTZ,

    -- Responsables
    prepared_by UUID NOT NULL,
    reviewed_by UUID,
    approved_by UUID,

    -- Auditoría
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(tenant_id, project_id, number, type)
);

-- Detalle de estimación
CREATE TABLE financial_management.estimation_details (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL,
    estimation_id UUID NOT NULL REFERENCES financial_management.estimations(id),

    -- Concepto
    budget_item_id UUID NOT NULL,
    budget_item_code VARCHAR(50) NOT NULL,
    description TEXT NOT NULL,
    unit_of_measure VARCHAR(20),

    -- Cantidades
    contracted_quantity DECIMAL(15,4) DEFAULT 0,
    previous_quantity DECIMAL(15,4) DEFAULT 0,
    current_quantity DECIMAL(15,4) DEFAULT 0,
    accumulated_quantity DECIMAL(15,4) DEFAULT 0,

    -- Precios
    unit_price DECIMAL(15,4) NOT NULL,
    previous_amount DECIMAL(15,2) DEFAULT 0,
    current_amount DECIMAL(15,2) DEFAULT 0,
    accumulated_amount DECIMAL(15,2) DEFAULT 0,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Índices
CREATE INDEX idx_estimations_tenant ON financial_management.estimations(tenant_id);
CREATE INDEX idx_estimations_project ON financial_management.estimations(project_id);
CREATE INDEX idx_estimations_status ON financial_management.estimations(status);
CREATE INDEX idx_estimation_details_estimation ON financial_management.estimation_details(estimation_id);

-- RLS
ALTER TABLE financial_management.estimations ENABLE ROW LEVEL SECURITY;
ALTER TABLE financial_management.estimation_details ENABLE ROW LEVEL SECURITY;

CREATE POLICY "tenant_isolation" ON financial_management.estimations
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
CREATE POLICY "tenant_isolation" ON financial_management.estimation_details
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Servicio de Estimaciones

@Injectable()
export class EstimationService {
  constructor(
    @InjectRepository(EstimationEntity)
    private estimationRepo: Repository<EstimationEntity>,
    @InjectRepository(EstimationDetailEntity)
    private detailRepo: Repository<EstimationDetailEntity>,
    private advanceService: AdvancePaymentService,
    private retentionService: RetentionService,
  ) {}

  async createEstimation(
    dto: CreateEstimationDto,
    tenantId: string,
    userId: string
  ): Promise<EstimationEntity> {
    // Generar número consecutivo
    const lastNumber = await this.getLastEstimationNumber(
      dto.projectId,
      dto.type,
      tenantId
    );

    // Calcular montos
    const amounts = await this.calculateAmounts(dto, tenantId);

    // Calcular deducciones
    const deductions = await this.calculateDeductions(
      amounts.currentAmount,
      dto.contractId,
      dto.type,
      tenantId
    );

    const estimation = this.estimationRepo.create({
      ...dto,
      tenantId,
      number: lastNumber + 1,
      code: this.generateCode(dto.projectId, lastNumber + 1),
      ...amounts,
      ...deductions,
      netAmount: amounts.currentAmount - deductions.totalDeductions,
      subtotal: amounts.currentAmount - deductions.totalDeductions,
      iva: (amounts.currentAmount - deductions.totalDeductions) * 0.16,
      total: (amounts.currentAmount - deductions.totalDeductions) * 1.16,
      preparedBy: userId,
      status: 'BORRADOR'
    });

    return this.estimationRepo.save(estimation);
  }

  private async calculateDeductions(
    amount: number,
    contractId: string,
    type: EstimationType,
    tenantId: string
  ): Promise<DeductionsResult> {
    // Obtener configuración de retenciones
    const config = await this.retentionService.getConfig(contractId, tenantId);

    // Calcular amortización de anticipo
    const amortization = await this.advanceService.calculateAmortization(
      amount,
      contractId,
      tenantId
    );

    // Calcular retenciones
    const retentions = calculateRetentions(amount, config, type);

    return {
      advanceAmortization: amortization,
      retentionGuarantee: retentions.guaranteeFund,
      retentionImss: retentions.imss,
      retentionIsr: retentions.isr,
      otherDeductions: 0,
      totalDeductions: amortization + retentions.total
    };
  }

  async submitForReview(
    id: string,
    tenantId: string,
    userId: string
  ): Promise<EstimationEntity> {
    const estimation = await this.findOne(id, tenantId);

    if (estimation.status !== 'BORRADOR' && estimation.status !== 'OBSERVACIONES') {
      throw new BadRequestException('Solo borradores pueden enviarse a revisión');
    }

    estimation.status = 'EN_REVISION';
    estimation.submittedAt = new Date();

    return this.estimationRepo.save(estimation);
  }
}

Documentos Generados

documentos:
  estimacion_pdf:
    contenido:
      - Datos del proyecto y contrato
      - Período de estimación
      - Tabla de conceptos con cantidades
      - Resumen de montos
      - Desglose de deducciones
      - Firmas de elaboró, revisó, autorizó

  caratula:
    contenido:
      - Resumen ejecutivo
      - Comparativo vs presupuesto
      - Gráfica de avance

  reporte_acumulado:
    contenido:
      - Histórico de estimaciones
      - Avance acumulado por concepto
      - Proyección de cierre

Validaciones Pre-Commit

  • Cálculo correcto de amortización de anticipos
  • Retenciones calculadas según configuración
  • Workflow de aprobación implementado
  • Números consecutivos sin duplicados
  • RLS habilitado en todas las tablas
  • Validación de cantidades vs presupuesto

Referencias

  • Documentación: /docs/01-fase-alcance-inicial/MAI-008-estimaciones-facturacion/
  • Especificaciones: /docs/01-fase-alcance-inicial/MAI-008-estimaciones-facturacion/especificaciones/
  • Core directivas: ../../erp-core/orchestration/directivas/

Directiva específica de Vertical Construcción