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

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"
)