Initial commit

This commit is contained in:
2025-12-05 11:27:16 -03:00
commit 804bacfbe3
87 changed files with 7260 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
from fastapi import APIRouter
from . import clientes, pedidos_cliente, proveedores, albaranes, kanban, alertas, referencias_proveedor
api_router = APIRouter()
api_router.include_router(clientes.router)
api_router.include_router(pedidos_cliente.router)
api_router.include_router(proveedores.router)
api_router.include_router(albaranes.router)
api_router.include_router(kanban.router)
api_router.include_router(alertas.router)
api_router.include_router(referencias_proveedor.router)

104
app/api/routes/albaranes.py Normal file
View File

@@ -0,0 +1,104 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from prisma import Prisma
from typing import List, Optional
from datetime import datetime
from pathlib import Path
from app.models.albaran import AlbaranCreate, AlbaranUpdate, AlbaranResponse
from app.api.dependencies import get_prisma
from app.config import settings
from app.services.albaran_processor import AlbaranProcessor
router = APIRouter(prefix="/albaranes", tags=["albaranes"])
@router.get("/", response_model=List[AlbaranResponse])
async def listar_albaranes(
skip: int = 0,
limit: int = 100,
estado_procesado: Optional[str] = None,
db: Prisma = Depends(get_prisma)
):
"""Listar todos los albaranes"""
where = {}
if estado_procesado:
where["estadoProcesado"] = estado_procesado
albaranes = await db.albaran.find_many(
where=where,
skip=skip,
take=limit,
include={"proveedor": True, "referencias": True},
order_by={"createdAt": "desc"}
)
return albaranes
@router.get("/{albaran_id}", response_model=AlbaranResponse)
async def obtener_albaran(
albaran_id: int,
db: Prisma = Depends(get_prisma)
):
"""Obtener un albarán por ID"""
albaran = await db.albaran.find_unique(
where={"id": albaran_id},
include={"proveedor": True, "referencias": True}
)
if not albaran:
raise HTTPException(status_code=404, detail="Albarán no encontrado")
return albaran
@router.post("/upload", response_model=AlbaranResponse, status_code=201)
async def subir_albaran(
archivo: UploadFile = File(...),
db: Prisma = Depends(get_prisma)
):
"""Subir un albarán desde móvil o web"""
# Guardar archivo
upload_dir = settings.ALBARANES_ESCANEADOS_DIR
file_path = upload_dir / archivo.filename
with open(file_path, "wb") as f:
content = await archivo.read()
f.write(content)
# Procesar
try:
processor = AlbaranProcessor(db)
albaran = await processor.process_albaran_file(file_path)
return albaran
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{albaran_id}/vincular-proveedor", response_model=AlbaranResponse)
async def vincular_proveedor(
albaran_id: int,
proveedor_id: int = Query(...),
db: Prisma = Depends(get_prisma)
):
"""Vincula un albarán a un proveedor manualmente"""
albaran = await db.albaran.find_unique(where={"id": albaran_id})
if not albaran:
raise HTTPException(status_code=404, detail="Albarán no encontrado")
proveedor = await db.proveedor.find_unique(where={"id": proveedor_id})
if not proveedor:
raise HTTPException(status_code=404, detail="Proveedor no encontrado")
albaran_actualizado = await db.albaran.update(
where={"id": albaran_id},
data={
"proveedorId": proveedor_id,
"estadoProcesado": "procesado",
"fechaProcesado": datetime.now()
},
include={"proveedor": True, "referencias": True}
)
# Reprocesar para vincular referencias
processor = AlbaranProcessor(db)
await processor.match_and_update_referencias(albaran_actualizado)
return albaran_actualizado

50
app/api/routes/alertas.py Normal file
View File

@@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends
from prisma import Prisma
from typing import List, Dict
from datetime import datetime, timedelta
from app.api.dependencies import get_prisma
router = APIRouter(prefix="/alertas", tags=["alertas"])
@router.get("/")
async def obtener_alertas(
db: Prisma = Depends(get_prisma)
) -> List[Dict]:
"""Obtener alertas de pedidos urgentes (menos de 12 horas)"""
ahora = datetime.now()
limite = ahora + timedelta(hours=12)
pedidos_urgentes = await db.pedidocliente.find_many(
where={
"fechaCita": {
"gte": ahora,
"lte": limite
},
"estado": {
"in": ["pendiente_revision", "en_revision", "pendiente_materiales"]
}
},
include={
"cliente": True,
"referencias": True
}
)
alertas = []
for pedido in pedidos_urgentes:
referencias_faltantes = [
ref.dict() for ref in pedido.referencias
if ref.unidadesPendientes > 0
]
if referencias_faltantes:
tiempo_restante = (pedido.fechaCita - ahora).total_seconds() / 3600
alertas.append({
"pedido": pedido.dict(),
"referencias_faltantes": referencias_faltantes,
"horas_restantes": tiempo_restante
})
return alertas

