Files
plantillas-proyectos/backend/api/v1/modules/a76/client_and_provider/service.py
acazares ef5c40af92 feat: Implement client and provider management service
- Added service layer for handling client and provider operations including creation, retrieval, updating, and deletion.
- Introduced DTOs for data transfer and validation.
- Implemented filtering and pagination for client/provider listing.
- Added logging for better traceability of operations.

feat: Create parts management module

- Developed a complete module for managing parts/components including creation, retrieval, updating, and deletion.
- Introduced DTOs for parts with detailed attributes and validation.
- Implemented search and filtering capabilities for parts based on various criteria.
- Added endpoints for regulatory information retrieval and parts statistics.
- Integrated logging for error handling and operational insights.
2025-11-06 20:29:18 -06:00

310 lines
12 KiB
Python

"""
Capa de servicio para lógica de negocio de clientes y proveedores
"""
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.exc import IntegrityError
from sqlalchemy import or_, and_
from fastapi import HTTPException
from typing import List, Optional
import logging
from .models import ClientProvider, GClientProviderAddress, GClientProviderPrograms
from .dto import (
ClientProviderCreateDTO,
ClientProviderUpdateDTO,
ClientProviderResponseDTO,
ClientProviderBasicDTO,
ClientProviderListDTO,
ClientProviderAddressDTO,
ClientProviderProgramsDTO
)
logger = logging.getLogger(__name__)
class ClientProviderService:
"""Servicio para gestión de clientes y proveedores"""
def __init__(self, db: Session):
self.db = db
def create_client_provider(self, client_data: ClientProviderCreateDTO) -> ClientProviderResponseDTO:
"""
Crea un nuevo cliente/proveedor en el sistema
Args:
client_data: Datos del cliente/proveedor a crear
Returns:
ClientProviderResponseDTO con información del cliente/proveedor creado
Raises:
HTTPException: Si el cliente ya existe o error en la creación
"""
try:
# Verificar que no exista el cliente
existing = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_data.client_id).first()
if existing:
raise HTTPException(status_code=400, detail=f"Client with ID '{client_data.client_id}' already exists")
# Crear cliente/proveedor principal
db_client = ClientProvider(
client_id=client_data.client_id,
type_nat_foreign=client_data.type_nat_foreign,
name=client_data.name,
short_name=client_data.short_name,
rfc=client_data.rfc,
curp=client_data.curp,
client_or_provider=client_data.client_or_provider,
linking=client_data.linking,
transform_subassembly=client_data.transform_subassembly,
extra_information=client_data.extra_information,
web_key=client_data.web_key,
responsible=client_data.responsible,
position=client_data.position,
incoterm=client_data.incoterm,
is_national_provider=client_data.is_national_provider,
enabled_disabled=client_data.enabled_disabled
)
self.db.add(db_client)
self.db.flush() # Para obtener el ID antes del commit
# Crear dirección si se proporciona
if client_data.address:
db_address = GClientProviderAddress(
client_id=client_data.client_id,
**client_data.address.model_dump(exclude_unset=True)
)
self.db.add(db_address)
# Crear programas si se proporciona
if client_data.programs:
db_programs = GClientProviderPrograms(
client_id=client_data.client_id,
**client_data.programs.model_dump(exclude_unset=True)
)
self.db.add(db_programs)
self.db.commit()
self.db.refresh(db_client)
logger.info(f"Client/Provider created: {db_client.client_id} - {db_client.name}")
return self._get_client_with_relations(client_data.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_client_provider(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 (C=Cliente, P=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.client_id.ilike(search_pattern)
)
)
if client_or_provider:
query = query.filter(ClientProvider.client_or_provider == client_or_provider)
if enabled_only:
query = query.filter(ClientProvider.enabled_disabled == 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_client_provider(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(GClientProviderAddress).filter(GClientProviderAddress.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 = GClientProviderAddress(
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(GClientProviderPrograms).filter(GClientProviderPrograms.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 = GClientProviderPrograms(
client_id=client_id,
**client_data.programs.model_dump(exclude_unset=True)
)
self.db.add(programs)
self.db.commit()
logger.info(f"Client/Provider updated: {client_id}")
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_client_provider(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()
logger.info(f"Client/Provider deleted: {client_id}")
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")
def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]:
"""Obtiene solo clientes (C)"""
query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'C')
clients = query.offset(skip).limit(limit).all()
return [ClientProviderBasicDTO.model_validate(client) for client in clients]
def get_providers_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]:
"""Obtiene solo proveedores (P)"""
query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'P')
providers = query.offset(skip).limit(limit).all()
return [ClientProviderBasicDTO.model_validate(provider) for provider in providers]
def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]:
"""Busca clientes/proveedores por RFC"""
clients = self.db.query(ClientProvider).filter(ClientProvider.rfc.ilike(f"%{rfc}%")).all()
return [ClientProviderBasicDTO.model_validate(client) for client in clients]
def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]:
"""Cambia el estado habilitado/deshabilitado"""
client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first()
if not client:
return None
# Toggle status (1 = habilitado, 0 = deshabilitado)
client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0
try:
self.db.commit()
logger.info(f"Client/Provider status toggled: {client_id} -> {client.enabled_disabled}")
return self._get_client_with_relations(client_id)
except Exception as e:
self.db.rollback()
logger.error(f"Error toggling status for {client_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating status")