Files
plantillas-proyectos/backend/api/v1/modules/a76/clients_and_providers/dto.py

281 lines
11 KiB
Python

"""
DTOs (Data Transfer Objects) para módulo de clientes y proveedores
Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
"""
from decimal import Decimal
from typing import List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
from .validators import is_valid_rfc, is_valid_tax_id
# Mensajes de error reutilizados por las validaciones de identificador fiscal
_RFC_FORMAT_ERROR = "El RFC no tiene el formato correcto. Ejemplo: XAXX010101000."
_TAX_ID_FORMAT_ERROR = (
"El TAX-ID debe tener formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres."
)
def _validate_fiscal_format(rfc: Optional[str], tax_id: Optional[str]) -> None:
"""
Valida el formato de cada identificador fiscal de forma independiente (no excluyente):
un registro puede traer RFC y TAX-ID a la vez; cada uno valida solo si tiene valor.
"""
if (rfc or "").strip() and not is_valid_rfc(rfc):
raise ValueError(_RFC_FORMAT_ERROR)
if (tax_id or "").strip() and not is_valid_tax_id(tax_id):
raise ValueError(_TAX_ID_FORMAT_ERROR)
def _require_fiscal_id_by_procedencia(
rfc: Optional[str], tax_id: Optional[str], type_nat_foreign: Optional[str]
) -> None:
"""Exige el identificador que corresponde a la procedencia: E ⇒ TAX-ID, otro ⇒ RFC."""
is_foreign = (type_nat_foreign or "").strip().upper().startswith("E")
if is_foreign and not (tax_id or "").strip():
raise ValueError("El TAX-ID es obligatorio para registros extranjeros.")
if not is_foreign and not (rfc or "").strip():
raise ValueError("El RFC es obligatorio para registros nacionales.")
# DTOs para dirección
class ClientProviderAddressDTO(BaseModel):
"""DTO para dirección de cliente/proveedor"""
municipality: Optional[str] = Field(
None, max_length=150, description="Municipality"
)
streets: Optional[str] = Field(None, max_length=100, description="Streets")
neighborhood: Optional[str] = Field(None, max_length=40, description="Neighborhood")
interior_number: Optional[str] = Field(
None, max_length=20, description="Interior number"
)
exterior_number: Optional[str] = Field(
None, max_length=20, description="Exterior number"
)
postal_code: Optional[str] = Field(None, max_length=15, description="Postal code")
city: Optional[str] = Field(None, max_length=30, description="City")
state: Optional[str] = Field(None, max_length=30, description="State")
country: Optional[str] = Field(None, max_length=3, description="Country code")
phone: Optional[str] = Field(None, max_length=30, description="Phone number")
fax_number: Optional[str] = Field(None, max_length=30, description="Fax number")
email: Optional[str] = Field(None, max_length=100, description="Email address")
contact: Optional[str] = Field(None, max_length=50, description="Contact person")
reference: Optional[str] = Field(None, max_length=250, description="Reference")
model_config = ConfigDict(from_attributes=True)
# DTOs para programas
class ClientProviderProgramsDTO(BaseModel):
"""DTO para programas de cliente/proveedor"""
program: Optional[str] = Field(None, max_length=7, description="Program")
program_number: Optional[str] = Field(
None, max_length=40, description="Program number"
)
prosec: Optional[str] = Field(None, max_length=8, description="PROSEC")
prosec_authorization: Optional[str] = Field(
None, max_length=20, description="PROSEC authorization"
)
secon_auth_date: Optional[int] = Field(None, description="SECON authorization date")
manufacturer_id: Optional[str] = Field(
None, max_length=25, description="Manufacturer ID"
)
broker: Optional[str] = Field(None, max_length=6, description="Broker")
import_broker: Optional[str] = Field(
None, max_length=6, description="Import broker"
)
transfer_key: Optional[str] = Field(None, max_length=8, description="Transfer key")
secon_authorization: Optional[str] = Field(
None, max_length=20, description="SECON authorization"
)
applied_proportion: Optional[Decimal] = Field(
None, description="Applied proportion"
)
is_certified_company: Optional[str] = Field(
None, max_length=1, description="Is certified company"
)
certified_company_registry: Optional[str] = Field(
None, max_length=40, description="Certified company registry"
)
donation_auth_number: Optional[str] = Field(
None, max_length=50, description="Donation authorization number"
)
ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI")
tax_registry_number: Optional[str] = Field(
None, max_length=40, description="Tax registry number"
)
subassembly_service: Optional[int] = Field(None, description="Subassembly service")
autse_dates: Optional[int] = Field(None, description="AUTSE dates")
autse_number: Optional[str] = Field(
None, max_length=300, description="AUTSE number"
)
model_config = ConfigDict(from_attributes=True)
# DTOs principales
class ClientProviderCreateDTO(BaseModel):
"""DTO para crear cliente/proveedor"""
type_nat_foreign: Optional[str] = Field(
None, max_length=1, description="Type national/foreign"
)
name: Optional[str] = Field(None, max_length=256, description="Name")
short_name: Optional[str] = Field(None, max_length=10, description="Short name")
rfc: Optional[str] = Field(None, max_length=30, description="RFC (nacional)")
tax_id: Optional[str] = Field(
None, max_length=30, description="TAX-ID (identificador fiscal extranjero)"
)
curp: Optional[str] = Field(None, max_length=19, description="CURP")
client_or_provider: Optional[Literal["client", "provider", "both"]] = Field(
None, description="Client or provider"
)
linking: Optional[str] = Field(None, max_length=1, description="Linking")
transform_subassembly: Optional[str] = Field(
None, max_length=1, description="Transform subassembly"
)
extra_information: Optional[str] = Field(
None, max_length=399, description="Extra information"
)
web_key: Optional[str] = Field(None, max_length=40, description="Web key")
responsible: Optional[str] = Field(
None, max_length=80, description="Responsible person"
)
position: Optional[str] = Field(None, max_length=30, description="Position")
incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm")
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
# Nested DTOs
address: Optional[ClientProviderAddressDTO] = Field(
None, description="Address information"
)
programs: Optional[ClientProviderProgramsDTO] = Field(
None, description="Programs information"
)
@model_validator(mode="after")
def validate_rfc_or_tax_id_format(self):
# Valida formato de ambos campos y exige el que corresponda a la procedencia
_validate_fiscal_format(self.rfc, self.tax_id)
_require_fiscal_id_by_procedencia(self.rfc, self.tax_id, self.type_nat_foreign)
return self
model_config = ConfigDict(from_attributes=True)
class ClientProviderUpdateDTO(BaseModel):
"""DTO para actualizar cliente/proveedor"""
type_nat_foreign: Optional[str] = Field(
None, max_length=1, description="Type national/foreign"
)
name: Optional[str] = Field(None, max_length=256, description="Name")
short_name: Optional[str] = Field(None, max_length=10, description="Short name")
rfc: Optional[str] = Field(None, max_length=30, description="RFC (nacional)")
tax_id: Optional[str] = Field(
None, max_length=30, description="TAX-ID (identificador fiscal extranjero)"
)
curp: Optional[str] = Field(None, max_length=19, description="CURP")
client_or_provider: Optional[Literal["client", "provider", "both"]] = Field(
None, description="Client or provider"
)
linking: Optional[str] = Field(None, max_length=1, description="Linking")
transform_subassembly: Optional[str] = Field(
None, max_length=1, description="Transform subassembly"
)
extra_information: Optional[str] = Field(
None, max_length=399, description="Extra information"
)
web_key: Optional[str] = Field(None, max_length=40, description="Web key")
responsible: Optional[str] = Field(
None, max_length=80, description="Responsible person"
)
position: Optional[str] = Field(None, max_length=30, description="Position")
incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm")
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
# Nested DTOs
address: Optional[ClientProviderAddressDTO] = Field(
None, description="Address information"
)
programs: Optional[ClientProviderProgramsDTO] = Field(
None, description="Programs information"
)
@model_validator(mode="after")
def validate_rfc_or_tax_id_format(self):
# Update es parcial (PATCH): solo se valida formato de lo que venga, sin exigir requerido
_validate_fiscal_format(self.rfc, self.tax_id)
return self
model_config = ConfigDict(from_attributes=True)
class ClientProviderResponseDTO(BaseModel):
"""DTO para respuesta de cliente/proveedor"""
id: int
type_nat_foreign: Optional[str] = None
name: Optional[str] = None
short_name: Optional[str] = None
rfc: Optional[str] = None
tax_id: Optional[str] = None
curp: Optional[str] = None
client_or_provider: Optional[str] = None
linking: Optional[str] = None
transform_subassembly: Optional[str] = None
extra_information: Optional[str] = None
web_key: Optional[str] = None
responsible: Optional[str] = None
position: Optional[str] = None
incoterm: Optional[str] = None
is_active: Optional[bool] = None
tenant_id: int
company_id: int
# Nested DTOs
address: Optional[ClientProviderAddressDTO] = None
programs: Optional[ClientProviderProgramsDTO] = None
model_config = ConfigDict(from_attributes=True)
# DTOs para respuestas específicas
class ClientProviderBasicDTO(BaseModel):
"""DTO para información básica de cliente/proveedor"""
client_id: str
name: Optional[str] = None
short_name: Optional[str] = None
rfc: Optional[str] = None
tax_id: Optional[str] = None
client_or_provider: Optional[str] = None
is_active: Optional[bool] = None
model_config = ConfigDict(from_attributes=True)
class ClientProviderListDTO(BaseModel):
"""DTO para lista de clientes/proveedores"""
clients: list[ClientProviderBasicDTO]
total: int
page: int
size: int
model_config = ConfigDict(from_attributes=True)
class ClientProviderPaginatedResponseDTO(BaseModel):
"""DTO para respuesta paginada de clientes/proveedores"""
items: List[ClientProviderResponseDTO]
total: int
page: int
page_size: int
model_config = ConfigDict(from_attributes=True)