View File

@@ -0,0 +1,77 @@
from fastapi import APIRouter, Depends, HTTPException
from prisma import Prisma
from typing import List
from app.models.cliente import ClienteCreate, ClienteUpdate, ClienteResponse
from app.api.dependencies import get_prisma
router = APIRouter(prefix="/clientes", tags=["clientes"])
@router.get("/", response_model=List[ClienteResponse])
async def listar_clientes(
skip: int = 0,
limit: int = 100,
db: Prisma = Depends(get_prisma)
):
"""Listar todos los clientes"""
clientes = await db.cliente.find_many(skip=skip, take=limit)
return clientes
@router.get("/{cliente_id}", response_model=ClienteResponse)
async def obtener_cliente(
cliente_id: int,
db: Prisma = Depends(get_prisma)
):
"""Obtener un cliente por ID"""
cliente = await db.cliente.find_unique(where={"id": cliente_id})
if not cliente:
raise HTTPException(status_code=404, detail="Cliente no encontrado")
return cliente
@router.post("/", response_model=ClienteResponse, status_code=201)
async def crear_cliente(
cliente: ClienteCreate,
db: Prisma = Depends(get_prisma)
):
"""Crear un nuevo cliente"""
try:
nuevo_cliente = await db.cliente.create(data=cliente.dict())
return nuevo_cliente
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{cliente_id}", response_model=ClienteResponse)
async def actualizar_cliente(
cliente_id: int,
cliente: ClienteUpdate,
db: Prisma = Depends(get_prisma)
):
"""Actualizar un cliente"""
cliente_existente = await db.cliente.find_unique(where={"id": cliente_id})
if not cliente_existente:
raise HTTPException(status_code=404, detail="Cliente no encontrado")
data = {k: v for k, v in cliente.dict().items() if v is not None}
cliente_actualizado = await db.cliente.update(
where={"id": cliente_id},
data=data
)
return cliente_actualizado
@router.delete("/{cliente_id}", status_code=204)
async def eliminar_cliente(
cliente_id: int,
db: Prisma = Depends(get_prisma)
):
"""Eliminar un cliente"""
cliente = await db.cliente.find_unique(where={"id": cliente_id})
if not cliente:
raise HTTPException(status_code=404, detail="Cliente no encontrado")
await db.cliente.delete(where={"id": cliente_id})
return None

47
app/api/routes/kanban.py Normal file
View File

@@ -0,0 +1,47 @@
from fastapi import APIRouter, Depends
from prisma import Prisma
from typing import Dict, List
from app.api.dependencies import get_prisma
from app.models.pedido_cliente import PedidoClienteResponse
router = APIRouter(prefix="/kanban", tags=["kanban"])
@router.get("/")
async def obtener_kanban(
db: Prisma = Depends(get_prisma)
) -> Dict[str, List[Dict]]:
"""Obtener datos del Kanban agrupados por estado"""
pedidos = await db.pedidocliente.find_many(
include={
"cliente": True,
"referencias": True
},
order_by={"fechaPedido": "desc"}
)
# Agrupar por estado
kanban_data = {
"pendiente_revision": [],
"en_revision": [],
"pendiente_materiales": [],
"completado": [],
}
for pedido in pedidos:
pedido_dict = pedido.dict()
# Calcular es_urgente
from datetime import datetime, timedelta
if pedido.fechaCita:
ahora = datetime.now(pedido.fechaCita.tzinfo) if pedido.fechaCita.tzinfo else datetime.now()
tiempo_restante = pedido.fechaCita - ahora
pedido_dict["es_urgente"] = 0 < tiempo_restante.total_seconds() < 12 * 3600
else:
pedido_dict["es_urgente"] = False
estado = pedido.estado
if estado in kanban_data:
kanban_data[estado].append(pedido_dict)
return kanban_data

View File

