feat(crm): Expediente — referencia única de trazabilidad del trámite (Fase D)
- Tabla crm.cases (expediente) con folio EXP2026-08-001 (next_folio entidad EXP,
sin dirección). Nace al crear la Oportunidad y se hereda vía case_id a
solicitud → cotización → operación → factura. advance_stage solo avanza.
- case_id (FK a crm.cases) en crm.opportunities/service_requests/quotes,
ops.shipments y fin.invoices; propagación en sus create_*. Migración
d4e5f6a7b8c9 reversible.
- Endpoints GET /v1/crm/cases, /cases/{id}, /cases/by-ref/{ref} con timeline
(historia completa para UI y otros sistemas).
- Frontend: casesAPI, ruta /dashboard/crm/expedientes (lista + timeline vertical),
chip "📁 Expediente" en solicitud/cotización, "Expedientes" en el sidebar.
- Consecutivo de folios sin tope (soporta >10,000,000/mes).
- 4 pruebas de expediente (minteo, propagación, timeline, no-retroceso). Suite en verde (113).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
75
backend/alembic/versions/d4e5f6a7b8c9_case_expediente.py
Normal file
75
backend/alembic/versions/d4e5f6a7b8c9_case_expediente.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Expediente (crm.cases) + case_id en el ciclo comercial
|
||||
|
||||
Revision ID: d4e5f6a7b8c9
|
||||
Revises: f0a1b2c3d4e5
|
||||
Create Date: 2026-08-07 02:00:00.000000
|
||||
|
||||
Crea crm.cases (expediente, hilo maestro con folio EXP...) y agrega case_id a
|
||||
crm.opportunities/service_requests/quotes, ops.shipments y fin.invoices.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "d4e5f6a7b8c9"
|
||||
down_revision: Union[str, None] = "f0a1b2c3d4e5"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
# (schema, tabla) donde se agrega case_id
|
||||
_CASE_FK_TABLES = [
|
||||
("crm", "opportunities"),
|
||||
("crm", "service_requests"),
|
||||
("crm", "quotes"),
|
||||
("ops", "shipments"),
|
||||
("fin", "invoices"),
|
||||
]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"cases",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("reference", sa.String(length=40), nullable=True),
|
||||
sa.Column("account_id", sa.Integer(), nullable=True),
|
||||
sa.Column("title", sa.String(length=255), nullable=True),
|
||||
sa.Column("stage", sa.String(length=20), nullable=False, server_default=sa.text("'oportunidad'")),
|
||||
sa.Column("status", sa.String(length=20), nullable=False, server_default=sa.text("'abierto'")),
|
||||
sa.Column("created_by", sa.String(length=64), nullable=True),
|
||||
sa.Column("updated_by", sa.String(length=64), nullable=True),
|
||||
sa.Column("tenant_id", sa.Integer(), nullable=False),
|
||||
sa.Column("company_id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("deleted_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.ForeignKeyConstraint(["tenant_id"], ["core.tenants.id"]),
|
||||
sa.ForeignKeyConstraint(["account_id"], ["crm.accounts.id"]),
|
||||
schema="crm",
|
||||
)
|
||||
op.create_index("ix_crm_cases_id", "cases", ["id"], schema="crm")
|
||||
op.create_index("ix_crm_cases_reference", "cases", ["reference"], schema="crm")
|
||||
op.create_index("ix_crm_cases_tenant_id", "cases", ["tenant_id"], schema="crm")
|
||||
op.create_index("ix_crm_cases_company_id", "cases", ["company_id"], schema="crm")
|
||||
op.create_index("ix_crm_cases_account_id", "cases", ["account_id"], schema="crm")
|
||||
op.create_index("ix_crm_cases_status", "cases", ["status"], schema="crm")
|
||||
|
||||
for schema, table in _CASE_FK_TABLES:
|
||||
op.add_column(table, sa.Column("case_id", sa.Integer(), nullable=True), schema=schema)
|
||||
op.create_index(f"ix_{schema}_{table}_case_id", table, ["case_id"], schema=schema)
|
||||
op.create_foreign_key(
|
||||
f"fk_{schema}_{table}_case_id", table, "cases",
|
||||
["case_id"], ["id"], source_schema=schema, referent_schema="crm",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for schema, table in _CASE_FK_TABLES:
|
||||
op.drop_constraint(f"fk_{schema}_{table}_case_id", table, schema=schema, type_="foreignkey")
|
||||
op.drop_index(f"ix_{schema}_{table}_case_id", table_name=table, schema=schema)
|
||||
op.drop_column(table, "case_id", schema=schema)
|
||||
|
||||
for idx in ("status", "account_id", "company_id", "tenant_id", "reference", "id"):
|
||||
op.drop_index(f"ix_crm_cases_{idx}", table_name="cases", schema="crm")
|
||||
op.drop_table("cases", schema="crm")
|
||||
Reference in New Issue
Block a user