T2026-08-047 feat(fin): catálogos SAT, conceptos de facturación y datos fiscales del emisor #5

Open
jcedilloAS wants to merge 10 commits from feature/AS-catalogos-sat-facturacion into feature/crm-cumplimiento-pdf
Member

feat(fin): catálogos SAT, conceptos y datos fiscales del emisor

Rama: feature/AS-catalogos-sat-facturacionbase: feature/crm-cumplimiento-pdf
Ticket: T2026-08-047
Tipo de actividad IA: DISEÑO DE PROMPTS IA


Qué se implementó

Schema sat — 9 catálogos globales de solo lectura

Tablas sin tenant_id ni company_id, sin CRUD y sin baja física (las claves que el
SAT retira se desactivan con is_active para no romper los CFDI históricos que las
referencian):

Tabla Catálogo Filas sembradas
sat.tax_regimes c_RegimenFiscal 19
sat.taxes c_Impuesto 3
sat.payment_forms c_FormaPago 22
sat.units_of_measure c_ClaveUnidad 21
sat.products_services c_ClaveProdServ 11
sat.voucher_types c_TipoDeComprobante 5
sat.payment_methods c_MetodoPago 2
sat.tax_objects c_ObjetoImp 4
sat.cfdi_uses c_UsoCFDI 24

Las semillas viven en backend/api/v1/modules/fin/catalogs/seed_data.py, no dentro de
la migración: corregir un dato del catálogo no debe exigir escribir una migración de
esquema nueva. sync_catalogs(connection) hace upsert por clave (inserta lo que falta,
actualiza descripción y banderas, nunca borra) y queda disponible para futuras
actualizaciones del catálogo.

fin.concepts — conceptos de facturación por empresa (CRUD)

Relación 1:1 con sat.products_services por empresa, garantizada por índice único
parcial (WHERE deleted_at IS NULL) y validada en el service para devolver 409
con mensaje en español en vez de un IntegrityError crudo. La baja lógica libera la
clave ProdServ y el código para un concepto nuevo. Las respuestas traen los objetos del
catálogo ya resueltos (selectin) para evitar N+1 en la UI.

fin.issuer_settings — datos fiscales del emisor

Razón social, RFC, régimen fiscal y CP del lugar de expedición. Una sola configuración
vigente por empresa (índice único parcial); GET + PUT (upsert), sin DELETE. El
RFC se valida con ^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$ y se normaliza a mayúsculas sin
espacios antes de aplicar el max_length, para que un RFC con espacios de sobra no
se rechace por longitud antes de limpiarlo.

Amarre a facturas

  • fin.invoices + voucher_type_id, payment_form_id, payment_method_id,
    expedition_zip_code (todas nullable).
  • fin.invoice_items + concept_id, product_service_id, unit_of_measure_id,
    tax_object_id (todas nullable).
  • Nueva fin.invoice_item_taxes para el detalle de impuestos trasladados y retenidos
    por partida. No interviene en el cálculo de subtotal/IVA/total, que sigue saliendo
    de invoices.tax_rate.
  • invoice_items.concept (texto libre) se conserva intacta y obligatoria porque la
    consume el PDF actual. Al crear una partida con concept_id sin concept, el service
    hereda ahí la descripción del concepto (recortada a 60 caracteres).

Migraciones

  • e6f7a8b9c0d1 (revises d5e6f7a8b9c0): crea el schema sat y sus 8 tablas
    iniciales con sus índices, siembra los catálogos llamando a
    sync_catalogs(op.get_bind()), crea fin.concepts, fin.issuer_settings y
    fin.invoice_item_taxes, y agrega las columnas y FKs nuevas. El downgrade() revierte
    en orden inverso y termina con DROP SCHEMA sat CASCADE.
  • f7a8b9c0d1e2 (revises e6f7a8b9c0d1): agrega sat.cfdi_uses, las FK fiscales de
    crm.accounts y el backfill del texto libre.

sync_catalogs omite los cat��logos cuya tabla todavía no existe: al correr el historial
desde cero, la primera migración la invoca antes de que exista sat.cfdi_uses, y cada
catálogo se siembra en la migración que lo crea.

