505 lines
17 KiB
Python
505 lines
17 KiB
Python
"""
|
|
Capa de servicio para lógica de negocio de clientes y proveedores
|
|
"""
|
|
|
|
import logging
|
|
from typing import List, Optional, Tuple, Dict, Any
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session, joinedload
|
|
from .models import ClientOrProviderEnum
|
|
|
|
from .dto import (
|
|
ClientProviderBasicDTO,
|
|
ClientProviderCreateDTO,
|
|
ClientProviderListDTO,
|
|
ClientProviderResponseDTO,
|
|
ClientProviderUpdateDTO,
|
|
)
|
|
from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ClientProviderService:
|
|
"""Servicio para gestión de clientes y proveedores"""
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
|
|
# Métodos para TenantCRUDRoutes
|
|
@staticmethod
|
|
def get_all(
|
|
db: Session,
|
|
tenant_id: int,
|
|
company_id: Optional[int],
|
|
skip: int = 0,
|
|
limit: int = 50,
|
|
filters: Optional[Dict[str, Any]] = None,
|
|
sort_by: Optional[str] = None,
|
|
sort_order: Optional[str] = "asc",
|
|
) -> Tuple[List[ClientProvider], int]:
|
|
"""Get all clients/providers for a tenant with pagination"""
|
|
query = db.query(ClientProvider).filter(ClientProvider.tenant_id == tenant_id)
|
|
|
|
if company_id is not None:
|
|
query = query.filter(ClientProvider.company_id == company_id)
|
|
|
|
# Apply filters if provided
|
|
if filters:
|
|
if filters.get("search"):
|
|
search_pattern = f"%{filters['search']}%"
|
|
query = query.filter(
|
|
or_(
|
|
ClientProvider.name.ilike(search_pattern),
|
|
ClientProvider.short_name.ilike(search_pattern),
|
|
ClientProvider.rfc.ilike(search_pattern),
|
|
ClientProvider.tax_id.ilike(search_pattern),
|
|
)
|
|
)
|
|
if filters.get("client_or_provider"):
|
|
|
|
query = query.filter(
|
|
or_(
|
|
ClientProvider.client_or_provider == filters["client_or_provider"],
|
|
ClientProvider.client_or_provider == ClientOrProviderEnum.BOTH
|
|
)
|
|
)
|
|
if filters.get("status"):
|
|
enabled = 1 if filters["status"] == "enabled" else 0
|
|
query = query.filter(ClientProvider.is_active == enabled)
|
|
|
|
# Apply sorting
|
|
if sort_by:
|
|
column = getattr(ClientProvider, sort_by, None)
|
|
if column:
|
|
if sort_order == "desc":
|
|
query = query.order_by(column.desc())
|
|
else:
|
|
query = query.order_by(column.asc())
|
|
else:
|
|
query = query.order_by(ClientProvider.id.desc())
|
|
|
|
total = query.count()
|
|
clients = (
|
|
query.options(
|
|
joinedload(ClientProvider.address), joinedload(ClientProvider.programs)
|
|
)
|
|
.offset(skip)
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
return clients, total
|
|
|
|
@staticmethod
|
|
def get_by_id(
|
|
db: Session, client_id: int, tenant_id: int, company_id: int
|
|
) -> Optional[ClientProvider]:
|
|
"""Get client/provider by ID"""
|
|
return (
|
|
db.query(ClientProvider)
|
|
.options(
|
|
joinedload(ClientProvider.address), joinedload(ClientProvider.programs)
|
|
)
|
|
.filter(
|
|
ClientProvider.id == client_id,
|
|
ClientProvider.tenant_id == tenant_id,
|
|
ClientProvider.company_id == company_id,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
@staticmethod
|
|
def create(
|
|
db: Session,
|
|
client_data: ClientProviderCreateDTO,
|
|
tenant_id: int,
|
|
company_id: int,
|
|
) -> ClientProvider:
|
|
"""Create a new client/provider"""
|
|
try:
|
|
# Create main client/provider
|
|
data_dict = client_data.model_dump(exclude={"address", "programs"})
|
|
db_client = ClientProvider(
|
|
**data_dict, tenant_id=tenant_id, company_id=company_id
|
|
)
|
|
|
|
db.add(db_client)
|
|
db.flush()
|
|
|
|
# Create address if provided
|
|
if client_data.address:
|
|
db_address = ClientProviderAddress(
|
|
tenant_id=tenant_id,
|
|
company_id=company_id,
|
|
client_id=db_client.id,
|
|
**client_data.address.model_dump(exclude_unset=True),
|
|
)
|
|
db.add(db_address)
|
|
|
|
# Create programs if provided
|
|
if client_data.programs:
|
|
db_programs = ClientProviderPrograms(
|
|
tenant_id=tenant_id,
|
|
company_id=company_id,
|
|
client_id=db_client.id,
|
|
**client_data.programs.model_dump(exclude_unset=True),
|
|
)
|
|
db.add(db_programs)
|
|
|
|
db.commit()
|
|
db.refresh(db_client)
|
|
|
|
return db_client
|
|
|
|
except IntegrityError as e:
|
|
db.rollback()
|
|
logger.error(f"IntegrityError creating client/provider: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=400, detail="Client/Provider already exists"
|
|
)
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error creating client/provider: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail="Error creating client/provider"
|
|
)
|
|
|
|
@staticmethod
|
|
def update(
|
|
db: Session,
|
|
client_id: int,
|
|
tenant_id: int,
|
|
company_id: int,
|
|
client_data: ClientProviderUpdateDTO,
|
|
) -> Optional[ClientProvider]:
|
|
"""Update a client/provider"""
|
|
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
|
|
if not client:
|
|
return None
|
|
|
|
try:
|
|
# Update main fields
|
|
update_data = client_data.model_dump(
|
|
exclude_unset=True, exclude={"address", "programs"}
|
|
)
|
|
for field, value in update_data.items():
|
|
setattr(client, field, value)
|
|
|
|
# Update address
|
|
if client_data.address:
|
|
if client.address:
|
|
address_data = client_data.address.model_dump(exclude_unset=True)
|
|
for field, value in address_data.items():
|
|
setattr(client.address, field, value)
|
|
else:
|
|
db_address = ClientProviderAddress(
|
|
client_id=client.id,
|
|
tenant_id=tenant_id,
|
|
company_id=company_id,
|
|
**client_data.address.model_dump(exclude_unset=True),
|
|
)
|
|
db.add(db_address)
|
|
|
|
# Update programs
|
|
if client_data.programs:
|
|
if client.programs:
|
|
programs_data = client_data.programs.model_dump(exclude_unset=True)
|
|
for field, value in programs_data.items():
|
|
setattr(client.programs, field, value)
|
|
else:
|
|
db_programs = ClientProviderPrograms(
|
|
client_id=client.id,
|
|
tenant_id=tenant_id,
|
|
company_id=company_id,
|
|
**client_data.programs.model_dump(exclude_unset=True),
|
|
)
|
|
db.add(db_programs)
|
|
|
|
db.commit()
|
|
db.refresh(client)
|
|
|
|
return client
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error updating client/provider {client_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail="Error updating client/provider"
|
|
)
|
|
|
|
@staticmethod
|
|
def delete(db: Session, client_id: int, tenant_id: int, company_id: int) -> bool:
|
|
"""Delete a client/provider"""
|
|
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
|
|
if not client:
|
|
return False
|
|
|
|
try:
|
|
db.delete(client)
|
|
db.commit()
|
|
return True
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error deleting client/provider {client_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail="Error deleting client/provider"
|
|
)
|
|
|
|
# Legacy methods for custom endpoints
|
|
|
|
def create_clients_and_providers_legacy(
|
|
self, company_id: int, client_data: ClientProviderCreateDTO
|
|
) -> ClientProviderResponseDTO:
|
|
"""Legacy method for creating client/provider"""
|
|
try:
|
|
# Create main client/provider
|
|
data_dict = client_data.model_dump(exclude={"address", "programs"})
|
|
db_client = ClientProvider(**data_dict)
|
|
self.db.add(db_client)
|
|
self.db.flush()
|
|
|
|
# Create address if provided
|
|
if client_data.address:
|
|
db_address = ClientProviderAddress(
|
|
company_id=company_id,
|
|
client_id=db_client.id,
|
|
**client_data.address.model_dump(exclude_unset=True),
|
|
)
|
|
self.db.add(db_address)
|
|
|
|
# Create programs if provided
|
|
if client_data.programs:
|
|
db_programs = ClientProviderPrograms(
|
|
company_id=company_id,
|
|
client_id=db_client.id,
|
|
**client_data.programs.model_dump(exclude_unset=True),
|
|
)
|
|
self.db.add(db_programs)
|
|
|
|
self.db.commit()
|
|
|
|
return self._get_client_with_relations(db_client.id)
|
|
|
|
except IntegrityError as e:
|
|
self.db.rollback()
|
|
logger.error(f"IntegrityError creating client/provider: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=400, detail="Client/Provider with this ID already exists"
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"Error creating client/provider: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail="Error creating client/provider"
|
|
)
|
|
|
|
def get_clients_and_providers(
|
|
self, client_id: str
|
|
) -> Optional[ClientProviderResponseDTO]:
|
|
"""
|
|
Obtiene un cliente/proveedor por ID
|
|
|
|
Args:
|
|
client_id: ID del cliente/proveedor
|
|
|
|
Returns:
|
|
ClientProviderResponseDTO o None si no existe
|
|
"""
|
|
return self._get_client_with_relations(client_id)
|
|
|
|
def _get_client_with_relations(
|
|
self, client_id: str
|
|
) -> Optional[ClientProviderResponseDTO]:
|
|
"""Método privado para obtener cliente con relaciones"""
|
|
client = (
|
|
self.db.query(ClientProvider)
|
|
.options(
|
|
joinedload(ClientProvider.address), joinedload(ClientProvider.programs)
|
|
)
|
|
.filter(ClientProvider.client_id == client_id)
|
|
.first()
|
|
)
|
|
|
|
if not client:
|
|
return None
|
|
return ClientProviderResponseDTO.model_validate(client)
|
|
|
|
def list_clients_providers(
|
|
self,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
search: Optional[str] = None,
|
|
client_or_provider: Optional[str] = None,
|
|
enabled_only: bool = False,
|
|
) -> ClientProviderListDTO:
|
|
"""
|
|
Lista clientes/proveedores con filtros
|
|
|
|
Args:
|
|
skip: Número de registros a omitir
|
|
limit: Número máximo de registros a retornar
|
|
search: Texto de búsqueda (nombre, RFC, ID)
|
|
client_or_provider: Filtrar por tipo (client=Cliente, provider=Proveedor)
|
|
enabled_only: Si True, solo retorna activos
|
|
|
|
Returns:
|
|
ClientProviderListDTO con la lista paginada
|
|
"""
|
|
query = self.db.query(ClientProvider)
|
|
|
|
# Aplicar filtros
|
|
if search:
|
|
search_pattern = f"%{search}%"
|
|
query = query.filter(
|
|
or_(
|
|
ClientProvider.name.ilike(search_pattern),
|
|
ClientProvider.short_name.ilike(search_pattern),
|
|
ClientProvider.rfc.ilike(search_pattern),
|
|
ClientProvider.tax_id.ilike(search_pattern),
|
|
ClientProvider.client_id.ilike(search_pattern),
|
|
)
|
|
)
|
|
|
|
if client_or_provider:
|
|
query = query.filter(
|
|
or_(
|
|
ClientProvider.client_or_provider == client_or_provider,
|
|
ClientProvider.client_or_provider == ClientOrProviderEnum.BOTH
|
|
)
|
|
)
|
|
|
|
if enabled_only:
|
|
query = query.filter(ClientProvider.is_active == 1)
|
|
|
|
# Contar total
|
|
total = query.count()
|
|
|
|
# Aplicar paginación
|
|
clients = query.offset(skip).limit(limit).all()
|
|
|
|
# Convertir a DTOs básicos
|
|
client_dtos = [
|
|
ClientProviderBasicDTO.model_validate(client) for client in clients
|
|
]
|
|
|
|
return ClientProviderListDTO(
|
|
clients=client_dtos,
|
|
total=total,
|
|
page=(skip // limit) + 1 if limit > 0 else 1,
|
|
size=len(client_dtos),
|
|
)
|
|
|
|
def update_clients_and_providers(
|
|
self, client_id: str, client_data: ClientProviderUpdateDTO
|
|
) -> Optional[ClientProviderResponseDTO]:
|
|
"""
|
|
Actualiza un cliente/proveedor
|
|
|
|
Args:
|
|
client_id: ID del cliente/proveedor a actualizar
|
|
client_data: Datos a actualizar
|
|
|
|
Returns:
|
|
ClientProviderResponseDTO actualizado o None si no existe
|
|
"""
|
|
client = (
|
|
self.db.query(ClientProvider)
|
|
.filter(ClientProvider.client_id == client_id)
|
|
.first()
|
|
)
|
|
if not client:
|
|
return None
|
|
|
|
try:
|
|
# Actualizar campos del cliente principal
|
|
update_data = client_data.model_dump(
|
|
exclude_unset=True, exclude={"address", "programs"}
|
|
)
|
|
for field, value in update_data.items():
|
|
setattr(client, field, value)
|
|
|
|
# Actualizar dirección
|
|
if client_data.address:
|
|
address = (
|
|
self.db.query(ClientProviderAddress)
|
|
.filter(ClientProviderAddress.client_id == client_id)
|
|
.first()
|
|
)
|
|
if address:
|
|
# Actualizar dirección existente
|
|
address_data = client_data.address.model_dump(exclude_unset=True)
|
|
for field, value in address_data.items():
|
|
setattr(address, field, value)
|
|
else:
|
|
# Crear nueva dirección
|
|
address = ClientProviderAddress(
|
|
client_id=client_id,
|
|
**client_data.address.model_dump(exclude_unset=True),
|
|
)
|
|
self.db.add(address)
|
|
|
|
# Actualizar programas
|
|
if client_data.programs:
|
|
programs = (
|
|
self.db.query(ClientProviderPrograms)
|
|
.filter(ClientProviderPrograms.client_id == client_id)
|
|
.first()
|
|
)
|
|
if programs:
|
|
# Actualizar programas existentes
|
|
programs_data = client_data.programs.model_dump(exclude_unset=True)
|
|
for field, value in programs_data.items():
|
|
setattr(programs, field, value)
|
|
else:
|
|
# Crear nuevos programas
|
|
programs = ClientProviderPrograms(
|
|
client_id=client_id,
|
|
**client_data.programs.model_dump(exclude_unset=True),
|
|
)
|
|
self.db.add(programs)
|
|
|
|
self.db.commit()
|
|
|
|
return self._get_client_with_relations(client_id)
|
|
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"Error updating client/provider {client_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail="Error updating client/provider"
|
|
)
|
|
|
|
def delete_clients_and_providers(self, client_id: str) -> bool:
|
|
"""
|
|
Elimina un cliente/proveedor
|
|
|
|
Args:
|
|
client_id: ID del cliente/proveedor a eliminar
|
|
|
|
Returns:
|
|
True si se eliminó, False si no existe
|
|
"""
|
|
client = (
|
|
self.db.query(ClientProvider)
|
|
.filter(ClientProvider.client_id == client_id)
|
|
.first()
|
|
)
|
|
if not client:
|
|
return False
|
|
|
|
try:
|
|
self.db.delete(client) # Las relaciones se eliminan en cascada
|
|
self.db.commit()
|
|
return True
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"Error deleting client/provider {client_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail="Error deleting client/provider"
|
|
) |