Saltar a contenido

Especificación Técnica: trx_avisos_cobro

Esquema: fuerza_ventas
Base de Datos: crm
Tabla Legacy Origen: c213t_amortizacion_cliente / vi_aviso_cobro
Propósito: Programación y seguimiento de órdenes de cobranza asignadas a los promotores en campo para la gestión y recaudación de facturas de clientes.


1. Justificación y Mejoras de Arquitectura

  • Aislamiento Multi-Tenant (RLS): Integra empresa_id BIGINT NOT NULL con políticas RLS obligatorias para garantizar aislamiento riguroso por tenant en todas las operaciones.
  • Integridad Referencial Compuesta y Normalización de Catálogos: Clave única compuesta (empresa_id, id) y claves foráneas compuestas hacia fuerza_ventas.mae_promotores(empresa_id, promotor_id), clientes.mae_clientes(empresa_id, cliente_id) y fuerza_ventas.cat_estados_cobro(empresa_id, estado_cobro_id).
  • Control Estricto de Unicidad de Código: Restricción única (empresa_id, codigo_aviso) para evitar duplicidad de correlativos operativos dentro de la misma organización.
  • Eliminación de Deuda Técnica Legacy: Depuración de identificadores BIGINT, campos redundantes y tipado homogéneo en UUID y NUMERIC(12,2).
  • Trazabilidad y Auditoría: Auditoría integral (created_at, updated_at, deleted_at, created_by, updated_by) con auditoría integral.

2. Definiciones de Implementación

Table fuerza_ventas.trx_avisos_cobro {
    id                      uuid          [pk, default: `gen_random_uuid()`, note: 'Identificador único UUID']
    empresa_id                  bigint        [not null, note: 'Identificador BIGINT de la empresa en core.identidad (RLS)']
    promotor_id             uuid          [not null, note: 'Promotor asignado a la cobranza en campo']
    cliente_id              uuid          [not null, note: 'Cliente deudor']
    codigo_aviso            varchar(30)   [not null, note: 'Código correlativo único del aviso de cobro']
    fecha_emision           date          [not null, note: 'Fecha de generación de la orden de cobro']
    fecha_limite_gestion    date          [not null, note: 'Fecha máxima estipulada para realizar la cobranza']
    monto_total_gestion     numeric(12,2) [not null, default: `0.00`, note: 'Monto total programado a cobrar']
    monto_cobrado_acumulado numeric(12,2) [not null, default: `0.00`, note: 'Monto total recaudado en campo']
    estado_cobro_id         uuid          [not null, note: 'Estado de la orden de cobro (fuerza_ventas.cat_estados_cobro)']
    observaciones           text          [note: 'Instrucciones y notas para la gestión de campo']

    // Auditoría General
    is_activo               boolean       [not null, default: `true`, note: 'Estado lógico del registro']
    created_at              timestamptz   [not null, default: `now()`, note: 'Fecha de creación']
    updated_at              timestamptz   [not null, default: `now()`, note: 'Fecha de última actualización']
    deleted_at              timestamptz   [note: 'Fecha de eliminación lógica']
    created_by              uuid          [note: 'Usuario creador']
    updated_by              uuid          [note: 'Usuario modificador']

    Indexes {
        (empresa_id, id) [unique, name: 'uq_trx_avisos_cobro_empresa_id']
        (empresa_id, codigo_aviso) [unique, name: 'uq_trx_avisos_cobro_empresa_codigo']
        (empresa_id, is_activo) [name: 'ix_trx_avisos_cobro_empresa_activo']
        (empresa_id, estado_cobro_id) [name: 'ix_trx_avisos_cobro_empresa_estado']
        (empresa_id, promotor_id, estado_cobro_id) [name: 'ix_trx_avisos_cobro_empresa_promotor_estado']
        (empresa_id, cliente_id) [name: 'ix_trx_avisos_cobro_empresa_cliente']
        (empresa_id, fecha_emision) [name: 'ix_trx_avisos_cobro_empresa_fecha']
    }
}