Permisos

  • fin.concept.{view,create,edit,delete} (vía _ENTITIES).
  • fin.settings.view y fin.settings.edit, registrados a mano.
  • Los catálogos del SAT no llevan permiso propio: basta fin.access.

Tras el merge hay que sincronizar permisos: python -m core.permissions.sync_cli
(o el comando equivalente del proyecto, backend/api/v1/modules/core/permissions/sync_cli.py)
para que los permisos nuevos queden asignables desde Roles y permisos.

Frontend

  • src/lib/api/fin/{catalogs,concepts,issuer}.ts, reexportados desde index.ts. Los
    catálogos se cachean en un Map del módulo tras la primera carga.
  • /dashboard/fin/conceptos — tabla, buscador y filtro activo/inactivo. El alta y la
    edición viven en páginas dedicadas (/nuevo y /[id]), siguiendo el patrón que ya usa
    el CRM para proveedores y cuentas; los campos se comparten en
    $lib/components/fin/ConceptFields.svelte, con combobox de clave ProdServ desde 2
    caracteres y el 409 del backend mostrado junto al campo. Entrada en el sidebar con
    permiso fin.concept.view.
  • /dashboard/settings/facturacion — formulario del emisor con la misma validación de
    RFC que el backend. Si el GET responde 404 se abre en modo alta, no como error; el
    guardar se deshabilita sin fin.settings.edit. Enlazada desde el índice de
    configuración y desde el sidebar con permiso fin.settings.view.
  • /dashboard/fin/facturas/[id] — el selector de concepto de la partida se alimenta del
    catálogo de conceptos de la empresa (manda concept_id, precarga el precio unitario).
    Las claves genéricas que estaban fijas en el código quedan en un segundo grupo del
    mismo selector, marcadas como «sin clave del SAT», para no bloquear a las empresas sin
    catálogo. El listado etiqueta con la clave del catálogo cuando la partida la referencia
    y cae al texto libre en las facturas anteriores.
  • Todo en Svelte 5 con runes.

Cómo probarlo

# Backend — migración contra una BD local vacía
cd backend
alembic upgrade head
alembic downgrade -1 && alembic upgrade head

# Pruebas
pytest tests/ -q

# Frontend
cd ../frontend
npx vitest --project server --run src/lib/api/fin/catalogs.test.ts
npm run check

En la UI: Facturación → Conceptos (alta de un concepto, intentar repetir la clave
ProdServ → error junto al campo) y Configuración → Facturación (capturar RFC dummy
XAXX010101000, guardar, recargar).


Salida de las pruebas

Migración contra PostgreSQL limpio (contenedor desechable, no la BD de desarrollo):

INFO  [alembic.runtime.migration] Running upgrade d5e6f7a8b9c0 -> e6f7a8b9c0d1, Catálogos SAT (schema sat)...
--- downgrade -1 ---  OK
--- upgrade head ---  OK
--- downgrade base --- OK
--- upgrade head ---  OK

Conteo de semillas y índices parciales:

 sat.payment_forms = 22       sat.tax_objects = 4
 sat.payment_methods = 2      sat.tax_regimes = 19
 sat.products_services = 11   sat.taxes = 3
 sat.units_of_measure = 21    sat.voucher_types = 5
 sat.cfdi_uses = 24

 uq_fin_concepts_code
 uq_fin_concepts_product_service
 uq_fin_invoice_item_taxes
 uq_fin_issuer_settings_company

Idempotencia de sync_catalogs sobre PostgreSQL (2ª y 3ª corrida):

insertados 2a corrida: 0 | 3a corrida: 0
estable: True

Suite de pruebas del backend (70 previas + 36 nuevas):

106 passed in 6.82s

Frontend:

✓ |server| src/lib/api/fin/catalogs.test.ts (5 tests)
svelte-check found 38 errors and 17 warnings in 20 files   # idéntico al baseline de la rama base

