import logging from typing import Any, Dict, List, Optional from fastapi import HTTPException from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session # Importamos el modelo PRINCIPAL from .models import Part # Importamos TODOS los DTOs necesarios from .dto import ( PartCreateDTO, PartUpdateDTO, PartResponseDTO, PartBasicDTO # ¡Importante tener este! ) logger = logging.getLogger(__name__) class PartService: """Servicio para gestión de Partes (Anexo 76 + Anexo 24)""" @staticmethod def get_all( db: Session, tenant_id: int, company_id: int, skip: int = 0, limit: int = 100, filters: Optional[Dict[str, Any]] = None, ) -> tuple[List[Part], int]: query = db.query(Part).filter( Part.tenant_id == tenant_id, Part.company_id == company_id ) if filters: if filters.get("q"): search = f"%{filters['q']}%" query = query.filter( or_( Part.part_number.ilike(search), Part.description_spanish.ilike(search), Part.commercial_part_number.ilike(search) ) ) # Otros filtros... total = query.count() items = query.offset(skip).limit(limit).all() return items, total @staticmethod def get_by_id(db: Session, part_id: int, tenant_id: int, company_id: int) -> Optional[Part]: return db.query(Part).filter( Part.id == part_id, Part.tenant_id == tenant_id, Part.company_id == company_id ).first() @staticmethod def create(db: Session, part_data: PartCreateDTO, tenant_id: int, company_id: int) -> Part: # 1. Preparar datos data = part_data.model_dump() # Separar datos anidados fa_dict = data.pop('fa_data', None) inv_dict = data.pop('inv_data', None) # Inyectar IDs de contexto (Seguridad Multi-tenant) data['company_id'] = company_id data['tenant_id'] = tenant_id # 2. Verificar duplicados (Usando la UniqueConstraint del modelo) existing = db.query(Part).filter( Part.tenant_id == tenant_id, Part.company_id == company_id, Part.part_number == data['part_number'] ).first() if existing: raise HTTPException( status_code=400, detail=f"El número de parte '{data['part_number']}' ya existe." ) # 3. Crear objeto Part db_part = Part(**data) # 4. Crear relaciones (Anexo 24) # Importamos aquí para evitar ciclos, usando las rutas de tu modelo if fa_dict: from api.v1.modules.a24.fa.fa_parts.models import FaPart # Importante: Pasar tenant/company también al hijo db_part.fa_data = FaPart(**fa_dict, tenant_id=tenant_id, company_id=company_id) if inv_dict: from api.v1.modules.a24.inv.inv_parts.models import InvPart db_part.inv_data = InvPart(**inv_dict, tenant_id=tenant_id, company_id=company_id) try: db.add(db_part) db.commit() db.refresh(db_part) return db_part except IntegrityError as e: db.rollback() err_msg = str(e.orig) logger.error(f"Error DB creando parte: {err_msg}") if "foreign key" in err_msg: if "unit_of_measure" in err_msg: raise HTTPException(400, "La Unidad de Medida no existe en el catálogo.") if "currency_key" in err_msg: raise HTTPException(400, "La Moneda no existe en el catálogo.") if "part_class" in err_msg: raise HTTPException(400, "La Clase no existe para este cliente.") # El error genérico si falla Company/Client raise HTTPException(400, "Error de referencia: Verifique Cliente, Compañía o Catálogos.") raise HTTPException(400, "Error al guardar la parte.") @staticmethod def update(db: Session, part_id: int, tenant_id: int, part_data: PartUpdateDTO, company_id: int) -> Optional[Part]: db_part = PartService.get_by_id(db, part_id, tenant_id, company_id) if not db_part: return None data = part_data.model_dump(exclude_unset=True) fa_dict = data.pop('fa_data', None) inv_dict = data.pop('inv_data', None) # Actualizar campos directos for key, value in data.items(): setattr(db_part, key, value) # Actualizar FA Data if fa_dict is not None: if db_part.fa_data: for k, v in fa_dict.items(): setattr(db_part.fa_data, k, v) else: from api.v1.modules.a24.fa.fa_parts.models import FaPart db_part.fa_data = FaPart(**fa_dict, tenant_id=tenant_id, company_id=company_id) # Actualizar INV Data if inv_dict is not None: if db_part.inv_data: for k, v in inv_dict.items(): setattr(db_part.inv_data, k, v) else: from api.v1.modules.a24.inv.inv_parts.models import InvPart db_part.inv_data = InvPart(**inv_dict, tenant_id=tenant_id, company_id=company_id) try: db.commit() db.refresh(db_part) return db_part except IntegrityError as e: db.rollback() raise HTTPException(400, f"Error actualizando: {str(e.orig)}") @staticmethod def delete(db: Session, part_id: int, tenant_id: int, company_id: int) -> bool: db_part = PartService.get_by_id(db, part_id, tenant_id, company_id) if not db_part: return False try: db.delete(db_part) db.commit() return True except Exception: db.rollback() raise HTTPException(500, "Error eliminando parte")