Files
CRM_AGENTES_CARGA/backend/alembic/versions/d5e6f7a8b9c0_pdf_compliance.py
Aduanasoft e79705e6e3 feat(ops,fin,crm): reglas de negocio del PDF (decisiones, cierre, facturación, continuidad, RBAC)
Cierra los huecos de la auditoría contra "SOFTWARE PARA AGENTES DE CARGA":

- ops (Diag. 2/3): bitácora con puntos de decisión (kind=decision) y ciclo de
  corrección (parent_event_id/attempt) para ¿Cut Off? y ¿despacho autorizado?
  (R-E-05/13, R-I-06). Reprogramación de salida (previous_etd, R-E-06). Hitos
  operativos completos export/import. Cierre operativo con costos finales
  (close_shipment, R-E-22).
- fin (Diag. 4): facturación con gate por cierre operativo y sin duplicar
  (R-F-01), costos de operación arrastrados (ops_cost_total, R-F-02), envío con
  PDF generado y guardado en MinIO (send_invoice + pdf.py sin dependencias,
  R-F-05) y revisión del cliente (en_revision_cliente + aprobación, R-F-06).
- crm (Diag. 1): opportunity_id enlaza embudo→RFQ (R-C-02), contacto como etapa
  (first_contact_at, R-C-04), re-cotización (clone_quote + reopen, R-C-12).
- transversal: catálogo de Incoterms y participantes/actores incl. autoridad
  aduanera (R-T-01/10), enforcement de permisos por carril (RBAC) con roles
  sembrados y dependencias dev-safe (R-T-07).
- Migración d5e6f7a8b9c0 con downgrade. Seed extendido. 70 tests (12 nuevos).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:46:53 -06:00

93 lines
5.4 KiB
Python

"""Cumplimiento PDF agente de carga: decisiones/costos en ops, envío/revisión en fin,
continuidad comercial en crm (opportunity_id, contacto).
Revision ID: d5e6f7a8b9c0
Revises: c4d5e6f7a8b9
Create Date: 2026-07-15 00:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "d5e6f7a8b9c0"
down_revision: Union[str, None] = "c4d5e6f7a8b9"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ---------- ops.shipments: transporte terrestre, reprogramación y cierre operativo ----------
op.add_column("shipments", sa.Column("ground_carrier_supplier_id", sa.Integer(), nullable=True), schema="ops")
op.add_column("shipments", sa.Column("pickup_at", sa.DateTime(), nullable=True), schema="ops")
op.add_column("shipments", sa.Column("previous_etd", sa.Date(), nullable=True), schema="ops")
op.add_column("shipments", sa.Column("actual_cost_total", sa.Numeric(precision=14, scale=2), nullable=True), schema="ops")
op.add_column("shipments", sa.Column("cost_currency", sa.String(length=3), nullable=True), schema="ops")
op.add_column("shipments", sa.Column("closed_at", sa.DateTime(), nullable=True), schema="ops")
op.add_column("shipments", sa.Column("closed_by", sa.String(length=64), nullable=True), schema="ops")
op.create_foreign_key(
"fk_ops_shipments_ground_carrier_supplier_id", "shipments", "suppliers",
["ground_carrier_supplier_id"], ["id"], source_schema="ops", referent_schema="crm",
)
# ---------- ops.shipment_events: puntos de decisión y ciclo de corrección ----------
op.add_column("shipment_events", sa.Column("kind", sa.String(length=20), nullable=False, server_default=sa.text("'hito'")), schema="ops")
op.add_column("shipment_events", sa.Column("outcome", sa.String(length=20), nullable=True), schema="ops")
op.add_column("shipment_events", sa.Column("parent_event_id", sa.Integer(), nullable=True), schema="ops")
op.add_column("shipment_events", sa.Column("attempt", sa.Integer(), nullable=False, server_default=sa.text("1")), schema="ops")
op.create_foreign_key(
"fk_ops_shipment_events_parent_event_id", "shipment_events", "shipment_events",
["parent_event_id"], ["id"], source_schema="ops", referent_schema="ops",
)
# ---------- fin.invoices: costos de operación, PDF y revisión del cliente ----------
op.add_column("invoices", sa.Column("ops_cost_total", sa.Numeric(precision=14, scale=2), nullable=True), schema="fin")
op.add_column("invoices", sa.Column("pdf_file_key", sa.String(length=512), nullable=True), schema="fin")
op.add_column("invoices", sa.Column("client_reviewed_at", sa.DateTime(), nullable=True), schema="fin")
op.add_column("invoices", sa.Column("client_approved", sa.Boolean(), nullable=True), schema="fin")
op.add_column("invoices", sa.Column("review_notes", sa.Text(), nullable=True), schema="fin")
# ---------- crm.service_requests: continuidad comercial y contacto ----------
op.add_column("service_requests", sa.Column("opportunity_id", sa.Integer(), nullable=True), schema="crm")
op.add_column("service_requests", sa.Column("first_contact_at", sa.DateTime(), nullable=True), schema="crm")
op.add_column("service_requests", sa.Column("first_contact_notes", sa.Text(), nullable=True), schema="crm")
op.create_index("ix_crm_service_requests_opportunity_id", "service_requests", ["opportunity_id"], schema="crm")
op.create_foreign_key(
"fk_crm_service_requests_opportunity_id", "service_requests", "opportunities",
["opportunity_id"], ["id"], source_schema="crm", referent_schema="crm",
)
def downgrade() -> None:
# crm.service_requests
op.drop_constraint("fk_crm_service_requests_opportunity_id", "service_requests", schema="crm", type_="foreignkey")
op.drop_index("ix_crm_service_requests_opportunity_id", table_name="service_requests", schema="crm")
op.drop_column("service_requests", "first_contact_notes", schema="crm")
op.drop_column("service_requests", "first_contact_at", schema="crm")
op.drop_column("service_requests", "opportunity_id", schema="crm")
# fin.invoices
op.drop_column("invoices", "review_notes", schema="fin")
op.drop_column("invoices", "client_approved", schema="fin")
op.drop_column("invoices", "client_reviewed_at", schema="fin")
op.drop_column("invoices", "pdf_file_key", schema="fin")
op.drop_column("invoices", "ops_cost_total", schema="fin")
# ops.shipment_events
op.drop_constraint("fk_ops_shipment_events_parent_event_id", "shipment_events", schema="ops", type_="foreignkey")
op.drop_column("shipment_events", "attempt", schema="ops")
op.drop_column("shipment_events", "parent_event_id", schema="ops")
op.drop_column("shipment_events", "outcome", schema="ops")
op.drop_column("shipment_events", "kind", schema="ops")
# ops.shipments
op.drop_constraint("fk_ops_shipments_ground_carrier_supplier_id", "shipments", schema="ops", type_="foreignkey")
op.drop_column("shipments", "closed_by", schema="ops")
op.drop_column("shipments", "closed_at", schema="ops")
op.drop_column("shipments", "cost_currency", schema="ops")
op.drop_column("shipments", "actual_cost_total", schema="ops")
op.drop_column("shipments", "previous_etd", schema="ops")
op.drop_column("shipments", "pickup_at", schema="ops")
op.drop_column("shipments", "ground_carrier_supplier_id", schema="ops")