- 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.
310 lines
12 KiB
Python
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")
|
|
|
|
|