Lint del backend (flake8 --max-line-length=140 sobre los archivos nuevos y
modificados): sin hallazgos. Los E501 que reporta fin/invoices/routes.py son previos
a este cambio; ese archivo no se tocó.


Decisiones cerradas en este PR

Las 7 decisiones que originalmente quedaban abiertas se resolvieron así:

# Tema Resolución
1 c_UsoCFDI Implementado. sat.cfdi_uses (24 claves) + GET /fin/catalogs/cfdi-uses + FK crm.accounts.cfdi_use_id. Las claves quedan marcadas pendiente validación Fiscal, igual que el subset de ProdServ.
2 c_ObjetoImp 05–07 No implementado — requiere el catálogo oficial. Ver abajo.
3 Claves ProdServ adicionales No implementado — requiere el catálogo oficial. Ver abajo.
4 Impuestos locales Resuelto por diseño: no se catalogan en sat.taxes. El ISH y similares viajan en el complemento Impuestos Locales con claves ajenas a c_Impuesto; mezclarlas rompería la unicidad del catálogo federal. is_local se conserva como bandera para cuando exista el catálogo del complemento, que es otro ticket.
5 Régimen fiscal del receptor Implementado. FK crm.accounts.tax_regime_id con backfill conservador; el texto libre se conserva.
6 company_id en los catálogos Resuelto por diseño: se mantiene heredando fin.access. La alternativa (montarlos fuera del router de fin) exigiría una dependencia de autorización propia para datos que igual solo consume el módulo de Facturación; no compensa. El cliente del frontend envía company_id.
7 Herencia de claves SAT a la partida Implementado. create_item y update_item heredan product_service_id, unit_of_measure_id y tax_object_id del concepto; lo que envía el cliente gana.

Backfill del régimen y uso de CFDI

La migración solo resuelve coincidencias inequívocas: el texto libre igual a la clave
del catálogo (601, G03) o igual a la descripción exacta, sin distinguir mayúsculas
ni espacios sobrantes. Lo que no case se queda en NULL y la ficha del cliente muestra
el texto capturado junto al selector, para que un humano elija la clave. No se deduce
el régimen de un receptor a partir de texto libre
: una clave equivocada provoca CFDI
rechazados.

Verificado contra PostgreSQL:

Con clave            regimen=601  uso=G03     # capturado como "601" / "Gastos en general"
Con descripcion      regimen=601  uso=G03     # capturado como la descripción completa
Sin coincidencia     regimen=NULL uso=NULL    # texto ambiguo → lo revisa el usuario

PENDIENTE DECISIÓN — bloqueado por datos, no por diseño

Quedan dos puntos abiertos. En ambos la estructura ya está lista y solo falta cargar
filas; no las escribí de memoria a propósito: una clave del SAT equivocada en un CFDI
es un problema fiscal real, y el ticket es explícito en que no se deducen ni se inventan.
Se cargan con sync_catalogs en cuanto Fiscal entregue las filas oficiales, sin migración
de esquema.

  1. c_ObjetoImp claves 05–07. Sembradas 01–04. Faltan las tres claves que
    incorporaron versiones posteriores del catálogo, con su descripción textual exacta.
    Lo que necesito: las 3 filas (clave + descripción) de la versión vigente.
  2. Claves c_ClaveProdServ adicionales. El subset de 11 claves no cubre servicios
    accesorios frecuentes del giro: maniobras, demoras/demurrage, custodia y servicios
    aduanales. Lo que necesito: la clave de 8 dígitos y su descripción para cada
    servicio que se vaya a facturar.

Además, dos validaciones que no bloquean el merge pero sí el paso a producción:

  • Las 24 claves de c_UsoCFDI y las 11 de c_ClaveProdServ ya sembradas
    requieren visto bueno de Fiscal antes de timbrar. Ambos bloques están marcados con ese
    comentario en seed_data.py.
  • No se cargó la matriz de compatibilidad de c_UsoCFDI (qué uso admite cada régimen
    y tipo de persona). Cambia entre versiones del catálogo y equivocarla provoca rechazos
    al timbrar; hoy la UI ofrece todos los usos. Si Fiscal la entrega, se agrega como
    columnas del catálogo igual que las banderas de c_RegimenFiscal.

