179 lines
6.1 KiB
Python
179 lines
6.1 KiB
Python
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") |