feat(fin): la partida hereda las claves del SAT de su concepto
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>
This commit is contained in:
@@ -332,12 +332,21 @@ def _get_item(db, item_id, tenant_id, company_id) -> InvoiceItem:
|
||||
return obj
|
||||
|
||||
|
||||
def _resolve_item_concept(db, data: dict, tenant_id, company_id) -> None:
|
||||
"""Completa ``concept`` a partir del concepto del catálogo cuando no se envió.
|
||||
# Claves del SAT que la partida hereda del concepto del catálogo cuando no se envían.
|
||||
_CONCEPT_INHERITED_FIELDS = ("product_service_id", "unit_of_measure_id", "tax_object_id")
|
||||
|
||||
El PDF de la factura sigue leyendo la columna de texto libre ``concept``, así que
|
||||
al capturar por catálogo se hereda ahí la descripción del concepto (recortada al
|
||||
largo de la columna).
|
||||
|
||||
def _resolve_item_concept(db, data: dict, tenant_id, company_id) -> None:
|
||||
"""Completa la partida a partir del concepto del catálogo.
|
||||
|
||||
Hereda dos cosas cuando el cliente no las manda:
|
||||
|
||||
- ``concept``: el PDF de la factura sigue leyendo esa columna de texto libre, así
|
||||
que ahí va la descripción del concepto (recortada al largo de la columna).
|
||||
- Las claves fiscales (``product_service_id``, ``unit_of_measure_id``,
|
||||
``tax_object_id``): sin ellas la partida capturada por catálogo quedaría
|
||||
incompleta para el CFDI. Lo que el cliente sí envía manda sobre el catálogo,
|
||||
para poder facturar una partida con una unidad distinta a la del concepto.
|
||||
"""
|
||||
concept_id = data.get("concept_id")
|
||||
if concept_id is not None:
|
||||
@@ -352,6 +361,9 @@ def _resolve_item_concept(db, data: dict, tenant_id, company_id) -> None:
|
||||
)
|
||||
if not data.get("concept"):
|
||||
data["concept"] = catalog_concept.description[:60]
|
||||
for field in _CONCEPT_INHERITED_FIELDS:
|
||||
if data.get(field) is None:
|
||||
data[field] = getattr(catalog_concept, field)
|
||||
if not data.get("concept"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
@@ -374,7 +386,12 @@ def create_item(db, payload: InvoiceItemCreate, tenant_id, company_id) -> Invoic
|
||||
|
||||
def update_item(db, item_id, payload: InvoiceItemUpdate, tenant_id, company_id) -> InvoiceItem:
|
||||
item = _get_item(db, item_id, tenant_id, company_id)
|
||||
for f, v in payload.model_dump(exclude_unset=True).items():
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
# Cambiar el concepto del catálogo revalida la referencia y vuelve a heredar
|
||||
# descripción y claves fiscales del concepto nuevo.
|
||||
if data.get("concept_id") is not None:
|
||||
_resolve_item_concept(db, data, tenant_id, company_id)
|
||||
for f, v in data.items():
|
||||
setattr(item, f, v)
|
||||
db.flush()
|
||||
_recompute(db, get_invoice(db, item.invoice_id, tenant_id, company_id))
|
||||
|
||||
Reference in New Issue
Block a user