feat(fin): partidas de factura capturadas desde el catálogo de conceptos

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>
This commit is contained in:
2026-08-07 17:24:50 -05:00
parent ae0664e987
commit ce09e0d30a
3 changed files with 83 additions and 9 deletions

View File

@@ -23,7 +23,7 @@ from api.v1.modules.fin.catalogs.seed_data import CATALOGS, sync_catalogs
from api.v1.modules.fin.concepts import service as concepts_service
from api.v1.modules.fin.concepts.dto import ConceptCreate, ConceptUpdate
from api.v1.modules.fin.invoices import service as invoices_service
from api.v1.modules.fin.invoices.dto import InvoiceCreate, InvoiceItemCreate
from api.v1.modules.fin.invoices.dto import InvoiceCreate, InvoiceItemCreate, InvoiceItemResponse
from api.v1.modules.fin.issuer import service as issuer_service
from api.v1.modules.fin.issuer.dto import IssuerSettingsInput
from api.v1.modules.fin.issuer.models import IssuerSettings
@@ -279,6 +279,12 @@ def test_invoice_item_inherits_concept_description(db):
assert item.concept == concept.description # copiada del catálogo para el PDF
assert item.concept_id == concept.id
# La respuesta expone las claves fiscales: el frontend etiqueta la partida con ellas.
payload = InvoiceItemResponse.model_validate(item).model_dump()
assert payload["concept_id"] == concept.id
assert payload["concept"] == concept.description
assert {"product_service_id", "unit_of_measure_id", "tax_object_id"} <= payload.keys()
# Si el cliente sí manda el texto, se respeta tal cual.
explicit = invoices_service.create_item(
db,