@@ -0,0 +1,246 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from prisma import Prisma
from typing import List, Optional
from datetime import datetime, timedelta
from app.models.pedido_cliente import (
PedidoClienteCreate, PedidoClienteUpdate, PedidoClienteResponse,
ReferenciaPedidoClienteUpdate
)
from app.api.dependencies import get_prisma
router = APIRouter(prefix="/pedidos-cliente", tags=["pedidos-cliente"])
def calcular_estado_referencia(unidades_solicitadas: int, unidades_en_stock: int) -> str:
"""Calcula el estado de una referencia"""
unidades_pendientes = max(0, unidades_solicitadas - unidades_en_stock)
if unidades_pendientes <= 0:
return "completo"
elif unidades_pendientes < unidades_solicitadas:
return "parcial"
return "pendiente"
def es_urgente(fecha_cita: Optional[datetime]) -> bool:
"""Verifica si un pedido es urgente (menos de 12 horas)"""
if not fecha_cita:
return False
ahora = datetime.now(fecha_cita.tzinfo) if fecha_cita.tzinfo else datetime.now()
tiempo_restante = fecha_cita - ahora
return 0 < tiempo_restante.total_seconds() < 12 * 3600
@router.get("/", response_model=List[PedidoClienteResponse])
async def listar_pedidos(
skip: int = 0,
limit: int = 100,
estado: Optional[str] = None,
urgente: Optional[bool] = None,
matricula: Optional[str] = None,
db: Prisma = Depends(get_prisma)
):
"""Listar pedidos de cliente"""
where = {}
if estado:
where["estado"] = estado
if matricula:
where["cliente"] = {"matriculaVehiculo": {"contains": matricula, "mode": "insensitive"}}
pedidos = await db.pedidocliente.find_many(
where=where,
skip=skip,
take=limit,
include={
"cliente": True,
"referencias": True
},
order_by={"fechaPedido": "desc"}
)
# Filtrar por urgente si se solicita
if urgente is not None:
pedidos = [p for p in pedidos if es_urgente(p.fechaCita) == urgente]
# Agregar es_urgente a cada pedido
result = []
for pedido in pedidos:
pedido_dict = pedido.dict()
pedido_dict["es_urgente"] = es_urgente(pedido.fechaCita)
result.append(pedido_dict)
return result
@router.get("/{pedido_id}", response_model=PedidoClienteResponse)
async def obtener_pedido(
pedido_id: int,
db: Prisma = Depends(get_prisma)
):
"""Obtener un pedido por ID"""
pedido = await db.pedidocliente.find_unique(
where={"id": pedido_id},
include={"cliente": True, "referencias": True}
)
if not pedido:
raise HTTPException(status_code=404, detail="Pedido no encontrado")
pedido_dict = pedido.dict()
pedido_dict["es_urgente"] = es_urgente(pedido.fechaCita)
return pedido_dict
@router.post("/", response_model=PedidoClienteResponse, status_code=201)
async def crear_pedido(
pedido: PedidoClienteCreate,
db: Prisma = Depends(get_prisma)
):
"""Crear un nuevo pedido de cliente"""
try:
# Verificar que el cliente existe
cliente = await db.cliente.find_unique(where={"id": pedido.cliente_id})
if not cliente:
raise HTTPException(status_code=404, detail="Cliente no encontrado")
# Crear pedido
referencias_data = pedido.referencias or []
pedido_data = pedido.dict(exclude={"referencias"})
# Convertir snake_case a camelCase para Prisma
pedido_data_prisma = {
"numeroPedido": pedido_data["numero_pedido"],
"clienteId": pedido_data["cliente_id"],
"fechaCita": pedido_data.get("fecha_cita"),
"estado": pedido_data["estado"],
"presupuestoId": pedido_data.get("presupuesto_id"),
"archivoPdfPath": pedido_data.get("archivo_pdf_path"),
"referencias": {
"create": [
{
"referencia": ref.referencia,
"denominacion": ref.denominacion,
"unidadesSolicitadas": ref.unidades_solicitadas,
"unidadesEnStock": ref.unidades_en_stock,
"unidadesPendientes": max(0, ref.unidades_solicitadas - ref.unidades_en_stock),
"estado": calcular_estado_referencia(ref.unidades_solicitadas, ref.unidades_en_stock)
}
for ref in referencias_data
]
}
}
nuevo_pedido = await db.pedidocliente.create(
data=pedido_data_prisma,
include={"cliente": True, "referencias": True}
)
nuevo_pedido_dict = nuevo_pedido.dict()
nuevo_pedido_dict["es_urgente"] = es_urgente(nuevo_pedido.fechaCita)
return nuevo_pedido_dict
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{pedido_id}", response_model=PedidoClienteResponse)
async def actualizar_pedido(
pedido_id: int,
pedido: PedidoClienteUpdate,
db: Prisma = Depends(get_prisma)
):
"""Actualizar un pedido"""
pedido_existente = await db.pedidocliente.find_unique(where={"id": pedido_id})
if not pedido_existente:
raise HTTPException(status_code=404, detail="Pedido no encontrado")
data = {k: v for k, v in pedido.dict().items() if v is not None}
# Convertir snake_case a camelCase
data_prisma = {}
field_mapping = {
"numero_pedido": "numeroPedido",
"cliente_id": "clienteId",
"fecha_cita": "fechaCita",
"presupuesto_id": "presupuestoId",
"archivo_pdf_path": "archivoPdfPath"
}
for key, value in data.items():
prisma_key = field_mapping.get(key, key)
data_prisma[prisma_key] = value
pedido_actualizado = await db.pedidocliente.update(
where={"id": pedido_id},
data=data_prisma,
include={"cliente": True, "referencias": True}
)
pedido_dict = pedido_actualizado.dict()
pedido_dict["es_urgente"] = es_urgente(pedido_actualizado.fechaCita)
return pedido_dict
@router.post("/{pedido_id}/actualizar-estado")
async def actualizar_estado_pedido(
pedido_id: int,
estado: str,
db: Prisma = Depends(get_prisma)
):
"""Actualizar el estado de un pedido"""
estados_validos = ["pendiente_revision", "en_revision", "pendiente_materiales", "completado"]
if estado not in estados_validos:
raise HTTPException(status_code=400, detail="Estado inválido")
pedido = await db.pedidocliente.find_unique(where={"id": pedido_id})
if not pedido:
raise HTTPException(status_code=404, detail="Pedido no encontrado")
await db.pedidocliente.update(
where={"id": pedido_id},
data={"estado": estado}
)
return {"status": "Estado actualizado"}
@router.post("/referencias/{referencia_id}/marcar-stock")
async def marcar_stock_referencia(
referencia_id: int,
unidades_en_stock: int,
db: Prisma = Depends(get_prisma)
):
"""Marcar unidades en stock para una referencia"""
referencia = await db.referenciapedidocliente.find_unique(
where={"id": referencia_id},
include={"pedidoCliente": True}
)
if not referencia:
raise HTTPException(status_code=404, detail="Referencia no encontrada")
unidades_en_stock = max(0, unidades_en_stock)
unidades_pendientes = max(0, referencia.unidadesSolicitadas - unidades_en_stock)
estado = calcular_estado_referencia(referencia.unidadesSolicitadas, unidades_en_stock)
referencia_actualizada = await db.referenciapedidocliente.update(
where={"id": referencia_id},
data={
"unidadesEnStock": unidades_en_stock,
"unidadesPendientes": unidades_pendientes,
"estado": estado
}
)
# Verificar si todas las referencias están completas
todas_referencias = await db.referenciapedidocliente.find_many(
where={"pedidoClienteId": referencia.pedidoClienteId}
)
todas_completas = all(ref.unidadesPendientes == 0 for ref in todas_referencias)
if todas_completas and referencia.pedidoCliente.estado != "completado":
await db.pedidocliente.update(
where={"id": referencia.pedidoClienteId},
data={"estado": "completado"}
)
elif referencia.pedidoCliente.estado == "pendiente_revision":
await db.pedidocliente.update(
where={"id": referencia.pedidoClienteId},
data={"estado": "en_revision"}
)
return referencia_actualizada