Ref: fuerza_ventas.trx_avisos_cobro.(empresa_id, promotor_id) > fuerza_ventas.mae_promotores.(empresa_id, id)
Ref: fuerza_ventas.trx_avisos_cobro.(empresa_id, cliente_id) > clientes.mae_clientes.(empresa_id, id)
Ref: fuerza_ventas.trx_avisos_cobro.(empresa_id, estado_cobro_id) > fuerza_ventas.cat_estados_cobro.(empresa_id, id)
CREATE TABLE IF NOT EXISTS fuerza_ventas.trx_avisos_cobro (
    id UUID NOT NULL DEFAULT gen_random_uuid(),
    empresa_id BIGINT NOT NULL,
    promotor_id UUID NOT NULL,
    cliente_id UUID NOT NULL,
    codigo_aviso VARCHAR(30) NOT NULL,
    fecha_emision DATE NOT NULL,
    fecha_limite_gestion DATE NOT NULL,
    monto_total_gestion NUMERIC(12,2) NOT NULL DEFAULT 0.00,
    monto_cobrado_acumulado NUMERIC(12,2) NOT NULL DEFAULT 0.00,
    estado_cobro_id UUID NOT NULL,
    observaciones TEXT,

    -- Auditoría General
    is_activo BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at TIMESTAMPTZ,
    created_by UUID,
    updated_by UUID,

    CONSTRAINT pk_trx_avisos_cobro
        PRIMARY KEY (id),

    CONSTRAINT uq_trx_avisos_cobro_empresa_id
        UNIQUE (empresa_id, id),

    CONSTRAINT uq_trx_avisos_cobro_empresa_codigo
        UNIQUE (empresa_id, codigo_aviso),

    CONSTRAINT fk_trx_avisos_cobro_mae_promotores
        FOREIGN KEY (empresa_id, promotor_id)
        REFERENCES fuerza_ventas.mae_promotores (empresa_id, id),

    CONSTRAINT fk_trx_avisos_cobro_mae_clientes
        FOREIGN KEY (empresa_id, cliente_id)
        REFERENCES clientes.mae_clientes (empresa_id, id),

    CONSTRAINT fk_trx_avisos_cobro_estado
        FOREIGN KEY (empresa_id, estado_cobro_id)
        REFERENCES fuerza_ventas.cat_estados_cobro (empresa_id, id)
);

CREATE INDEX IF NOT EXISTS ix_trx_avisos_cobro_empresa_activo
    ON fuerza_ventas.trx_avisos_cobro (empresa_id, is_activo);

CREATE INDEX IF NOT EXISTS ix_trx_avisos_cobro_empresa_estado
    ON fuerza_ventas.trx_avisos_cobro (empresa_id, estado_cobro_id);

CREATE INDEX IF NOT EXISTS ix_trx_avisos_cobro_empresa_promotor_estado
    ON fuerza_ventas.trx_avisos_cobro (empresa_id, promotor_id, estado_cobro_id);

CREATE INDEX IF NOT EXISTS ix_trx_avisos_cobro_empresa_cliente
    ON fuerza_ventas.trx_avisos_cobro (empresa_id, cliente_id);

CREATE INDEX IF NOT EXISTS ix_trx_avisos_cobro_empresa_fecha
    ON fuerza_ventas.trx_avisos_cobro (empresa_id, fecha_emision);

COMMENT ON TABLE fuerza_ventas.trx_avisos_cobro IS
    'Avisos de Cobro y Órdenes de Cobranza (Cabecera): Programación de cobros asignados a los promotores en campo para recaudación de facturas pendientes.';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.id IS
    'Identificador único UUID';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.empresa_id IS
    'Identificador UUID de la empresa en core.identidad (RLS)';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.promotor_id IS
    'Promotor asignado a la cobranza en campo';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.cliente_id IS
    'Cliente deudor';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.codigo_aviso IS
    'Código correlativo único del aviso de cobro';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.fecha_emision IS
    'Fecha de generación de la orden de cobro';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.fecha_limite_gestion IS
    'Fecha máxima estipulada para realizar la cobranza';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.monto_total_gestion IS
    'Monto total programado a cobrar';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.monto_cobrado_acumulado IS
    'Monto total recaudado en campo';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.estado_cobro_id IS
    'Estado de la orden de cobro (fuerza_ventas.cat_estados_cobro)';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.observaciones IS
    'Instrucciones y notas para la gestión de campo';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.is_activo IS
    'Estado lógico del registro';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.created_at IS
    'Fecha de creación';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.updated_at IS
    'Fecha de última actualización';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.deleted_at IS
    'Fecha de eliminación lógica';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.created_by IS
    'Usuario creador';

