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>
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>
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>
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>
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>
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>
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>
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.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>
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>