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>
56 lines
1.9 KiB
Python
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
|