COMMENT ON COLUMN fuerza_ventas.trx_avisos_cobro.updated_by IS
    'Usuario modificador';

ALTER TABLE fuerza_ventas.trx_avisos_cobro
    ENABLE ROW LEVEL SECURITY;

CREATE POLICY rls_trx_avisos_cobro_empresa
    ON fuerza_ventas.trx_avisos_cobro
    FOR ALL
    USING (
        empresa_id = current_setting(
            'app.current_empresa_id',
            true
        )::BIGINT
    )
    WITH CHECK (
        empresa_id = current_setting(
            'app.current_empresa_id',
            true
        )::BIGINT
    );
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Optional

from sqlalchemy import (
    BigInteger,
    Boolean,
    Date,
    ForeignKeyConstraint,
    Index,
    Numeric,
    String,
    Text,
    UniqueConstraint,
    text,
)
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ, UUID
from sqlalchemy.orm import Mapped, mapped_column

from app.db.base import Base


class TrxAvisosCobro(Base):
    """
    Avisos de Cobro y Órdenes de Cobranza (Cabecera).

    Programación de cobros asignados a los promotores en campo
    para recaudación de facturas pendientes de clientes.

    Legacy:
        c213t_amortizacion_cliente / vi_aviso_cobro
    """

    __tablename__ = "trx_avisos_cobro"

    __table_args__ = (
        UniqueConstraint(
            "empresa_id",
            "id",
            name="uq_trx_avisos_cobro_empresa_id",
        ),
        UniqueConstraint(
            "empresa_id",
            "codigo_aviso",
            name="uq_trx_avisos_cobro_empresa_codigo",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "promotor_id"],
            ["fuerza_ventas.mae_promotores.empresa_id", "fuerza_ventas.mae_promotores.id"],
            name="fk_trx_avisos_cobro_mae_promotores",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "cliente_id"],
            ["clientes.mae_clientes.empresa_id", "clientes.mae_clientes.id"],
            name="fk_trx_avisos_cobro_mae_clientes",
        ),
        ForeignKeyConstraint(
            ["empresa_id", "estado_cobro_id"],
            ["fuerza_ventas.cat_estados_cobro.empresa_id", "fuerza_ventas.cat_estados_cobro.id"],
            name="fk_trx_avisos_cobro_estado",
        ),
        Index(
            "ix_trx_avisos_cobro_empresa_activo",
            "empresa_id",
            "is_activo",
        ),
        Index(
            "ix_trx_avisos_cobro_empresa_estado",
            "empresa_id",
            "estado_cobro_id",
        ),
        Index(
            "ix_trx_avisos_cobro_empresa_promotor_estado",
            "empresa_id",
            "promotor_id",
            "estado_cobro_id",
        ),
        Index(
            "ix_trx_avisos_cobro_empresa_cliente",
            "empresa_id",
            "cliente_id",
        ),
        Index(
            "ix_trx_avisos_cobro_empresa_fecha",
            "empresa_id",
            "fecha_emision",
        ),
        {
            "schema": "fuerza_ventas",
            "comment": "Avisos de Cobro y Órdenes de Cobranza (Cabecera)",
        },
    )

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        primary_key=True,
        server_default=text("gen_random_uuid()"),
        comment="Identificador único UUID",
    )

    empresa_id: Mapped[int] = mapped_column(
        BigInteger,
        nullable=False,
        comment="Identificador UUID de la empresa en core.identidad (RLS)",
    )

    promotor_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Promotor asignado a la cobranza en campo",
    )

    cliente_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Cliente deudor",
    )

    codigo_aviso: Mapped[str] = mapped_column(
        String(30),
        nullable=False,
        comment="Código correlativo único del aviso de cobro",
    )

    fecha_emision: Mapped[date] = mapped_column(
        Date,
        nullable=False,
        comment="Fecha de generación de la orden de cobro",
    )

    fecha_limite_gestion: Mapped[date] = mapped_column(
        Date,
        nullable=False,
        comment="Fecha máxima estipulada para realizar la cobranza",
    )

    monto_total_gestion: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        server_default=text("0.00"),
        comment="Monto total programado a cobrar",
    )

    monto_cobrado_acumulado: Mapped[Decimal] = mapped_column(
        Numeric(12, 2),
        nullable=False,
        server_default=text("0.00"),
        comment="Monto total recaudado en campo",
    )

    estado_cobro_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        comment="Estado de la orden de cobro (fuerza_ventas.cat_estados_cobro)",
    )

    observaciones: Mapped[Optional[str]] = mapped_column(
        Text,
        nullable=True,
        comment="Instrucciones y notas para la gestión de campo",
    )

    is_activo: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        server_default=text("true"),
        comment="Estado lógico del registro",
    )

    created_at: Mapped[datetime] = mapped_column(
        TIMESTAMPTZ,
        nullable=False,
        server_default=text("now()"),
        comment="Fecha de creación",
    )

    updated_at: Mapped[datetime] = mapped_column(
        TIMESTAMPTZ,
        nullable=False,
        server_default=text("now()"),
        comment="Fecha de última actualización",
    )

    deleted_at: Mapped[Optional[datetime]] = mapped_column(
        TIMESTAMPTZ,
        nullable=True,
        comment="Fecha de eliminación lógica",
    )

    created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Usuario creador",
    )

    updated_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        nullable=True,
        comment="Usuario modificador",
    )