Fuera de alcance (confirmado)

Generación del XML CFDI 4.0, sellado, timbrado con PAC, cancelaciones, complemento de
pago y Carta Porte. Tampoco se tocó el cálculo de totales de fin/invoices/service.py
ni se cargó el catálogo completo de c_ClaveProdServ (~52,000 claves).

🤖 Generated with Claude Code

# feat(fin): catálogos SAT, conceptos y datos fiscales del emisor **Rama:** `feature/AS-catalogos-sat-facturacion` → **base:** `feature/crm-cumplimiento-pdf` **Ticket:** T2026-08-047 **Tipo de actividad IA:** DISEÑO DE PROMPTS IA --- ## Qué se implementó ### Schema `sat` — 9 catálogos globales de solo lectura Tablas sin `tenant_id` ni `company_id`, sin CRUD y sin baja física (las claves que el SAT retira se desactivan con `is_active` para no romper los CFDI históricos que las referencian): | Tabla | Catálogo | Filas sembradas | |---|---|---| | `sat.tax_regimes` | `c_RegimenFiscal` | 19 | | `sat.taxes` | `c_Impuesto` | 3 | | `sat.payment_forms` | `c_FormaPago` | 22 | | `sat.units_of_measure` | `c_ClaveUnidad` | 21 | | `sat.products_services` | `c_ClaveProdServ` | 11 | | `sat.voucher_types` | `c_TipoDeComprobante` | 5 | | `sat.payment_methods` | `c_MetodoPago` | 2 | | `sat.tax_objects` | `c_ObjetoImp` | 4 | | `sat.cfdi_uses` | `c_UsoCFDI` | 24 | Las semillas viven en `backend/api/v1/modules/fin/catalogs/seed_data.py`, no dentro de la migración: corregir un dato del catálogo no debe exigir escribir una migración de esquema nueva. `sync_catalogs(connection)` hace upsert por clave (inserta lo que falta, actualiza descripción y banderas, **nunca borra**) y queda disponible para futuras actualizaciones del catálogo. ### `fin.concepts` — conceptos de facturación por empresa (CRUD) Relación **1:1 con `sat.products_services` por empresa**, garantizada por índice único parcial (`WHERE deleted_at IS NULL`) **y** validada en el service para devolver `409` con mensaje en español en vez de un `IntegrityError` crudo. La baja lógica libera la clave ProdServ y el código para un concepto nuevo. Las respuestas traen los objetos del catálogo ya resueltos (`selectin`) para evitar N+1 en la UI. ### `fin.issuer_settings` — datos fiscales del emisor Razón social, RFC, régimen fiscal y CP del lugar de expedición. Una sola configuración vigente por empresa (índice único parcial); `GET` + `PUT` (upsert), sin `DELETE`. El RFC se valida con `^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$` y se normaliza a mayúsculas sin espacios **antes** de aplicar el `max_length`, para que un RFC con espacios de sobra no se rechace por longitud antes de limpiarlo. ### Amarre a facturas - `fin.invoices` + `voucher_type_id`, `payment_form_id`, `payment_method_id`, `expedition_zip_code` (todas nullable). - `fin.invoice_items` + `concept_id`, `product_service_id`, `unit_of_measure_id`, `tax_object_id` (todas nullable). - Nueva `fin.invoice_item_taxes` para el detalle de impuestos trasladados y retenidos por partida. **No** interviene en el cálculo de subtotal/IVA/total, que sigue saliendo de `invoices.tax_rate`. - `invoice_items.concept` (texto libre) se conserva intacta y obligatoria porque la consume el PDF actual. Al crear una partida con `concept_id` sin `concept`, el service hereda ahí la descripción del concepto (recortada a 60 caracteres). ### Migraciones - **`e6f7a8b9c0d1`** (revises `d5e6f7a8b9c0`): crea el schema `sat` y sus 8 tablas iniciales con sus índices, siembra los catálogos llamando a `sync_catalogs(op.get_bind())`, crea `fin.concepts`, `fin.issuer_settings` y `fin.invoice_item_taxes`, y agrega las columnas y FKs nuevas. El `downgrade()` revierte en orden inverso y termina con `DROP SCHEMA sat CASCADE`. - **`f7a8b9c0d1e2`** (revises `e6f7a8b9c0d1`): agrega `sat.cfdi_uses`, las FK fiscales de `crm.accounts` y el backfill del texto libre. `sync_catalogs` omite los cat��logos cuya tabla todavía no existe: al correr el historial desde cero, la primera migración la invoca antes de que exista `sat.cfdi_uses`, y cada catálogo se siembra en la migración que lo crea. ### Permisos - `fin.concept.{view,create,edit,delete}` (vía `_ENTITIES`). - `fin.settings.view` y `fin.settings.edit`, registrados a mano. - Los catálogos del SAT no llevan permiso propio: basta `fin.access`. **Tras el merge hay que sincronizar permisos:** `python -m core.permissions.sync_cli` (o el comando equivalente del proyecto, `backend/api/v1/modules/core/permissions/sync_cli.py`) para que los permisos nuevos queden asignables desde Roles y permisos. ### Frontend - `src/lib/api/fin/{catalogs,concepts,issuer}.ts`, reexportados desde `index.ts`. Los catálogos se cachean en un `Map` del módulo tras la primera carga. - `/dashboard/fin/conceptos` — tabla, buscador y filtro activo/inactivo. El alta y la edición viven en páginas dedicadas (`/nuevo` y `/[id]`), siguiendo el patrón que ya usa el CRM para proveedores y cuentas; los campos se comparten en `$lib/components/fin/ConceptFields.svelte`, con combobox de clave ProdServ desde 2 caracteres y el `409` del backend mostrado junto al campo. Entrada en el sidebar con permiso `fin.concept.view`. - `/dashboard/settings/facturacion` — formulario del emisor con la misma validación de RFC que el backend. Si el `GET` responde `404` se abre en modo alta, no como error; el guardar se deshabilita sin `fin.settings.edit`. Enlazada desde el índice de configuración y desde el sidebar con permiso `fin.settings.view`. - `/dashboard/fin/facturas/[id]` — el selector de concepto de la partida se alimenta del catálogo de conceptos de la empresa (manda `concept_id`, precarga el precio unitario). Las claves genéricas que estaban fijas en el código quedan en un segundo grupo del mismo selector, marcadas como «sin clave del SAT», para no bloquear a las empresas sin catálogo. El listado etiqueta con la clave del catálogo cuando la partida la referencia y cae al texto libre en las facturas anteriores. - Todo en Svelte 5 con runes. --- ## Cómo probarlo ```bash # Backend — migración contra una BD local vacía cd backend alembic upgrade head alembic downgrade -1 && alembic upgrade head # Pruebas pytest tests/ -q # Frontend cd ../frontend npx vitest --project server --run src/lib/api/fin/catalogs.test.ts npm run check ``` En la UI: Facturación → Conceptos (alta de un concepto, intentar repetir la clave ProdServ → error junto al campo) y Configuración → Facturación (capturar RFC dummy `XAXX010101000`, guardar, recargar). --- ## Salida de las pruebas **Migración contra PostgreSQL limpio** (contenedor desechable, no la BD de desarrollo): ``` INFO [alembic.runtime.migration] Running upgrade d5e6f7a8b9c0 -> e6f7a8b9c0d1, Catálogos SAT (schema sat)... --- downgrade -1 --- OK --- upgrade head --- OK --- downgrade base --- OK --- upgrade head --- OK ``` **Conteo de semillas y índices parciales:** ``` sat.payment_forms = 22 sat.tax_objects = 4 sat.payment_methods = 2 sat.tax_regimes = 19 sat.products_services = 11 sat.taxes = 3 sat.units_of_measure = 21 sat.voucher_types = 5 sat.cfdi_uses = 24 uq_fin_concepts_code uq_fin_concepts_product_service uq_fin_invoice_item_taxes uq_fin_issuer_settings_company ``` **Idempotencia de `sync_catalogs` sobre PostgreSQL** (2ª y 3ª corrida): ``` insertados 2a corrida: 0 | 3a corrida: 0 estable: True ``` **Suite de pruebas del backend** (70 previas + 36 nuevas): ``` 106 passed in 6.82s ``` **Frontend:** ``` ✓ |server| src/lib/api/fin/catalogs.test.ts (5 tests) svelte-check found 38 errors and 17 warnings in 20 files # idéntico al baseline de la rama base ``` **Lint del backend** (`flake8 --max-line-length=140` sobre los archivos nuevos y modificados): sin hallazgos. Los `E501` que reporta `fin/invoices/routes.py` son previos a este cambio; ese archivo no se tocó. --- ## Decisiones cerradas en este PR Las 7 decisiones que originalmente quedaban abiertas se resolvieron así: | # | Tema | Resolución | |---|---|---| | 1 | `c_UsoCFDI` | **Implementado.** `sat.cfdi_uses` (24 claves) + `GET /fin/catalogs/cfdi-uses` + FK `crm.accounts.cfdi_use_id`. Las claves quedan marcadas *pendiente validación Fiscal*, igual que el subset de ProdServ. | | 2 | `c_ObjetoImp` 05–07 | **No implementado — requiere el catálogo oficial.** Ver abajo. | | 3 | Claves ProdServ adicionales | **No implementado — requiere el catálogo oficial.** Ver abajo. | | 4 | Impuestos locales | **Resuelto por diseño: no se catalogan en `sat.taxes`.** El ISH y similares viajan en el complemento *Impuestos Locales* con claves ajenas a `c_Impuesto`; mezclarlas rompería la unicidad del catálogo federal. `is_local` se conserva como bandera para cuando exista el catálogo del complemento, que es otro ticket. | | 5 | Régimen fiscal del receptor | **Implementado.** FK `crm.accounts.tax_regime_id` con backfill conservador; el texto libre se conserva. | | 6 | `company_id` en los catálogos | **Resuelto por diseño: se mantiene heredando `fin.access`.** La alternativa (montarlos fuera del router de `fin`) exigiría una dependencia de autorización propia para datos que igual solo consume el módulo de Facturación; no compensa. El cliente del frontend envía `company_id`. | | 7 | Herencia de claves SAT a la partida | **Implementado.** `create_item` y `update_item` heredan `product_service_id`, `unit_of_measure_id` y `tax_object_id` del concepto; lo que envía el cliente gana. | ### Backfill del régimen y uso de CFDI La migración solo resuelve coincidencias inequívocas: el texto libre igual a la clave del catálogo (`601`, `G03`) o igual a la descripción exacta, sin distinguir mayúsculas ni espacios sobrantes. Lo que no case se queda en `NULL` y la ficha del cliente muestra el texto capturado junto al selector, para que un humano elija la clave. **No se deduce el régimen de un receptor a partir de texto libre**: una clave equivocada provoca CFDI rechazados. Verificado contra PostgreSQL: ``` Con clave regimen=601 uso=G03 # capturado como "601" / "Gastos en general" Con descripcion regimen=601 uso=G03 # capturado como la descripción completa Sin coincidencia regimen=NULL uso=NULL # texto ambiguo → lo revisa el usuario ``` --- ## PENDIENTE DECISIÓN — bloqueado por datos, no por diseño Quedan dos puntos abiertos. En ambos la estructura ya está lista y solo falta cargar filas; **no las escribí de memoria a propósito**: una clave del SAT equivocada en un CFDI es un problema fiscal real, y el ticket es explícito en que no se deducen ni se inventan. Se cargan con `sync_catalogs` en cuanto Fiscal entregue las filas oficiales, sin migración de esquema. 1. **`c_ObjetoImp` claves 05–07.** Sembradas 01–04. Faltan las tres claves que incorporaron versiones posteriores del catálogo, con su descripción textual exacta. *Lo que necesito:* las 3 filas (clave + descripción) de la versión vigente. 2. **Claves `c_ClaveProdServ` adicionales.** El subset de 11 claves no cubre servicios accesorios frecuentes del giro: maniobras, demoras/demurrage, custodia y servicios aduanales. *Lo que necesito:* la clave de 8 dígitos y su descripción para cada servicio que se vaya a facturar. Además, dos validaciones que no bloquean el merge pero sí el paso a producción: - Las **24 claves de `c_UsoCFDI`** y las **11 de `c_ClaveProdServ`** ya sembradas requieren visto bueno de Fiscal antes de timbrar. Ambos bloques están marcados con ese comentario en `seed_data.py`. - **No se cargó la matriz de compatibilidad de `c_UsoCFDI`** (qué uso admite cada régimen y tipo de persona). Cambia entre versiones del catálogo y equivocarla provoca rechazos al timbrar; hoy la UI ofrece todos los usos. Si Fiscal la entrega, se agrega como columnas del catálogo igual que las banderas de `c_RegimenFiscal`. --- ## Fuera de alcance (confirmado) Generación del XML CFDI 4.0, sellado, timbrado con PAC, cancelaciones, complemento de pago y Carta Porte. Tampoco se tocó el cálculo de totales de `fin/invoices/service.py` ni se cargó el catálogo completo de `c_ClaveProdServ` (~52,000 claves). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jcedilloAS added 10 commits 2026-08-07 22:54:56 +00:00
Agrega los 8 catálogos oficiales del SAT (c_RegimenFiscal, c_Impuesto,
c_FormaPago, c_ClaveUnidad, c_ClaveProdServ, c_TipoDeComprobante, c_MetodoPago
y c_ObjetoImp) como tablas globales de solo lectura en el schema sat: sin
tenant_id, sin CRUD y sin baja física (las claves retiradas se desactivan para
no romper los CFDI históricos).

