Especificación Técnica: trx_pedidos_detalle¶
Esquema: pedidos
Base de Datos: erp
Tabla Legacy Origen: c066t_detalle_pedido
Propósito: Desglose ítem por ítem de los productos solicitados en el pedido, con alta precisión decimal en precio unitario (NUMERIC(14,4)), cantidades fraccionables (NUMERIC(12,4)) y cálculo tributario de descuentos e impuestos.
1. Justificación y Mejoras de Arquitectura¶
- Precisión Numérica Extendida: Incorpora
precio_unitario NUMERIC(14,4)y cantidadesNUMERIC(12,4)para evitar errores de redondeo en operaciones de mayoreo y conversión de unidades comerciales. - Integridad Referencial Compuesta Multi-Tenant: Clave única compuesta
(empresa_id, id)y clave foránea compuesta(empresa_id, pedido_id)haciapedidos.trx_pedidos(empresa_id, id)con eliminación en cascada (ON DELETE CASCADE). - Integración Desacoplada de Inventario: Los campos
producto_idyalmacen_idcorrelacionan lógicamente con el módulo de inventarios/operaciones del ERP mediante identificadores UUID sin claves foráneas físicas entre microservicios. - Aislamiento Multi-Tenant (RLS): Integra
empresa_id BIGINT NOT NULLcon políticas RLS obligatorias (USINGyWITH CHECK). - Unicidad de Líneas de Pedido: Restricción
UNIQUE (empresa_id, pedido_id, numero_linea)para garantizar la correlación secuencial ordenada de cada renglón comercial. - 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 pedidos.trx_pedidos_detalle {
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)']
pedido_id uuid [not null, note: 'Cabecera de la orden de pedido asociada']
numero_linea int [not null, note: 'Número secuencial de renglón (1, 2, 3...)']
producto_id uuid [not null, note: 'Correlación lógica con operaciones.mae_productos (sin FK física)']
almacen_id uuid [not null, note: 'Correlación lógica con operaciones.mae_almacenes (sin FK física)']
cantidad_pedida numeric(12,4) [not null, note: 'Cantidad ordenada en unidad base']
cantidad_aprobada numeric(12,4) [not null, note: 'Cantidad autorizada por crédito e inventario']
cantidad_despachada numeric(12,4) [not null, default: `0.0000`, note: 'Cantidad despachada real acumulada']
precio_unitario numeric(14,4) [not null, note: 'Precio unitario pactado con 4 decimales de precisión']
porcentaje_descuento numeric(5,2) [not null, default: `0.00`, note: 'Porcentaje de descuento otorgado']
monto_descuento_linea numeric(12,2) [not null, default: `0.00`, note: 'Monto monetario de descuento en la línea']
monto_impuesto_linea numeric(12,2) [not null, default: `0.00`, note: 'Monto de tributos aplicables a la línea']
monto_total_linea numeric(12,2) [not null, note: 'Importe neto final de la línea']
// 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_pedidos_det_empresa_id']
(empresa_id, pedido_id, numero_linea) [unique, name: 'uq_trx_pedidos_det_empresa_pedido_linea']
(empresa_id, is_activo) [name: 'ix_trx_pedidos_det_empresa_activo']
(empresa_id, pedido_id) [name: 'ix_trx_pedidos_det_empresa_pedido']
(empresa_id, producto_id) [name: 'ix_trx_pedidos_det_empresa_producto']
(empresa_id, almacen_id) [name: 'ix_trx_pedidos_det_empresa_almacen']
}
}
Ref: pedidos.trx_pedidos_detalle.(empresa_id, pedido_id) > pedidos.trx_pedidos.(empresa_id, id) [delete: cascade]
CREATE TABLE IF NOT EXISTS pedidos.trx_pedidos_detalle (
id UUID NOT NULL DEFAULT gen_random_uuid(),
empresa_id BIGINT NOT NULL,
pedido_id UUID NOT NULL,
numero_linea INT NOT NULL,
producto_id UUID NOT NULL,
almacen_id UUID NOT NULL,
cantidad_pedida NUMERIC(12,4) NOT NULL,
cantidad_aprobada NUMERIC(12,4) NOT NULL,
cantidad_despachada NUMERIC(12,4) NOT NULL DEFAULT 0.0000,
precio_unitario NUMERIC(14,4) NOT NULL,
porcentaje_descuento NUMERIC(5,2) NOT NULL DEFAULT 0.00,
monto_descuento_linea NUMERIC(12,2) NOT NULL DEFAULT 0.00,
monto_impuesto_linea NUMERIC(12,2) NOT NULL DEFAULT 0.00,
monto_total_linea NUMERIC(12,2) NOT NULL,
-- 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_pedidos_detalle
PRIMARY KEY (id),
CONSTRAINT uq_trx_pedidos_det_empresa_id
UNIQUE (empresa_id, id),
CONSTRAINT uq_trx_pedidos_det_empresa_pedido_linea
UNIQUE (empresa_id, pedido_id, numero_linea),
CONSTRAINT fk_trx_pedidos_det_pedido
FOREIGN KEY (empresa_id, pedido_id)
REFERENCES pedidos.trx_pedidos (empresa_id, id)
ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS ix_trx_pedidos_det_empresa_activo
ON pedidos.trx_pedidos_detalle (empresa_id, is_activo);
CREATE INDEX IF NOT EXISTS ix_trx_pedidos_det_empresa_pedido
ON pedidos.trx_pedidos_detalle (empresa_id, pedido_id);
CREATE INDEX IF NOT EXISTS ix_trx_pedidos_det_empresa_producto
ON pedidos.trx_pedidos_detalle (empresa_id, producto_id);
CREATE INDEX IF NOT EXISTS ix_trx_pedidos_det_empresa_almacen
ON pedidos.trx_pedidos_detalle (empresa_id, almacen_id);
COMMENT ON TABLE pedidos.trx_pedidos_detalle IS
'Líneas de Detalle de Órdenes de Pedido: Desglose ítem por ítem de los productos solicitados en el pedido, con alta precisión en precio unitario (NUMERIC(14,4)) y descuentos.';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.id IS
'Identificador único UUID';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.empresa_id IS
'Identificador UUID de la empresa en core.identidad (RLS)';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.pedido_id IS
'Cabecera de la orden de pedido asociada';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.numero_linea IS
'Número secuencial de renglón (1, 2, 3...)';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.producto_id IS
'Correlación lógica con operaciones.mae_productos (sin FK física)';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.almacen_id IS
'Correlación lógica con operaciones.mae_almacenes (sin FK física)';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.cantidad_pedida IS
'Cantidad ordenada en unidad base';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.cantidad_aprobada IS
'Cantidad autorizada por crédito e inventario';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.cantidad_despachada IS
'Cantidad despachada real acumulada';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.precio_unitario IS
'Precio unitario pactado con 4 decimales de precisión';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.porcentaje_descuento IS
'Porcentaje de descuento otorgado';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.monto_descuento_linea IS
'Monto monetario de descuento en la línea';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.monto_impuesto_linea IS
'Monto de tributos aplicables a la línea';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.monto_total_linea IS
'Importe neto final de la línea';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.is_activo IS
'Estado lógico del registro';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.created_at IS
'Fecha de creación';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.updated_at IS
'Fecha de última actualización';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.deleted_at IS
'Fecha de eliminación lógica';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.created_by IS
'Usuario creador';
COMMENT ON COLUMN pedidos.trx_pedidos_detalle.updated_by IS
'Usuario modificador';
ALTER TABLE pedidos.trx_pedidos_detalle
ENABLE ROW LEVEL SECURITY;
CREATE POLICY rls_trx_pedidos_det_empresa
ON pedidos.trx_pedidos_detalle
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 datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import (
BigInteger,
Boolean,
ForeignKeyConstraint,
Index,
Integer,
Numeric,
UniqueConstraint,
text,
)
from sqlalchemy.dialects.postgresql import TIMESTAMPTZ, UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class TrxPedidosDetalle(Base):
"""
Líneas de Detalle de Órdenes de Pedido.
Desglose ítem por ítem de los productos solicitados en el pedido,
con alta precisión en precio unitario (NUMERIC(14,4)) y descuentos.
Legacy:
c066t_detalle_pedido
"""
__tablename__ = "trx_pedidos_detalle"
__table_args__ = (
UniqueConstraint(
"empresa_id",
"id",
name="uq_trx_pedidos_det_empresa_id",
),
UniqueConstraint(
"empresa_id",
"pedido_id",
"numero_linea",
name="uq_trx_pedidos_det_empresa_pedido_linea",
),
ForeignKeyConstraint(
["empresa_id", "pedido_id"],
["pedidos.trx_pedidos.empresa_id", "pedidos.trx_pedidos.id"],
ondelete="CASCADE",
name="fk_trx_pedidos_det_pedido",
),
Index(
"ix_trx_pedidos_det_empresa_activo",
"empresa_id",
"is_activo",
),
Index(
"ix_trx_pedidos_det_empresa_pedido",
"empresa_id",
"pedido_id",
),
Index(
"ix_trx_pedidos_det_empresa_producto",
"empresa_id",
"producto_id",
),
Index(
"ix_trx_pedidos_det_empresa_almacen",
"empresa_id",
"almacen_id",
),
{
"schema": "pedidos",
"comment": "Líneas de Detalle de Órdenes de Pedido",
},
)
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)",
)
pedido_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
comment="Cabecera de la orden de pedido asociada",
)
numero_linea: Mapped[int] = mapped_column(
Integer,
nullable=False,
comment="Número secuencial de renglón (1, 2, 3...)",
)
producto_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
comment="Correlación lógica con operaciones.mae_productos (sin FK física)",
)
almacen_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
comment="Correlación lógica con operaciones.mae_almacenes (sin FK física)",
)
cantidad_pedida: Mapped[Decimal] = mapped_column(
Numeric(12, 4),
nullable=False,
comment="Cantidad ordenada en unidad base",
)
cantidad_aprobada: Mapped[Decimal] = mapped_column(
Numeric(12, 4),
nullable=False,
comment="Cantidad autorizada por crédito e inventario",
)
cantidad_despachada: Mapped[Decimal] = mapped_column(
Numeric(12, 4),
nullable=False,
server_default=text("0.0000"),
comment="Cantidad despachada real acumulada",
)
precio_unitario: Mapped[Decimal] = mapped_column(
Numeric(14, 4),
nullable=False,
comment="Precio unitario pactado con 4 decimales de precisión",
)
porcentaje_descuento: Mapped[Decimal] = mapped_column(
Numeric(5, 2),
nullable=False,
server_default=text("0.00"),
comment="Porcentaje de descuento otorgado",
)
monto_descuento_linea: Mapped[Decimal] = mapped_column(
Numeric(12, 2),
nullable=False,
server_default=text("0.00"),
comment="Monto monetario de descuento en la línea",
)
monto_impuesto_linea: Mapped[Decimal] = mapped_column(
Numeric(12, 2),
nullable=False,
server_default=text("0.00"),
comment="Monto de tributos aplicables a la línea",
)
monto_total_linea: Mapped[Decimal] = mapped_column(
Numeric(12, 2),
nullable=False,
comment="Importe neto final de la línea",
)
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_pedidos_detalle
Revision ID: erp_0002
Revises: erp_0001
Create Date: 2026-09-10
"""
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 = "erp_0002"
down_revision: Union[str, Sequence[str], None] = "erp_0001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"trx_pedidos_detalle",
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(
"pedido_id",
postgresql.UUID(as_uuid=True),
nullable=False,
comment="Cabecera de la orden de pedido asociada",
),
sa.Column(
"numero_linea",
sa.Integer(),
nullable=False,
comment="Número secuencial de renglón (1, 2, 3...)",
),
sa.Column(
"producto_id",
postgresql.UUID(as_uuid=True),
nullable=False,
comment="Correlación lógica con operaciones.mae_productos (sin FK física)",
),
sa.Column(
"almacen_id",
postgresql.UUID(as_uuid=True),
nullable=False,
comment="Correlación lógica con operaciones.mae_almacenes (sin FK física)",
),
sa.Column(
"cantidad_pedida",
sa.Numeric(precision=12, scale=4),
nullable=False,
comment="Cantidad ordenada en unidad base",
),
sa.Column(
"cantidad_aprobada",
sa.Numeric(precision=12, scale=4),
nullable=False,
comment="Cantidad autorizada por crédito e inventario",
),
sa.Column(
"cantidad_despachada",
sa.Numeric(precision=12, scale=4),
nullable=False,
server_default=sa.text("0.0000"),
comment="Cantidad despachada real acumulada",
),
sa.Column(
"precio_unitario",
sa.Numeric(precision=14, scale=4),
nullable=False,
comment="Precio unitario pactado con 4 decimales de precisión",
),
sa.Column(
"porcentaje_descuento",
sa.Numeric(precision=5, scale=2),
nullable=False,
server_default=sa.text("0.00"),
comment="Porcentaje de descuento otorgado",
),
sa.Column(
"monto_descuento_linea",
sa.Numeric(precision=12, scale=2),
nullable=False,
server_default=sa.text("0.00"),
comment="Monto monetario de descuento en la línea",
),
sa.Column(
"monto_impuesto_linea",
sa.Numeric(precision=12, scale=2),
nullable=False,
server_default=sa.text("0.00"),
comment="Monto de tributos aplicables a la línea",
),
sa.Column(
"monto_total_linea",
sa.Numeric(precision=12, scale=2),
nullable=False,
comment="Importe neto final de la línea",
),
# 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_pedidos_detalle",
),
sa.UniqueConstraint(
"empresa_id",
"id",
name="uq_trx_pedidos_det_empresa_id",
),
sa.UniqueConstraint(
"empresa_id",
"pedido_id",
"numero_linea",
name="uq_trx_pedidos_det_empresa_pedido_linea",
),
sa.ForeignKeyConstraint(
["empresa_id", "pedido_id"],
["pedidos.trx_pedidos.empresa_id", "pedidos.trx_pedidos.id"],
ondelete="CASCADE",
name="fk_trx_pedidos_det_pedido",
),
comment="Líneas de Detalle de Órdenes de Pedido",
schema="pedidos",
)
op.create_index(
"ix_trx_pedidos_det_empresa_activo",
"trx_pedidos_detalle",
["empresa_id", "is_activo"],
unique=False,
schema="pedidos",
)
op.create_index(
"ix_trx_pedidos_det_empresa_pedido",
"trx_pedidos_detalle",
["empresa_id", "pedido_id"],
unique=False,
schema="pedidos",
)
op.create_index(
"ix_trx_pedidos_det_empresa_producto",
"trx_pedidos_detalle",
["empresa_id", "producto_id"],
unique=False,
schema="pedidos",
)
op.create_index(
"ix_trx_pedidos_det_empresa_almacen",
"trx_pedidos_detalle",
["empresa_id", "almacen_id"],
unique=False,
schema="pedidos",
)
op.execute(
"""
ALTER TABLE pedidos.trx_pedidos_detalle
ENABLE ROW LEVEL SECURITY;
"""
)
op.execute(
"""
CREATE POLICY rls_trx_pedidos_det_empresa
ON pedidos.trx_pedidos_detalle
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_pedidos_det_empresa
ON pedidos.trx_pedidos_detalle;
"""
)
op.execute(
"""
ALTER TABLE pedidos.trx_pedidos_detalle
DISABLE ROW LEVEL SECURITY;
"""
)
op.drop_index(
"ix_trx_pedidos_det_empresa_almacen",
table_name="trx_pedidos_detalle",
schema="pedidos",
)
op.drop_index(
"ix_trx_pedidos_det_empresa_producto",
table_name="trx_pedidos_detalle",
schema="pedidos",
)
op.drop_index(
"ix_trx_pedidos_det_empresa_pedido",
table_name="trx_pedidos_detalle",
schema="pedidos",
)
op.drop_index(
"ix_trx_pedidos_det_empresa_activo",
table_name="trx_pedidos_detalle",
schema="pedidos",
)
op.drop_table(
"trx_pedidos_detalle",
schema="pedidos",
)