"""create trx_avisos_cobro

Revision ID: trx_0006
Revises: trx_0005
Create Date: 2026-09-09
"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


# revision identifiers, used by Alembic.
revision: str = "trx_0006"
down_revision: Union[str, Sequence[str], None] = "trx_0005"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:

    op.create_table(
        "trx_avisos_cobro",

        sa.Column(
            "id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            server_default=sa.text("gen_random_uuid()"),
            comment="Identificador único UUID",
        ),

        sa.Column(
            "empresa_id",
            sa.BigInteger(),
            nullable=False,
            comment="Identificador UUID de la empresa en core.identidad (RLS)",
        ),

        sa.Column(
            "promotor_id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            comment="Promotor asignado a la cobranza en campo",
        ),

        sa.Column(
            "cliente_id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            comment="Cliente deudor",
        ),

        sa.Column(
            "codigo_aviso",
            sa.String(length=30),
            nullable=False,
            comment="Código correlativo único del aviso de cobro",
        ),

        sa.Column(
            "fecha_emision",
            sa.Date(),
            nullable=False,
            comment="Fecha de generación de la orden de cobro",
        ),

        sa.Column(
            "fecha_limite_gestion",
            sa.Date(),
            nullable=False,
            comment="Fecha máxima estipulada para realizar la cobranza",
        ),

        sa.Column(
            "monto_total_gestion",
            sa.Numeric(precision=12, scale=2),
            nullable=False,
            server_default=sa.text("0.00"),
            comment="Monto total programado a cobrar",
        ),

        sa.Column(
            "monto_cobrado_acumulado",
            sa.Numeric(precision=12, scale=2),
            nullable=False,
            server_default=sa.text("0.00"),
            comment="Monto total recaudado en campo",
        ),

        sa.Column(
            "estado_cobro_id",
            postgresql.UUID(as_uuid=True),
            nullable=False,
            comment="Estado de la orden de cobro (fuerza_ventas.cat_estados_cobro)",
        ),

        sa.Column(
            "observaciones",
            sa.Text(),
            nullable=True,
            comment="Instrucciones y notas para la gestión de campo",
        ),

        # Auditoría General
        sa.Column(
            "is_activo",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("true"),
            comment="Estado lógico del registro",
        ),

        sa.Column(
            "created_at",
            sa.TIMESTAMP(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
            comment="Fecha de creación",
        ),

        sa.Column(
            "updated_at",
            sa.TIMESTAMP(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
            comment="Fecha de última actualización",
        ),

        sa.Column(
            "deleted_at",
            sa.TIMESTAMP(timezone=True),
            nullable=True,
            comment="Fecha de eliminación lógica",
        ),

        sa.Column(
            "created_by",
            postgresql.UUID(as_uuid=True),
            nullable=True,
            comment="Usuario creador",
        ),

        sa.Column(
            "updated_by",
            postgresql.UUID(as_uuid=True),
            nullable=True,
            comment="Usuario modificador",
        ),

        sa.PrimaryKeyConstraint(
            "id",
            name="pk_trx_avisos_cobro",
        ),

        sa.UniqueConstraint(
            "empresa_id",
            "id",
            name="uq_trx_avisos_cobro_empresa_id",
        ),

        sa.UniqueConstraint(
            "empresa_id",
            "codigo_aviso",
            name="uq_trx_avisos_cobro_empresa_codigo",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "promotor_id"],
            ["fuerza_ventas.mae_promotores.empresa_id", "fuerza_ventas.mae_promotores.id"],
            name="fk_trx_avisos_cobro_mae_promotores",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "cliente_id"],
            ["clientes.mae_clientes.empresa_id", "clientes.mae_clientes.id"],
            name="fk_trx_avisos_cobro_mae_clientes",
        ),

        sa.ForeignKeyConstraint(
            ["empresa_id", "estado_cobro_id"],
            ["fuerza_ventas.cat_estados_cobro.empresa_id", "fuerza_ventas.cat_estados_cobro.id"],
            name="fk_trx_avisos_cobro_estado",
        ),

        comment="Avisos de Cobro y Órdenes de Cobranza (Cabecera)",
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_avisos_cobro_empresa_activo",
        "trx_avisos_cobro",
        ["empresa_id", "is_activo"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_avisos_cobro_empresa_estado",
        "trx_avisos_cobro",
        ["empresa_id", "estado_cobro_id"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_avisos_cobro_empresa_promotor_estado",
        "trx_avisos_cobro",
        ["empresa_id", "promotor_id", "estado_cobro_id"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_avisos_cobro_empresa_cliente",
        "trx_avisos_cobro",
        ["empresa_id", "cliente_id"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.create_index(
        "ix_trx_avisos_cobro_empresa_fecha",
        "trx_avisos_cobro",
        ["empresa_id", "fecha_emision"],
        unique=False,
        schema="fuerza_ventas",
    )

    op.execute(
        """
        ALTER TABLE fuerza_ventas.trx_avisos_cobro
        ENABLE ROW LEVEL SECURITY;
        """
    )

    op.execute(
        """
        CREATE POLICY rls_trx_avisos_cobro_empresa
        ON fuerza_ventas.trx_avisos_cobro
        FOR ALL
        USING (
            empresa_id = current_setting(
                'app.current_empresa_id',
                true
            )::BIGINT
        )
        WITH CHECK (
            empresa_id = current_setting(
                'app.current_empresa_id',
                true
            )::BIGINT
        );
        """
    )


def downgrade() -> None:

    op.execute(
        """
        DROP POLICY IF EXISTS rls_trx_avisos_cobro_empresa
        ON fuerza_ventas.trx_avisos_cobro;
        """
    )

    op.execute(
        """
        ALTER TABLE fuerza_ventas.trx_avisos_cobro
        DISABLE ROW LEVEL SECURITY;
        """
    )

    op.drop_index(
        "ix_trx_avisos_cobro_empresa_fecha",
        table_name="trx_avisos_cobro",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_avisos_cobro_empresa_cliente",
        table_name="trx_avisos_cobro",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_avisos_cobro_empresa_promotor_estado",
        table_name="trx_avisos_cobro",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_avisos_cobro_empresa_estado",
        table_name="trx_avisos_cobro",
        schema="fuerza_ventas",
    )

    op.drop_index(
        "ix_trx_avisos_cobro_empresa_activo",
        table_name="trx_avisos_cobro",
        schema="fuerza_ventas",
    )

    op.drop_table(
        "trx_avisos_cobro",
        schema="fuerza_ventas",
    )