Las semillas viven en catalogs/seed_data.py, no dentro de una migración, para
que corregir un dato del catálogo no exija escribir una migración nueva.
sync_catalogs() hace upsert por clave: inserta lo que falta, actualiza
descripción y banderas, y nunca borra.

El subset de c_ClaveProdServ (11 claves de logística) queda pendiente de
validación con el área Fiscal antes de producción.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fin.concepts es el catálogo de conceptos facturables de cada empresa, ligado a
una clave de producto/servicio del SAT. La relación es 1:1 por empresa: si dos
conceptos compartieran la misma clave, al timbrar no habría forma de saber qué
descripción corresponde.

La unicidad se garantiza por índice único parcial (WHERE deleted_at IS NULL) y
se valida además en el service para devolver 409 con mensaje en español en vez
de un IntegrityError crudo. La baja lógica libera la clave y el código.

Las respuestas traen los objetos del catálogo ya resueltos (selectin) para que
el frontend no dispare N+1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fin.issuer_settings guarda la identidad fiscal con la que la empresa emite
CFDI: razón social, RFC, régimen fiscal y CP del lugar de expedición.

Una sola configuración vigente por empresa, garantizada con índice único
parcial; el guardado es un upsert (GET + PUT, sin DELETE). El RFC se valida con
la expresión oficial y se normaliza a mayúsculas sin espacios antes de aplicar
la restricción de longitud.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fin.invoices gana tipo de comprobante, forma y método de pago y CP de
expedición; fin.invoice_items gana concepto de catálogo y las claves ProdServ,
unidad y objeto de impuesto. Todas nullable: las facturas ya emitidas no las
tienen y siguen funcionando igual (listado, detalle, PDF, envío).

