feat: plantilla base workspace SaaS
Convierte el repositorio de Anexo76 en una plantilla limpia y reutilizable para nuevos proyectos del ecosistema Workspace de Aduanasoft. Cambios principales: - Elimina módulos específicos de Anexo76: a76, a24, sitar, public - Agrega módulo example/ con patrón CRUD de referencia (models/dto/service/routes) - Limpia migraciones Alembic: solo quedan las 6 de core (users, tenants, permissions) - Reemplaza todas las rutas del dashboard con stubs genéricos - Elimina lógica de negocio aduanera: shortcuts, CSV imports, permisos, catálogos - Simplifica variables de entorno: una sola WORKSPACE_URL deriva Hub y Keycloak - Agrega scripts/auth-mode.sh para alternar entre auth local y workspace - Configura docker-compose con nombres genéricos (app-*) - Corrige flujo SSO: elimina system-gate SCAF/SCAII que bloqueaba el login - Modo DEV_LOCAL_AUTH para desarrollo sin Keycloak ni Hub Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,477 +4,21 @@ Revision ID: 8c9bad3da37f
|
||||
Revises: 9db46c604463
|
||||
Create Date: 2026-05-01 22:01:50.174319
|
||||
|
||||
Nota: la migración original sembraba catálogos de referencia de Anexo 76.
|
||||
En la plantilla este paso es un no-op — agrega tus seeds aquí si los necesitas.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.seed import (
|
||||
seed as code_pedimento_regimens_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.containers.seed import (
|
||||
seed as container_types_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.countries.seed import seed as countries_seed
|
||||
from api.v1.modules.public.reference_data.currency_types.seed import (
|
||||
seed as currency_types_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.customs_sections.seed import (
|
||||
seed as customs_sections_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.customs_warehouses.seed import (
|
||||
seed as customs_warehouses_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.incoterms.seed import seed as incoterms_seed
|
||||
from api.v1.modules.public.reference_data.invoice_types.seed import (
|
||||
seed as invoice_types_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.material_types.seed import (
|
||||
seed as material_types_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.payment_methods.seed import (
|
||||
seed as payment_methods_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.pedimento_codes.seed import (
|
||||
seed as pedimento_codes_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.pedimento_regimens.seed import (
|
||||
seed as pedimento_regimens_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.states.seed import seed as states_seed
|
||||
from api.v1.modules.public.reference_data.transport_modes.seed import (
|
||||
seed as transport_modes_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.pedimento_transport_catalog.seed import (
|
||||
seed as pedimento_transport_catalog_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.transport_types.seed import (
|
||||
seed as transport_types_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.valuation_methods.seed import (
|
||||
seed as valuation_methods_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed import (
|
||||
seed as units_of_measure_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_ace import (
|
||||
seed as ace_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_oma import (
|
||||
seed as oma_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_ame import (
|
||||
seed as ame_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_adua import (
|
||||
seed as adua_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.seed import (
|
||||
seed as tariff_fractions_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.trailer_types.seed import (
|
||||
seed as trailer_types_seed,
|
||||
)
|
||||
from api.v1.modules.core.permissions.seed_v2 import registry
|
||||
|
||||
from api.v1.modules.public.reference_data.license_exceptions.seed import seed_license_exceptions
|
||||
from api.v1.modules.public.reference_data.agency_tariff_codes.seed import seed_agency_tariff_codes
|
||||
from api.v1.modules.public.reference_data.identifiers.seed import seed_identifiers
|
||||
from api.v1.modules.public.reference_data.carta_porte_codes.seed import seed_carta_porte
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "8c9bad3da37f"
|
||||
down_revision: Union[str, Sequence[str], None] = "9db46c604463"
|
||||
down_revision: Union[str, None] = "9db46c604463"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Catálogos de referencia y permisos base (datos)."""
|
||||
|
||||
def format_value(val):
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
return "NULL"
|
||||
return f"'{str(val).replace(chr(39), chr(39)*2)}'"
|
||||
|
||||
# --- SEEDS PUBLIC (Tablas base) ---
|
||||
values_pc = ", ".join(
|
||||
[
|
||||
f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for code, desc in pedimento_codes_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO pedimento_codes (code, description) VALUES
|
||||
{values_pc}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_pr = ", ".join(
|
||||
[
|
||||
f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for code, desc in pedimento_regimens_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.pedimento_regimens (code, description) VALUES
|
||||
{values_pr}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_cpr = ", ".join(
|
||||
[
|
||||
f"('{ped_code}', '{reg_code}', '{type_code}')"
|
||||
for ped_code, reg_code, type_code in code_pedimento_regimens_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.code_pedimento_regimens (pedimento_code, regimen_code, type_code) VALUES
|
||||
{values_cpr}
|
||||
"""
|
||||
)
|
||||
|
||||
values_c = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for key, desc in container_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.containers (key, description) VALUES
|
||||
{values_c}
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_country = ", ".join(
|
||||
[
|
||||
f"('{m3_key}', '{mex_key}', '{ame_key}', '{desc_es.replace(chr(39), chr(39)*2)}', '{desc_en.replace(chr(39), chr(39)*2)}')"
|
||||
for m3_key, mex_key, ame_key, desc_es, desc_en in countries_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.countries (m3_key, mex_key, ame_key, description_es, description_en) VALUES
|
||||
{values_country}
|
||||
ON CONFLICT (m3_key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_ct = ", ".join(
|
||||
[
|
||||
f"('{code}', '{currency_name.replace(chr(39), chr(39)*2)}', '{country_desc.replace(chr(39), chr(39)*2)}')"
|
||||
for code, currency_name, country_desc in currency_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.currency_types (code, currency_name, country_description) VALUES
|
||||
{values_ct}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_cs = ", ".join(
|
||||
[
|
||||
f"('{code}', '{name.replace(chr(39), chr(39)*2)}')"
|
||||
for code, name in customs_sections_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.customs_sections (customs_code, section_name) VALUES
|
||||
{values_cs}
|
||||
ON CONFLICT (customs_code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_cw = ", ".join(
|
||||
[
|
||||
f"('{key}', '{customs.replace(chr(39), chr(39)*2)}', '{fiscalized_warehouse.replace(chr(39), chr(39)*2)}')"
|
||||
for key, customs, fiscalized_warehouse in customs_warehouses_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.customs_warehouses (key, customs, fiscalized_warehouse)
|
||||
VALUES
|
||||
{values_cw}
|
||||
ON CONFLICT (key, customs) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_incoterms = ", ".join(
|
||||
[
|
||||
f"('{code}', '{desc_es.replace(chr(39), chr(39)*2)}', '{desc_en.replace(chr(39), chr(39)*2)}')"
|
||||
for code, desc_es, desc_en in incoterms_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.incoterms (code, description_es, description_en) VALUES
|
||||
{values_incoterms}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_it = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}', '{operation.replace(chr(39), chr(39)*2)}')"
|
||||
for key, desc, note, type, operation in invoice_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.invoice_types (key, description, note, type, operation) VALUES
|
||||
{values_it}
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_mt = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{category.replace(chr(39), chr(39)*2)}')"
|
||||
for key, desc, category in material_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.material_types (key, description, type) VALUES
|
||||
{values_mt}
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_pm = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for key, desc in payment_methods_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.payment_methods (key, description) VALUES
|
||||
{values_pm}
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_tm = ", ".join(
|
||||
[
|
||||
f"('{key}', '{name.replace(chr(39), chr(39)*2)}')"
|
||||
for key, name in transport_modes_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.transport_modes (key, name) VALUES
|
||||
{values_tm}
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_ptc = ", ".join(
|
||||
[
|
||||
f"('{code}', '{en.replace(chr(39), chr(39)*2)}', '{es.replace(chr(39), chr(39)*2)}', '{pdc}')"
|
||||
for code, en, es, pdc in pedimento_transport_catalog_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.pedimento_transport_catalog (code, transport_en, transport_es, payment_date_code) VALUES
|
||||
{values_ptc}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_tt = ", ".join(
|
||||
[
|
||||
f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for code, desc in transport_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.transport_types (transport_code, description) VALUES
|
||||
{values_tt}
|
||||
ON CONFLICT (transport_code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_trailer = ", ".join(
|
||||
[
|
||||
f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for code, desc in trailer_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.trailer_type (trailer_type_key, description) VALUES
|
||||
{values_trailer}
|
||||
ON CONFLICT (trailer_type_key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_vm = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for key, desc in valuation_methods_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.valuation_methods (key, description) VALUES
|
||||
{values_vm}
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
# --- SEEDS A76 (Unidades de Medida) ---
|
||||
val_ace = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in ace_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_ace (code, description) VALUES {val_ace} ON CONFLICT ON CONSTRAINT uq_uom_ace_code DO NOTHING;"
|
||||
)
|
||||
|
||||
val_oma = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in oma_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_oma (code, description) VALUES {val_oma} ON CONFLICT ON CONSTRAINT uq_uom_oma_code DO NOTHING;"
|
||||
)
|
||||
|
||||
val_ame = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in ame_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_american (code, description) VALUES {val_ame} ON CONFLICT ON CONSTRAINT uq_uom_american_code DO NOTHING;"
|
||||
)
|
||||
|
||||
val_adua = ", ".join(
|
||||
[f"({format_value(code)}, {format_value(desc)}, {format_value(a76_code)})" for code, desc, a76_code in adua_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_customs (code, description, a76_unit_code) VALUES {val_adua} ON CONFLICT ON CONSTRAINT uq_uom_customs_code DO NOTHING;"
|
||||
)
|
||||
|
||||
additional_customs = set()
|
||||
additional_american = set()
|
||||
additional_ace = set()
|
||||
additional_oma = set()
|
||||
|
||||
existing_customs = {code for code, desc, a76_code in adua_seed}
|
||||
existing_american = {c for c, d in ame_seed}
|
||||
existing_ace = {c for c, d in ace_seed}
|
||||
existing_oma = {c for c, d in oma_seed}
|
||||
|
||||
for code, desc, desc_en, customs, american, ace, oma in units_of_measure_seed:
|
||||
if customs and customs.strip() and customs not in existing_customs:
|
||||
additional_customs.add((customs, f"Auto-generated from {code}"))
|
||||
if american and american.strip() and american not in existing_american:
|
||||
additional_american.add((american, f"Auto-generated from {code}"))
|
||||
if ace and ace.strip() and ace not in existing_ace:
|
||||
additional_ace.add((ace, f"Auto-generated from {code}"))
|
||||
if oma and oma.strip() and oma not in existing_oma:
|
||||
additional_oma.add((oma, f"Auto-generated from {code}"))
|
||||
|
||||
if additional_customs:
|
||||
val_add_customs = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)}, NULL)" for c, d in additional_customs]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_customs (code, description, a76_unit_code) VALUES {val_add_customs} ON CONFLICT ON CONSTRAINT uq_uom_customs_code DO NOTHING;"
|
||||
)
|
||||
|
||||
if additional_american:
|
||||
val_add_american = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in additional_american]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_american (code, description) VALUES {val_add_american} ON CONFLICT ON CONSTRAINT uq_uom_american_code DO NOTHING;"
|
||||
)
|
||||
|
||||
if additional_ace:
|
||||
val_add_ace = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in additional_ace]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_ace (code, description) VALUES {val_add_ace} ON CONFLICT ON CONSTRAINT uq_uom_ace_code DO NOTHING;"
|
||||
)
|
||||
|
||||
if additional_oma:
|
||||
val_add_oma = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in additional_oma]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_oma (code, description) VALUES {val_add_oma} ON CONFLICT ON CONSTRAINT uq_uom_oma_code DO NOTHING;"
|
||||
)
|
||||
|
||||
# --- SEEDS CORE (Permissions) ---
|
||||
all_permissions = registry.get_all()
|
||||
|
||||
values_permissions = ", ".join(
|
||||
[
|
||||
f"({format_value(p.code)}, {format_value(p.description)}, {format_value(p.module)}, {format_value(p.action)})"
|
||||
for p in all_permissions
|
||||
]
|
||||
)
|
||||
|
||||
if values_permissions:
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO core.permissions (code, description, module, action)
|
||||
VALUES {values_permissions}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_states = ", ".join(
|
||||
[
|
||||
f"('{m3_key}', '{description.replace(chr(39), chr(39)*2)}', {format_value(mex_key)})"
|
||||
for m3_key, description, mex_key in states_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.states (m3_key, description, mex_key)
|
||||
VALUES {values_states}
|
||||
ON CONFLICT (m3_key, description) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_tariff_fractions = ", ".join(
|
||||
[
|
||||
f"({format_value(code)}, {format_value(fraction)}, {format_value(description)}, "
|
||||
f"{format_value(nico)}, {format_value(umt)}, {format_value(adv_impo)}, {format_value(adv_expo)})"
|
||||
for code, fraction, description, nico, umt, adv_impo, adv_expo in tariff_fractions_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO a76.tariff_fractions (code, fraction, description, nico, umt, adv_impo, adv_expo)
|
||||
VALUES {values_tariff_fractions}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
seed_license_exceptions(session)
|
||||
seed_agency_tariff_codes(session)
|
||||
seed_identifiers(session)
|
||||
seed_carta_porte(session)
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Los datos de catálogo no se revierten automáticamente."""
|
||||
pass
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""add system column to classes and parts
|
||||
|
||||
Revision ID: a7b8c9d0e1f2
|
||||
Revises: d2e3f4a5b6c7
|
||||
Create Date: 2026-05-26 10:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "a7b8c9d0e1f2"
|
||||
down_revision: Union[str, None] = "d2e3f4a5b6c7"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Columna system en a76.classes — indica si la clase pertenece a SCAF (fixed_asset) o SCAII (inventory)
|
||||
op.add_column(
|
||||
"classes",
|
||||
sa.Column("system", sa.String(length=12), nullable=False, server_default="fixed_asset"),
|
||||
schema="a76",
|
||||
)
|
||||
# Columna system en a76.parts — misma discriminación por sistema
|
||||
op.add_column(
|
||||
"parts",
|
||||
sa.Column("system", sa.String(length=12), nullable=False, server_default="fixed_asset"),
|
||||
schema="a76",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("classes", "system", schema="a76")
|
||||
op.drop_column("parts", "system", schema="a76")
|
||||
@@ -1,79 +0,0 @@
|
||||
"""split rfc and tax_id in clients_and_providers
|
||||
|
||||
Separa el identificador fiscal en dos columnas:
|
||||
- rfc: RFC mexicano (nacional)
|
||||
- tax_id: identificador fiscal extranjero
|
||||
|
||||
Antes, ambos compartían la columna `rfc` discriminados por `type_nat_foreign`.
|
||||
La migración mueve el valor de los extranjeros (type_nat_foreign='E') de `rfc` a
|
||||
`tax_id` y deja `rfc` en NULL para esos registros. Los nacionales no se tocan.
|
||||
|
||||
Revision ID: b3c4d5e6f7a8
|
||||
Revises: a7b8c9d0e1f2
|
||||
Create Date: 2026-06-02 10:00:00.000000
|
||||
|
||||
"""
|
||||
import logging
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "b3c4d5e6f7a8"
|
||||
down_revision: Union[str, None] = "a7b8c9d0e1f2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
logger = logging.getLogger("alembic.runtime.migration")
|
||||
|
||||
# Extranjero ⟺ type_nat_foreign empieza con 'E' (misma convención que DTOs/modelo/frontend).
|
||||
_FOREIGN_PREDICATE = "UPPER(COALESCE(type_nat_foreign, '')) LIKE 'E%'"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1) Nueva columna tax_id (nullable: un registro puede no tener identificador extranjero).
|
||||
op.add_column(
|
||||
"clients_and_providers",
|
||||
sa.Column("tax_id", sa.String(length=30), nullable=True),
|
||||
schema="a76",
|
||||
)
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
# 2) Conteo previo (visibilidad antes de mover datos; UPDATE precedido de SELECT COUNT).
|
||||
to_move = conn.execute(
|
||||
sa.text(
|
||||
"SELECT COUNT(*) FROM a76.clients_and_providers "
|
||||
f"WHERE {_FOREIGN_PREDICATE} AND rfc IS NOT NULL"
|
||||
)
|
||||
).scalar()
|
||||
logger.info(
|
||||
"split_rfc_tax_id: %s registros extranjeros con rfc serán movidos a tax_id",
|
||||
to_move,
|
||||
)
|
||||
|
||||
# 3) Mover rfc -> tax_id para extranjeros (idempotente por el guard tax_id IS NULL).
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE a76.clients_and_providers SET tax_id = rfc "
|
||||
f"WHERE {_FOREIGN_PREDICATE} AND tax_id IS NULL AND rfc IS NOT NULL"
|
||||
)
|
||||
)
|
||||
# 4) Limpiar rfc en extranjeros (la columna rfc queda solo para RFC nacional).
|
||||
conn.execute(
|
||||
sa.text(
|
||||
f"UPDATE a76.clients_and_providers SET rfc = NULL WHERE {_FOREIGN_PREDICATE}"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Reconsolidar: regresar el identificador extranjero a rfc antes de eliminar la columna.
|
||||
conn = op.get_bind()
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE a76.clients_and_providers SET rfc = tax_id "
|
||||
f"WHERE {_FOREIGN_PREDICATE} AND rfc IS NULL AND tax_id IS NOT NULL"
|
||||
)
|
||||
)
|
||||
op.drop_column("clients_and_providers", "tax_id", schema="a76")
|
||||
@@ -1,7 +1,7 @@
|
||||
"""add workspace profile fields to user_tenants
|
||||
|
||||
Revision ID: c3d4e5f6a7b
|
||||
Revises: ca7d3c4e8b2a
|
||||
Revises: b2c3d4e5f6a7
|
||||
Create Date: 2026-05-08 00:00:00.000000
|
||||
|
||||
"""
|
||||
@@ -12,7 +12,7 @@ import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "c3d4e5f6a7b"
|
||||
down_revision: Union[str, None] = "ca7d3c4e8b2a"
|
||||
down_revision: Union[str, None] = "b2c3d4e5f6a7"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""add stock_unit_of_measure to classes
|
||||
|
||||
Revision ID: c8d9e0f1a2b3
|
||||
Revises: b3c4d5e6f7a8
|
||||
Create Date: 2026-06-02 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "c8d9e0f1a2b3"
|
||||
down_revision: Union[str, None] = "b3c4d5e6f7a8"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"classes",
|
||||
sa.Column("stock_unit_of_measure", sa.String(length=5), nullable=True),
|
||||
schema="a76",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_classes_stock_uom",
|
||||
"classes",
|
||||
"units_of_measure",
|
||||
["stock_unit_of_measure", "tenant_id", "company_id"],
|
||||
["code", "tenant_id", "company_id"],
|
||||
source_schema="a76",
|
||||
referent_schema="a76",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint(
|
||||
"fk_classes_stock_uom",
|
||||
"classes",
|
||||
schema="a76",
|
||||
type_="foreignkey",
|
||||
)
|
||||
op.drop_column("classes", "stock_unit_of_measure", schema="a76")
|
||||
@@ -1,67 +0,0 @@
|
||||
"""add transportation id sequences
|
||||
|
||||
Revision ID: ca7d3c4e8b2a
|
||||
Revises: b2c3d4e5f6a7
|
||||
Create Date: 2026-05-08 08:34:00.000000
|
||||
|
||||
Crea las secuencias `a76.<tabla>_<id>_seq` que los servicios de catálogos de
|
||||
transporte (transporter, vehicle, driver, trailer) referencian vía
|
||||
`SELECT nextval(...)`. La migración es idempotente y reposiciona cada
|
||||
secuencia a `MAX(<id>)` para no chocar con filas pre-existentes.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "ca7d3c4e8b2a"
|
||||
down_revision: Union[str, None] = "b2c3d4e5f6a7"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
# (tabla, columna_id_sustituto) en el esquema a76. El nombre de secuencia
|
||||
# resultante es `a76.<tabla>_<columna>_seq`, que es el patrón que los
|
||||
# servicios ya consumen (ver transporters/vehicles/drivers/trailers services).
|
||||
_SEQUENCE_TARGETS = (
|
||||
("transporter", "transporter_id"),
|
||||
("vehicle", "vehicle_id"),
|
||||
("driver", "driver_id"),
|
||||
("trailer", "trailer_id"),
|
||||
)
|
||||
|
||||
|
||||
def _seq_name(table: str, column: str) -> str:
|
||||
return f"{table}_{column}_seq"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for table, column in _SEQUENCE_TARGETS:
|
||||
seq = _seq_name(table, column)
|
||||
op.execute(
|
||||
f'CREATE SEQUENCE IF NOT EXISTS a76."{seq}" '
|
||||
f"AS BIGINT START WITH 1 INCREMENT BY 1"
|
||||
)
|
||||
# Reposicionar la secuencia al MAX(id) actual para evitar colisiones
|
||||
# con datos ya cargados antes de existir la secuencia.
|
||||
op.execute(
|
||||
f"SELECT setval('a76.\"{seq}\"', "
|
||||
f"GREATEST((SELECT COALESCE(MAX({column}), 0) FROM a76.{table}), 1), "
|
||||
f"true)"
|
||||
)
|
||||
op.execute(
|
||||
f'ALTER SEQUENCE a76."{seq}" OWNED BY a76.{table}.{column}'
|
||||
)
|
||||
op.execute(
|
||||
f"ALTER TABLE a76.{table} "
|
||||
f"ALTER COLUMN {column} SET DEFAULT nextval('a76.\"{seq}\"')"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for table, column in _SEQUENCE_TARGETS:
|
||||
seq = _seq_name(table, column)
|
||||
op.execute(
|
||||
f"ALTER TABLE a76.{table} ALTER COLUMN {column} DROP DEFAULT"
|
||||
)
|
||||
op.execute(f'DROP SEQUENCE IF EXISTS a76."{seq}"')
|
||||
@@ -1,57 +0,0 @@
|
||||
"""seed document_types_digitization
|
||||
|
||||
Revision ID: d2e3f4a5b6c7
|
||||
Revises: f1a2b3c4d5e6
|
||||
Create Date: 2026-05-25
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.v1.modules.a76.doc_types_dig.seed import seed as doc_types_dig_seed
|
||||
|
||||
revision: str = "d2e3f4a5b6c7"
|
||||
down_revision: Union[str, None] = "f1a2b3c4d5e6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Quitar columna active (no se usa en ningún flujo)
|
||||
op.drop_column('document_types_digitization', 'active', schema='a76')
|
||||
|
||||
def format_value(val):
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
return "NULL"
|
||||
return f"'{str(val).replace(chr(39), chr(39)*2)}'"
|
||||
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
companies = session.execute(
|
||||
text("SELECT tenant_id, id FROM a76.company WHERE deleted_at IS NULL")
|
||||
).fetchall()
|
||||
|
||||
for tenant_id, company_id in companies:
|
||||
values = ", ".join([
|
||||
f"({format_value(code)}, {format_value(description)}, {tenant_id}, {company_id})"
|
||||
for code, description in doc_types_dig_seed
|
||||
])
|
||||
if values:
|
||||
session.execute(text(f"""
|
||||
INSERT INTO a76.document_types_digitization (code, description, tenant_id, company_id)
|
||||
VALUES {values}
|
||||
ON CONFLICT (tenant_id, company_id, code) DO NOTHING;
|
||||
"""))
|
||||
session.commit()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column(
|
||||
'document_types_digitization',
|
||||
sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.text('true')),
|
||||
schema='a76',
|
||||
)
|
||||
@@ -1,63 +0,0 @@
|
||||
"""seed sectors for existing companies
|
||||
|
||||
Siembra el catálogo de sectores PROSEC (a76.sectors) para todas las empresas
|
||||
existentes. Los sectores son por empresa y solo se sembraban dentro de
|
||||
_seed_company_data() al crear una empresa; las empresas que no pasaron por ese
|
||||
flujo (BD recreada o creadas por otra vía) quedaban sin sectores y el selector
|
||||
del frontend aparecía vacío. Idempotente: ON CONFLICT DO NOTHING.
|
||||
|
||||
Revision ID: e1f2a3b4c5d6
|
||||
Revises: c8d9e0f1a2b3
|
||||
Create Date: 2026-06-02 14:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.sectors.seed import seed as sectors_seed
|
||||
|
||||
revision: str = "e1f2a3b4c5d6"
|
||||
down_revision: Union[str, None] = "c8d9e0f1a2b3"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
def format_value(val):
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
return "NULL"
|
||||
return f"'{str(val).replace(chr(39), chr(39) * 2)}'"
|
||||
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
companies = session.execute(
|
||||
text("SELECT tenant_id, id FROM a76.company WHERE deleted_at IS NULL")
|
||||
).fetchall()
|
||||
|
||||
for tenant_id, company_id in companies:
|
||||
values = ", ".join(
|
||||
[
|
||||
f"({format_value(key)}, {format_value(description)}, {str(authorized).upper()}, {tenant_id}, {company_id})"
|
||||
for key, description, authorized in sectors_seed
|
||||
]
|
||||
)
|
||||
if values:
|
||||
session.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO a76.sectors (key, description, authorized, tenant_id, company_id)
|
||||
VALUES {values}
|
||||
ON CONFLICT (key, tenant_id, company_id) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Backfill de datos: no se eliminan sectores en el downgrade porque podrían
|
||||
# haberse editado/agregado manualmente tras la siembra. No-op intencional.
|
||||
pass
|
||||
@@ -1,30 +0,0 @@
|
||||
"""add incoterm DAF legacy catalog row
|
||||
|
||||
Revision ID: e4f5a6b7c8d9
|
||||
Revises: c3d4e5f6a7b
|
||||
Create Date: 2026-05-08
|
||||
|
||||
Incoterm histórico DAF (Delivered At Frontier) para CSV y referencias legacy.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "e4f5a6b7c8d9"
|
||||
down_revision: Union[str, None] = "c3d4e5f6a7b"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO public.incoterms (code, description_es, description_en) VALUES
|
||||
('DAF', 'ENTREGADO EN FRONTERA', 'DELIVERED AT FRONTIER')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DELETE FROM public.incoterms WHERE code = 'DAF';")
|
||||
@@ -1,75 +0,0 @@
|
||||
"""isolate equivalency items
|
||||
|
||||
Revision ID: f1a2b3c4d5e6
|
||||
Revises: e4f5a6b7c8d9
|
||||
Create Date: 2026-05-12 17:18:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "f1a2b3c4d5e6"
|
||||
down_revision: Union[str, None] = "e4f5a6b7c8d9"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Add equivalency_id column
|
||||
op.add_column(
|
||||
'equivalency_items',
|
||||
sa.Column(
|
||||
'equivalency_id',
|
||||
sa.Integer(),
|
||||
sa.ForeignKey('a76.equivalencies.id', ondelete='CASCADE'),
|
||||
nullable=True
|
||||
),
|
||||
schema='a76'
|
||||
)
|
||||
|
||||
# 2. Drop old unique constraint
|
||||
# Note: In some DBs we might need to specify the type, but for Postgres 'unique' or 'foreignkey' works.
|
||||
op.drop_constraint('uq_equivalency_item_fields', 'equivalency_items', schema='a76', type_='unique')
|
||||
|
||||
# 3. Create new unique constraint including equivalency_id
|
||||
op.create_unique_constraint(
|
||||
'uq_equivalency_item_fields',
|
||||
'equivalency_items',
|
||||
['equivalency_id', 'original_field', 'external_field', 'tenant_id', 'company_id'],
|
||||
schema='a76'
|
||||
)
|
||||
|
||||
# 4. Increase identifier length in equivalencies table
|
||||
op.alter_column(
|
||||
'equivalencies',
|
||||
'identifier',
|
||||
type_=sa.String(50),
|
||||
existing_type=sa.String(10),
|
||||
schema='a76'
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 1. Drop new unique constraint
|
||||
op.drop_constraint('uq_equivalency_item_fields', 'equivalency_items', schema='a76', type_='unique')
|
||||
|
||||
# 2. Re-create old unique constraint
|
||||
op.create_unique_constraint(
|
||||
'uq_equivalency_item_fields',
|
||||
'equivalency_items',
|
||||
['original_field', 'external_field', 'tenant_id', 'company_id'],
|
||||
schema='a76'
|
||||
)
|
||||
|
||||
# 4. Revert identifier length in equivalencies table
|
||||
op.alter_column(
|
||||
'equivalencies',
|
||||
'identifier',
|
||||
type_=sa.String(10),
|
||||
existing_type=sa.String(50),
|
||||
schema='a76'
|
||||
)
|
||||
# 5. Drop equivalency_id column
|
||||
op.drop_column('equivalency_items', 'equivalency_id', schema='a76')
|
||||
@@ -1,7 +1,7 @@
|
||||
"""add invite codes
|
||||
|
||||
Revision ID: g2h3i4j5k6l7
|
||||
Revises: e1f2a3b4c5d6
|
||||
Revises: c3d4e5f6a7b
|
||||
Create Date: 2026-06-02 00:00:00.000000
|
||||
|
||||
"""
|
||||
@@ -11,7 +11,7 @@ import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "g2h3i4j5k6l7"
|
||||
down_revision: str = "e1f2a3b4c5d6"
|
||||
down_revision: str = "c3d4e5f6a7b"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user