from datetime import datetime, timezone from fastapi import HTTPException, status from sqlalchemy.orm import Session from api.v1.modules.crm.accounts.models import Account from api.v1.modules.crm.quotes.models import Quote from api.v1.modules.crm.service_requests.models import ServiceRequest from api.v1.modules.crm.suppliers.models import Supplier from .dto import ShipmentCreate, ShipmentDocumentCreate, ShipmentDocumentUpdate, ShipmentUpdate from .models import Shipment, ShipmentDocument def _exists(db: Session, model, _id: int | None, tenant_id: int, company_id: int) -> bool: if _id is None: return True return ( db.query(model.id) .filter( model.id == _id, model.tenant_id == tenant_id, model.company_id == company_id, model.deleted_at.is_(None), ) .first() is not None ) def _validate_refs(db: Session, data: dict, tenant_id: int, company_id: int) -> None: checks = [ ("account_id", Account, "El cliente asociado no existe"), ("quote_id", Quote, "La cotización asociada no existe"), ("service_request_id", ServiceRequest, "La solicitud asociada no existe"), ("carrier_supplier_id", Supplier, "El transportista/naviera no existe"), ("customs_agent_id", Supplier, "El agente aduanal no existe"), ("destination_agent_id", Supplier, "El agente en destino no existe"), ] for field, model, msg in checks: if field in data and not _exists(db, model, data.get(field), tenant_id, company_id): raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=msg) def get_shipments( db: Session, tenant_id: int, company_id: int, search: str | None = None, shipment_status: str | None = None, account_id: int | None = None, ) -> list[Shipment]: query = db.query(Shipment).filter( Shipment.tenant_id == tenant_id, Shipment.company_id == company_id, Shipment.deleted_at.is_(None), ) if shipment_status: query = query.filter(Shipment.status == shipment_status) if account_id is not None: query = query.filter(Shipment.account_id == account_id) if search: pattern = f"%{search}%" query = query.filter( Shipment.reference.ilike(pattern) | Shipment.booking_number.ilike(pattern) | Shipment.origin.ilike(pattern) | Shipment.destination.ilike(pattern) ) return query.order_by(Shipment.created_at.desc()).all() def get_shipment(db: Session, shipment_id: int, tenant_id: int, company_id: int) -> Shipment: obj = ( db.query(Shipment) .filter( Shipment.id == shipment_id, Shipment.tenant_id == tenant_id, Shipment.company_id == company_id, Shipment.deleted_at.is_(None), ) .first() ) if not obj: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Embarque no encontrado") return obj def create_shipment( db: Session, payload: ShipmentCreate, tenant_id: int, company_id: int, user_id: str | None = None ) -> Shipment: data = payload.model_dump() _validate_refs(db, data, tenant_id, company_id) obj = Shipment(**data, tenant_id=tenant_id, company_id=company_id, created_by=user_id, updated_by=user_id) db.add(obj) db.commit() db.refresh(obj) return obj def update_shipment( db: Session, shipment_id: int, payload: ShipmentUpdate, tenant_id: int, company_id: int, user_id: str | None = None ) -> Shipment: obj = get_shipment(db, shipment_id, tenant_id, company_id) data = payload.model_dump(exclude_unset=True) _validate_refs(db, data, tenant_id, company_id) for field, value in data.items(): setattr(obj, field, value) obj.updated_by = user_id db.commit() db.refresh(obj) return obj def delete_shipment(db: Session, shipment_id: int, tenant_id: int, company_id: int) -> None: obj = get_shipment(db, shipment_id, tenant_id, company_id) obj.deleted_at = datetime.now(timezone.utc) db.commit() def create_shipment_from_quote( db: Session, quote_id: int, tenant_id: int, company_id: int, user_id: str | None = None ) -> Shipment: """Liberar a Operaciones: crea el embarque a partir de una cotización aceptada.""" quote = ( db.query(Quote) .filter( Quote.id == quote_id, Quote.tenant_id == tenant_id, Quote.company_id == company_id, Quote.deleted_at.is_(None), ) .first() ) if not quote: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cotización no encontrada") if quote.status != "aceptada": raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="La cotización debe estar aceptada para liberarse a Operaciones", ) sr = None if quote.service_request_id: sr = db.query(ServiceRequest).filter(ServiceRequest.id == quote.service_request_id).first() shipment = Shipment( reference=quote.reference, quote_id=quote.id, service_request_id=quote.service_request_id, account_id=quote.account_id, operation_type=sr.operation_type if sr else None, transport_mode=sr.transport_mode if sr else None, service_type=sr.service_type if sr else None, incoterm=sr.incoterm if sr else None, origin=sr.origin if sr else None, destination=sr.destination if sr else None, destination_agent_id=sr.destination_agent_id if sr else None, status="abierta", owner_user_id=quote.owner_user_id, tenant_id=tenant_id, company_id=company_id, created_by=user_id, updated_by=user_id, ) db.add(shipment) if sr: sr.status = "liberada" db.commit() db.refresh(shipment) return shipment # ----- Documentos del embarque ----- def get_shipment_documents( db: Session, tenant_id: int, company_id: int, shipment_id: int | None = None ) -> list[ShipmentDocument]: query = db.query(ShipmentDocument).filter( ShipmentDocument.tenant_id == tenant_id, ShipmentDocument.company_id == company_id, ShipmentDocument.deleted_at.is_(None), ) if shipment_id is not None: query = query.filter(ShipmentDocument.shipment_id == shipment_id) return query.order_by(ShipmentDocument.id.asc()).all() def _get_document(db: Session, doc_id: int, tenant_id: int, company_id: int) -> ShipmentDocument: obj = ( db.query(ShipmentDocument) .filter( ShipmentDocument.id == doc_id, ShipmentDocument.tenant_id == tenant_id, ShipmentDocument.company_id == company_id, ShipmentDocument.deleted_at.is_(None), ) .first() ) if not obj: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Documento no encontrado") return obj def create_shipment_document( db: Session, payload: ShipmentDocumentCreate, tenant_id: int, company_id: int ) -> ShipmentDocument: get_shipment(db, payload.shipment_id, tenant_id, company_id) # valida scope obj = ShipmentDocument(**payload.model_dump(), tenant_id=tenant_id, company_id=company_id) db.add(obj) db.commit() db.refresh(obj) return obj def update_shipment_document( db: Session, doc_id: int, payload: ShipmentDocumentUpdate, tenant_id: int, company_id: int ) -> ShipmentDocument: obj = _get_document(db, doc_id, tenant_id, company_id) for field, value in payload.model_dump(exclude_unset=True).items(): setattr(obj, field, value) db.commit() db.refresh(obj) return obj def delete_shipment_document(db: Session, doc_id: int, tenant_id: int, company_id: int) -> None: obj = _get_document(db, doc_id, tenant_id, company_id) obj.deleted_at = datetime.now(timezone.utc) db.commit()