La columna de texto libre invoice_items.concept se conserva obligatoria porque
la consume el PDF actual; al capturar por catálogo, el service hereda ahí la
descripción del concepto cuando el cliente no la envía.

Nueva tabla fin.invoice_item_taxes para el detalle de impuestos trasladados y
retenidos por partida. No interviene en el cálculo de subtotal/IVA/total, que
sigue saliendo de invoices.tax_rate.

Incluye la migración e6f7a8b9c0d1 (crea el schema sat, siembra los catálogos con
sync_catalogs y monta las tablas e índices nuevos) y registra los permisos
fin.concept.* y fin.settings.{view,edit}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clientes API por dominio para los catálogos del SAT, conceptos y emisor. Los
catálogos se cachean en un Map del módulo tras la primera carga: son fijos y no
cambian durante la sesión.

Pantalla de conceptos (/dashboard/fin/conceptos) con tabla, buscador, filtro de
activos y alta/edición en diálogo. La clave de producto/servicio se elige con un
combobox que consulta el catálogo a partir de 2 caracteres, y el 409 del backend
por clave ya asignada se muestra junto al campo.

Sección de configuración fiscal (/dashboard/settings/facturacion) con razón
social, RFC (misma validación que el backend), régimen fiscal y CP. Si el GET
responde 404 se abre en modo alta, no como error; el guardar se deshabilita sin
fin.settings.edit.

