feat(fin,crm): catálogo c_UsoCFDI y claves fiscales del receptor
Cierra las decisiones pendientes 1 y 5. Agrega sat.cfdi_uses con su endpoint de solo lectura y amarra la ficha del cliente a los catálogos del SAT con crm.accounts.tax_regime_id y cfdi_use_id. Las columnas de texto libre tax_regime y cfdi_use se conservan intactas: la migración hace un backfill conservador que solo resuelve lo inequívoco (la clave del catálogo, o la descripción exacta sin distinguir mayúsculas ni espacios) y deja en NULL lo que no case, porque deducir el régimen de un receptor a partir de texto libre provoca CFDI rechazados. La UI muestra el texto anterior junto al selector para que el usuario elija la clave que corresponde. El selector de régimen se acota al tipo de persona de la cuenta, y el service valida ambas claves contra el catálogo. sync_catalogs ahora omite los catálogos cuya tabla todavía no existe: al correr el historial desde cero, la migración anterior la invoca antes de que se creen los catálogos agregados después. Las claves de c_UsoCFDI quedan pendientes de validación con el área Fiscal antes de producción, igual que el subset de c_ClaveProdServ; no se cargaron las banderas de persona física/moral ni la compatibilidad por régimen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Catálogo c_UsoCFDI y claves fiscales del receptor en crm.accounts.
|
||||
|
||||
Cierra las decisiones pendientes 1 y 5 del ticket de catálogos SAT: agrega
|
||||
``sat.cfdi_uses`` y amarra el régimen fiscal y el uso de CFDI de la cuenta a los
|
||||
catálogos, conservando las columnas de texto libre que ya existían.
|
||||
|
||||
Revision ID: f7a8b9c0d1e2
|
||||
Revises: e6f7a8b9c0d1
|
||||
Create Date: 2026-08-07 00:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from api.v1.modules.fin.catalogs.seed_data import sync_catalogs
|
||||
|
||||
revision: str = "f7a8b9c0d1e2"
|
||||
down_revision: Union[str, None] = "e6f7a8b9c0d1"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ---------- sat.cfdi_uses ----------
|
||||
op.create_table(
|
||||
"cfdi_uses",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("code", sa.String(length=4), nullable=False),
|
||||
sa.Column("description", sa.String(length=500), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
|
||||
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.PrimaryKeyConstraint("id"),
|
||||
schema="sat",
|
||||
)
|
||||
op.create_index("ix_sat_cfdi_uses_id", "cfdi_uses", ["id"], schema="sat")
|
||||
op.create_index("ix_sat_cfdi_uses_code", "cfdi_uses", ["code"], unique=True, schema="sat")
|
||||
|
||||
# sync_catalogs es idempotente: siembra c_UsoCFDI y deja intactos los catálogos
|
||||
# que ya sembró la migración anterior.
|
||||
sync_catalogs(op.get_bind())
|
||||
|
||||
# ---------- crm.accounts: claves fiscales del receptor ----------
|
||||
# Nullables: las cuentas existentes solo tienen el texto libre.
|
||||
op.add_column("accounts", sa.Column("tax_regime_id", sa.Integer(), nullable=True), schema="crm")
|
||||
op.add_column("accounts", sa.Column("cfdi_use_id", sa.Integer(), nullable=True), schema="crm")
|
||||
op.create_foreign_key(
|
||||
"fk_crm_accounts_tax_regime_id", "accounts", "tax_regimes",
|
||||
["tax_regime_id"], ["id"], source_schema="crm", referent_schema="sat",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_crm_accounts_cfdi_use_id", "accounts", "cfdi_uses",
|
||||
["cfdi_use_id"], ["id"], source_schema="crm", referent_schema="sat",
|
||||
)
|
||||
|
||||
# Backfill conservador: solo resuelve lo inequívoco. Se compara el texto libre
|
||||
# contra la clave del catálogo (p. ej. "601", "G03") y contra la descripción
|
||||
# exacta, sin distinguir mayúsculas ni espacios sobrantes. Lo que no case así se
|
||||
# queda en NULL para que lo revise el usuario: adivinar el régimen de un receptor
|
||||
# a partir de texto libre provoca CFDI rechazados.
|
||||
for column, catalog in [("tax_regime", "tax_regimes"), ("cfdi_use", "cfdi_uses")]:
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE crm.accounts AS a
|
||||
SET {column}_id = c.id
|
||||
FROM sat.{catalog} AS c
|
||||
WHERE a.{column}_id IS NULL
|
||||
AND a.{column} IS NOT NULL
|
||||
AND (
|
||||
upper(btrim(a.{column})) = upper(c.code)
|
||||
OR upper(btrim(a.{column})) = upper(c.description)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("fk_crm_accounts_cfdi_use_id", "accounts", schema="crm", type_="foreignkey")
|
||||
op.drop_constraint("fk_crm_accounts_tax_regime_id", "accounts", schema="crm", type_="foreignkey")
|
||||
op.drop_column("accounts", "cfdi_use_id", schema="crm")
|
||||
op.drop_column("accounts", "tax_regime_id", schema="crm")
|
||||
op.drop_table("cfdi_uses", schema="sat")
|
||||
Reference in New Issue
Block a user