Files
CRM_AGENTES_CARGA/backend/api/v1/modules/crm/documents/service.py
Aduanasoft b12af1a561 feat(crm): catálogos Clientes/Prospectos (enriquecido) y Proveedores + direcciones/documentos
Alinea el dominio al spec de catálogos:
- accounts (Clientes/Prospectos): tipo registro/persona, CURP, clasificación
  comercial, bloque fiscal (régimen, CFDI, pago, crédito), auditoría, notas internas
- suppliers (Proveedores): clasificación múltiple, cobertura, países/puertos/
  aeropuertos/aduanas (JSON), fiscal
- addresses y documents: tablas compartidas con FK a cliente o proveedor
- contacts enriquecidos (extensión, whatsapp, área, flags "recibe…", supplier_id)
- migración a7b8c9d0e1f2 (ALTER + CREATE) con downgrade completo
- permisos supplier/address/document; seed de datos actualizado
- 39 tests pytest en verde

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:19:13 -06:00

101 lines
3.3 KiB
Python

from datetime import datetime, timezone
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from ..accounts.models import Account
from ..suppliers.models import Supplier
from .dto import DocumentCreate, DocumentUpdate
from .models import Document
def _validate_owner(db: Session, account_id: int | None, supplier_id: int | None, tenant_id: int, company_id: int) -> None:
"""Un documento debe pertenecer a exactamente un cliente o proveedor existente."""
if (account_id is None) == (supplier_id is None):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="El documento debe asociarse a un cliente O a un proveedor",
)
model, _id, msg = (
(Account, account_id, "El cliente asociado no existe")
if account_id is not None
else (Supplier, supplier_id, "El proveedor asociado no existe")
)
exists = (
db.query(model.id)
.filter(
model.id == _id,
model.tenant_id == tenant_id,
model.company_id == company_id,
model.deleted_at.is_(None),
)
.first()
)
if not exists:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=msg)
def get_documents(
db: Session,
tenant_id: int,
company_id: int,
account_id: int | None = None,
supplier_id: int | None = None,
) -> list[Document]:
query = db.query(Document).filter(
Document.tenant_id == tenant_id,
Document.company_id == company_id,
Document.deleted_at.is_(None),
)
if account_id is not None:
query = query.filter(Document.account_id == account_id)
if supplier_id is not None:
query = query.filter(Document.supplier_id == supplier_id)
return query.order_by(Document.created_at.desc()).all()
def get_document(db: Session, document_id: int, tenant_id: int, company_id: int) -> Document:
document = (
db.query(Document)
.filter(
Document.id == document_id,
Document.tenant_id == tenant_id,
Document.company_id == company_id,
Document.deleted_at.is_(None),
)
.first()
)
if not document:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Documento no encontrado")
return document
def create_document(
db: Session, payload: DocumentCreate, tenant_id: int, company_id: int, user_id: str | None = None
) -> Document:
_validate_owner(db, payload.account_id, payload.supplier_id, tenant_id, company_id)
document = Document(
**payload.model_dump(), tenant_id=tenant_id, company_id=company_id, uploaded_by=user_id
)
db.add(document)
db.commit()
db.refresh(document)
return document
def update_document(
db: Session, document_id: int, payload: DocumentUpdate, tenant_id: int, company_id: int
) -> Document:
document = get_document(db, document_id, tenant_id, company_id)
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(document, field, value)
db.commit()
db.refresh(document)
return document
def delete_document(db: Session, document_id: int, tenant_id: int, company_id: int) -> None:
document = get_document(db, document_id, tenant_id, company_id)
document.deleted_at = datetime.now(timezone.utc)
db.commit()