Todo en Svelte 5 con runes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backend: los 8 endpoints de catálogo responden 200 con las semillas exactas y
filtran por búsqueda; tax-regimes acota por tipo de persona; ninguna ruta de
catálogo acepta escritura (405); sync_catalogs es idempotente. CRUD de
conceptos, conflicto 409 por clave ProdServ repetida en la misma empresa,
la misma clave permitida en otra empresa, la baja lógica liberándola,
aislamiento multi-tenant, upsert del emisor sin duplicar filas y RFC inválido
rechazado. También que una partida con concept_id hereda la descripción y que
las facturas sin claves del SAT siguen listándose y generando PDF.

El fixture de pruebas siembra los catálogos con la misma función que usa la
migración, sobre el schema sat mapeado a SQLite.

Frontend: prueba del cacheo del cliente de catálogos.

RFC dummy XAXX010101000 en todas las pruebas: sin datos reales.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sustituye el diálogo de alta/edición por el patrón que ya usa el CRM para
proveedores y cuentas: la lista solo lista, y el alta y la edición viven en
/dashboard/fin/conceptos/nuevo y /dashboard/fin/conceptos/[id].

Los campos del formulario se extraen a $lib/components/fin/ConceptFields.svelte
para que ambas pantallas compartan el combobox de clave ProdServ y los selects
de unidad y objeto de impuesto. El 409 del backend por clave ya asignada se
sigue mostrando junto al campo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El selector de concepto de la partida deja de ser una lista fija en el código y
se alimenta del catálogo de conceptos de la empresa: al elegir uno se manda
concept_id y el backend copia la descripción a la columna de texto libre que
consume el PDF. Si el concepto trae precio unitario, se precarga en la partida.

