122 lines
4.9 KiB
Python
122 lines
4.9 KiB
Python
"""
|
|
Client Profile Model - ServiceManagerWeb
|
|
|
|
Modelo para perfil empresarial de clientes
|
|
Almacena información detallada de la empresa cliente
|
|
"""
|
|
|
|
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Text, Numeric
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from typing import Optional, TYPE_CHECKING
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from app.core.database import Base, GUID
|
|
|
|
if TYPE_CHECKING:
|
|
from app.models.tenant import Tenant
|
|
|
|
|
|
class ClientProfile(Base):
|
|
"""Modelo de Perfil de Cliente Empresarial."""
|
|
|
|
__tablename__ = "client_profiles"
|
|
|
|
# Relación con tenant (uno a uno)
|
|
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("tenants.id", ondelete="CASCADE"),
|
|
unique=True,
|
|
nullable=False,
|
|
index=True
|
|
)
|
|
|
|
# === INFORMACIÓN GENERAL ===
|
|
business_name: Mapped[Optional[str]] = mapped_column(String(255)) # Razón social
|
|
commercial_name: Mapped[Optional[str]] = mapped_column(String(255)) # Nombre comercial
|
|
client_code: Mapped[Optional[str]] = mapped_column(String(50)) # Clave de cliente
|
|
client_type: Mapped[Optional[str]] = mapped_column(String(50)) # Tipo de cliente
|
|
rfc: Mapped[Optional[str]] = mapped_column(String(13)) # RFC México
|
|
tax_id: Mapped[Optional[str]] = mapped_column(String(50)) # ID fiscal general
|
|
|
|
# === UBICACIÓN ===
|
|
country: Mapped[Optional[str]] = mapped_column(String(100))
|
|
state: Mapped[Optional[str]] = mapped_column(String(100))
|
|
city: Mapped[Optional[str]] = mapped_column(String(100))
|
|
address: Mapped[Optional[str]] = mapped_column(Text)
|
|
external_number: Mapped[Optional[str]] = mapped_column(String(20))
|
|
internal_number: Mapped[Optional[str]] = mapped_column(String(20))
|
|
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
|
|
neighborhood: Mapped[Optional[str]] = mapped_column(String(100))
|
|
|
|
# === CONTACTO ===
|
|
main_phone: Mapped[Optional[str]] = mapped_column(String(20))
|
|
secondary_phone: Mapped[Optional[str]] = mapped_column(String(20))
|
|
direct_phone: Mapped[Optional[str]] = mapped_column(String(20))
|
|
phone_extension: Mapped[Optional[str]] = mapped_column(String(10))
|
|
fax: Mapped[Optional[str]] = mapped_column(String(20))
|
|
|
|
# === INFORMACIÓN ADICIONAL ===
|
|
business_hours: Mapped[Optional[str]] = mapped_column(String(255))
|
|
website: Mapped[Optional[str]] = mapped_column(String(255))
|
|
main_email: Mapped[Optional[str]] = mapped_column(String(320))
|
|
billing_email: Mapped[Optional[str]] = mapped_column(String(320))
|
|
|
|
# === MARKETING ===
|
|
advertising_medium: Mapped[Optional[str]] = mapped_column(String(255))
|
|
nationality: Mapped[Optional[str]] = mapped_column(String(100))
|
|
|
|
# === CONFIGURACIÓN EMPRESARIAL ===
|
|
logo_url: Mapped[Optional[str]] = mapped_column(String(500))
|
|
company_representative: Mapped[Optional[str]] = mapped_column(String(255)) # Encargado/Representante
|
|
legal_representative: Mapped[Optional[str]] = mapped_column(String(255))
|
|
|
|
# === FINANZAS/FACTURACIÓN ===
|
|
credit_limit: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
|
payment_terms: Mapped[Optional[str]] = mapped_column(String(100))
|
|
preferred_currency: Mapped[str] = mapped_column(String(3), default="MXN")
|
|
|
|
# === METADATOS ===
|
|
send_to_billing: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
is_active_client: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
is_prospect: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
notes: Mapped[Optional[str]] = mapped_column(Text)
|
|
|
|
# === RELACIONES ===
|
|
tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="client_profile")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<ClientProfile(tenant_id={self.tenant_id}, business_name='{self.business_name}')>"
|
|
|
|
@property
|
|
def full_address(self) -> str:
|
|
"""Dirección completa formateada."""
|
|
address_parts = []
|
|
|
|
if self.address:
|
|
address_parts.append(self.address)
|
|
|
|
if self.external_number:
|
|
if self.internal_number:
|
|
address_parts.append(f"#{self.external_number}-{self.internal_number}")
|
|
else:
|
|
address_parts.append(f"#{self.external_number}")
|
|
|
|
if self.neighborhood:
|
|
address_parts.append(f"Col. {self.neighborhood}")
|
|
|
|
if self.city and self.state:
|
|
address_parts.append(f"{self.city}, {self.state}")
|
|
|
|
if self.postal_code:
|
|
address_parts.append(f"C.P. {self.postal_code}")
|
|
|
|
if self.country:
|
|
address_parts.append(self.country)
|
|
|
|
return ", ".join(address_parts)
|
|
|
|
@property
|
|
def display_name(self) -> str:
|
|
"""Nombre para mostrar (comercial o razón social)."""
|
|
return self.commercial_name or self.business_name or "Sin nombre" |