Files
CRM_AGENTES_CARGA/backend/api/v1/modules/fin/concepts/dto.py
Jair Cedillo 9cf142add6 feat(fin): CRUD de conceptos con relación 1:1 a clave ProdServ
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>
2026-08-07 16:57:39 -05:00

56 lines
1.9 KiB
Python

"""Esquemas del catálogo de conceptos de facturación."""
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, ConfigDict, Field
from ..catalogs.dto import ProductServiceResponse, TaxObjectResponse, UnitOfMeasureResponse
class ConceptBase(BaseModel):
code: str = Field(..., min_length=1, max_length=40, description="Clave interna del concepto")
description: str = Field(..., min_length=1, max_length=500)
product_service_id: int = Field(..., description="Clave ProdServ del SAT (1:1 por empresa)")
unit_of_measure_id: int | None = None
tax_object_id: int | None = None
unit_price: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=2)
currency: str = Field("MXN", min_length=3, max_length=3)
is_active: bool = True
notes: str | None = None
class ConceptCreate(ConceptBase):
pass
class ConceptUpdate(BaseModel):
"""Actualización parcial: solo se tocan los campos enviados."""
code: str | None = Field(None, min_length=1, max_length=40)
description: str | None = Field(None, min_length=1, max_length=500)
product_service_id: int | None = None
unit_of_measure_id: int | None = None
tax_object_id: int | None = None
unit_price: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=2)
currency: str | None = Field(None, min_length=3, max_length=3)
is_active: bool | None = None
notes: str | None = None
class ConceptResponse(ConceptBase):
"""Incluye los objetos del catálogo del SAT ya resueltos, para evitar N+1 en la UI."""
model_config = ConfigDict(from_attributes=True)
id: int
tenant_id: int
company_id: int
product_service: ProductServiceResponse | None = None
unit_of_measure: UnitOfMeasureResponse | None = None
tax_object: TaxObjectResponse | None = None
created_by: str | None = None
updated_by: str | None = None
created_at: datetime
updated_at: datetime