Las claves genéricas anteriores quedan en un segundo grupo del mismo selector,
marcadas como "sin clave del SAT", para no bloquear a las empresas que aún no
tienen catálogo; si está vacío se enlaza al alta de conceptos.

El listado de partidas etiqueta con la clave y descripción del catálogo cuando
la partida lo referencia, y cae al texto libre para las facturas anteriores.

Los tipos de Invoice e InvoiceItem se completan con las claves fiscales que el
backend ya devuelve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cierra la decisión pendiente 7. create_item ya no copia solo la descripción del
concepto: también hereda product_service_id, unit_of_measure_id y tax_object_id
cuando el cliente no los envía, para que la partida capturada por catálogo quede
completa para el CFDI. Lo que el cliente sí manda gana sobre el catálogo, para
poder facturar con una unidad distinta a la del concepto.

update_item pasa por la misma resolución cuando cambia concept_id: revalida que
el concepto sea de la empresa (antes el PATCH no lo validaba y admitía apuntar a
un concepto de otro tenant) y vuelve a heredar del concepto nuevo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
jcedilloAS changed title from feat(fin): catálogos SAT, conceptos de facturación y datos fiscales del emisor to T2026-08-047 feat(fin): catálogos SAT, conceptos de facturación y datos fiscales del emisor 2026-08-07 23:01:32 +00:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/AS-catalogos-sat-facturacion:feature/AS-catalogos-sat-facturacion
git checkout feature/AS-catalogos-sat-facturacion
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ADUANASOFT/CRM_AGENTES_CARGA#5
No description provided.