View File

@@ -0,0 +1,85 @@
from fastapi import APIRouter, Depends, HTTPException
from prisma import Prisma
from typing import List
from app.models.proveedor import ProveedorCreate, ProveedorUpdate, ProveedorResponse
from app.api.dependencies import get_prisma
router = APIRouter(prefix="/proveedores", tags=["proveedores"])
@router.get("/", response_model=List[ProveedorResponse])
async def listar_proveedores(
skip: int = 0,
limit: int = 100,
activo: bool = True,
db: Prisma = Depends(get_prisma)
):
"""Listar todos los proveedores"""
proveedores = await db.proveedor.find_many(
where={"activo": activo} if activo else {},
skip=skip,
take=limit
)
return proveedores
@router.get("/{proveedor_id}", response_model=ProveedorResponse)
async def obtener_proveedor(
proveedor_id: int,
db: Prisma = Depends(get_prisma)
):
"""Obtener un proveedor por ID"""
proveedor = await db.proveedor.find_unique(where={"id": proveedor_id})
if not proveedor:
raise HTTPException(status_code=404, detail="Proveedor no encontrado")
return proveedor
@router.post("/", response_model=ProveedorResponse, status_code=201)
async def crear_proveedor(
proveedor: ProveedorCreate,
db: Prisma = Depends(get_prisma)
):
"""Crear un nuevo proveedor"""
try:
nuevo_proveedor = await db.proveedor.create(data=proveedor.dict())
return nuevo_proveedor
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{proveedor_id}", response_model=ProveedorResponse)
async def actualizar_proveedor(
proveedor_id: int,
proveedor: ProveedorUpdate,
db: Prisma = Depends(get_prisma)
):
"""Actualizar un proveedor"""
proveedor_existente = await db.proveedor.find_unique(where={"id": proveedor_id})
if not proveedor_existente:
raise HTTPException(status_code=404, detail="Proveedor no encontrado")
data = {k: v for k, v in proveedor.dict().items() if v is not None}
proveedor_actualizado = await db.proveedor.update(
where={"id": proveedor_id},
data=data
)
return proveedor_actualizado
@router.delete("/{proveedor_id}", status_code=204)
async def eliminar_proveedor(
proveedor_id: int,
db: Prisma = Depends(get_prisma)
):
"""Eliminar un proveedor (soft delete)"""
proveedor = await db.proveedor.find_unique(where={"id": proveedor_id})
if not proveedor:
raise HTTPException(status_code=404, detail="Proveedor no encontrado")
await db.proveedor.update(
where={"id": proveedor_id},
data={"activo": False}
)
return None

View File

@@ -0,0 +1,102 @@
from fastapi import APIRouter, Depends, Query
from prisma import Prisma
from typing import List, Dict, Optional
from app.api.dependencies import get_prisma
router = APIRouter(prefix="/referencias-proveedor", tags=["referencias-proveedor"])
@router.get("/")
async def obtener_referencias_proveedor(
proveedor_id: Optional[int] = Query(None),
db: Prisma = Depends(get_prisma)
) -> List[Dict]:
"""Obtener referencias pendientes por proveedor"""
# Obtener referencias pendientes de pedidos a proveedor
where = {
"estado": {"in": ["pendiente", "parcial"]}
}
if proveedor_id:
where["pedidoProveedor"] = {"proveedorId": proveedor_id}
referencias = await db.referenciapedidoproveedor.find_many(
where=where,
include={
"pedidoProveedor": {
"include": {
"proveedor": True
}
},
"referenciaPedidoCliente": {
"include": {
"pedidoCliente": {
"include": {
"cliente": True
}
}
}
}
}
)
# Agrupar por proveedor
proveedores_data = {}
for ref in referencias:
proveedor = ref.pedidoProveedor.proveedor
if proveedor.id not in proveedores_data:
proveedores_data[proveedor.id] = {
"proveedor": {
"id": proveedor.id,
"nombre": proveedor.nombre,
"email": proveedor.email,
"tiene_web": proveedor.tieneWeb,
"activo": proveedor.activo,
},
"referencias_pendientes": [],
"referencias_devolucion": [],
}
proveedores_data[proveedor.id]["referencias_pendientes"].append({
"id": ref.id,
"referencia": ref.referencia,
"denominacion": ref.denominacion,
"unidades_pedidas": ref.unidadesPedidas,
"unidades_recibidas": ref.unidadesRecibidas,
"estado": ref.estado,
})
# Agregar devoluciones pendientes
devoluciones_where = {"estadoAbono": "pendiente"}
if proveedor_id:
devoluciones_where["proveedorId"] = proveedor_id
devoluciones = await db.devolucion.find_many(
where=devoluciones_where,
include={"proveedor": True}
)
for dev in devoluciones:
if dev.proveedorId not in proveedores_data:
proveedores_data[dev.proveedorId] = {
"proveedor": {
"id": dev.proveedor.id,
"nombre": dev.proveedor.nombre,
"email": dev.proveedor.email,
"tiene_web": dev.proveedor.tieneWeb,
"activo": dev.proveedor.activo,
},
"referencias_pendientes": [],
"referencias_devolucion": [],
}
proveedores_data[dev.proveedorId]["referencias_devolucion"].append({
"id": dev.id,
"referencia": dev.referencia,
"denominacion": dev.denominacion,
"unidades": dev.unidades,
"fecha_devolucion": dev.fechaDevolucion.isoformat() if dev.fechaDevolucion else None,
})
return list(proveedores_data.values())