- Hoja de ruta: Roadmap.md
- Workflow de Desarrollo (Local vs Docker): docs/DEVELOPMENT_WORKFLOW.md ⚡
- Capa MCP (servers/tools): docs/MCP.md
- Arquitectura chatbot admin: docs/CHATBOT_ARCHITECTURE.md
- Roles del chatbot admin: docs/CHATBOT_ROLES.md
- Compras (incluye iAVaL - Validador de IA del remito): docs/PURCHASES.md
- Persona de chat: docs/CHAT_PERSONA.md
- SKU Canónico (formato, generación, secuencias): docs/CANONICAL_SKU.md
- Logging y diagnóstico de enriquecimiento IA: docs/ENRICHMENT_LOGS.md
- Growen responde en espanol rioplatense con tono malhumorado, humor negro y sarcasmo directo.
- Solo cubre temas de Nice Grow: catalogo, promociones, servicios y consejos de cultivo; desvia con ironia cualquier consulta ajena al rubro.
- Mantiene limites de seguridad: nada de insultos personales, discursos de odio ni llamados a la violencia.
- Cuando una consulta de precio coincide con varios productos, pide al usuario que aclare la opcion antes de informar montos (flujo legacy en deprecación).
- Migración en curso: consultas de producto en
/chatahora usan tool-calling (OpenAI →mcp_products) para obtener datos consistentes y cacheados; móduloprice_lookup.pymarcado DEPRECATED. - Evolución próxima: chatbot corporativo diferenciado por roles con auditoría y acceso al repositorio controlado (ver documentación de arquitectura y roles).
-
Autenticacion
POST /auth/login: inicio de sesionGET /auth/me: info de sesion actual
-
Compras
GET /purchases: listar (filtros y paginacion)POST /purchases: crear (BORRADOR)GET /purchases/{id}: detalle con lineas y adjuntosPUT /purchases/{id}: actualizar encabezado + lineas (upsert/delete)POST /purchases/{id}/validate: validar (marca lineas OK/SIN_VINCULAR)POST /purchases/{id}/confirm: confirmar (impacta stock y precios)POST /purchases/{id}/cancel: anular (revierte stock si corresponde)GET /purchases/{id}/logs: auditoria e import logsGET /purchases/{id}/attachments/{att}/file: descargar adjunto inlinePOST /purchases/import/santaplanta: importar PDF (pipeline OCR con dedupe)
-
Admin / Servicios
GET /admin/services: listar servicios y estadoGET /admin/services/{name}/status: estado puntualPOST /admin/services/{name}/start|stop: control de servicioGET /admin/services/{name}/logs: ultimos N logsGET /admin/services/{name}/logs/stream: SSE de logsGET /admin/services/{name}/deps/check: chequeos de depsPOST /admin/services/{name}/deps/install: instalar deps (dev)
-
Imagenes / Crawler
GET /admin/image-jobs/status: estado del scheduler/crawlerGET /admin/image-jobs/logs: ultimos logsGET /admin/image-jobs/logs/stream: SSE de logsPOST /admin/image-jobs/trigger/*: disparadores (crawl/purge/etc.)GET /admin/image-jobs/snapshots: listar snapshot files porcorrelation_idGET /admin/image-jobs/snapshots/file?path=...: servir snapshot
-
Health / Diagnostico
GET /health: livenessGET /health/summary: resumen (DB/Redis/Storage/Workers/etc.)GET /health/service/{name}: deps por servicio (pdf_import/playwright/...)GET /health/db|redis|storage|optional|dramatiq: checks especificos
-
Backups (DB)
GET /admin/backups: listar backupsPOST /admin/backups/run: crear backup inmediatoGET /admin/backups/download/{filename}: descargar- Ver guía completa: docs/BACKUPS.md
-
WebSocket
WS /ws: canal de chat; pings cada 30s; timeout lectura 60s
-
Productos / Media (ejemplos)
GET /products: catalogoGET /media/*: estaticos del build (cuando aplica)
-
Ventas / Clientes (nuevo módulo)
GET /sales/POST /sales(rate limited) / flujo confirmaciónPOST /sales/{id}/confirm|deliver|annulPOST /sales/{id}/payments+GET /sales/{id}/paymentsGET /sales/metrics/summarymétricas rápidas (cache 30s)GET /sales/exportCSV históricoGET /sales/catalog/searchautocomplete productos- Documentación completa: docs/SALES.md
Notas:
- Rutas de Admin en frontend:
/admin/servicios,/admin/usuarios,/admin/imagenes-productos. - Alias legacy
/admin/imagenesredirige a/admin/imagenes-productos.
-
Estética Minimal Dark:
- Toggle en
/productos/:idcon selector “Estética: Default | Minimal Dark`. - Persiste por usuario en
user_preferences(scope = product_detail_style) víaGET/PUT /products-ex/users/me/preferences/product-detail. Fallback alocalStorage. - Estilo oscuro minimalista con más aire y acentos verdes/fucsia.
- Toggle en
-
Subir imagen (solo Admin):
- Botón “Subir imagen” en la ficha (visible solo para rol
admin). - Validaciones frontend: tipos
jpg/png/webp, tamaño ≤ 10 MB, dimensiones ≥ 600×600. - Progreso de subida; toasts de éxito/error.
- Endpoint backend:
POST /products/{id}/images/upload(valida tipos/tamaño/dimensiones, AV opcional, deriva webp). - Auditoría:
audit_logconaction=upload_imagey metadatos (producto, filename, size).
- Botón “Subir imagen” en la ficha (visible solo para rol
Tips:
- Colaborador mantiene acciones de URL (“Descargar”) ; la subida directa se reserva a Admin.
Agente para gestión de catálogo y stock de Nice Grow con interfaz de chat web e IA híbrida.
- Backend: FastAPI + WebSocket.
- Base de datos: PostgreSQL 15 (Alembic para migraciones).
- IA: ruteo automático entre Ollama (local) y OpenAI.
- Frontend: React + Vite con listas virtualizadas mediante
react-window. - Adapters: exportación a TiendaNegocio via XLS.
- MCP Servers (nuevo): microservicios auxiliares (ej.
mcp_products,mcp_web_search) que exponen herramientas (tools) vía un endpoint uniformePOST /invoke_toolpara consumo de agentes LLM, actuando como fachada HTTP hacia la API principal (sin acceso directo a DB).- Products: tools
get_product_infoyget_product_full_info(URL defaulthttp://mcp_products:8001/invoke_tool, configurable conMCP_PRODUCTS_URL). - Web Search (MVP): tool
search_web(query)que retorna títulos/URLs/snippets desde un buscador HTML (URL defaulthttp://mcp_web_search:8002/invoke_tool, configurable conMCP_WEB_SEARCH_URL). - Enriquecimiento IA puede anexar contexto de
search_webal prompt siAI_USE_WEB_SEARCH=1yai_allow_external=true.
- Products: tools
- UI (detalle de producto): botón “Enriquecer con IA” (visibilidad: admin/colaborador) y menú de acciones:
- Reenriquecer (force):
POST /products/{id}/enrich?force=true. - Borrar enriquecimiento:
DELETE /products/{id}/enrichment.
- Reenriquecer (force):
- Backend:
POST /products/{id}/enrichgenera descripción y puede mapear campos técnicos (weight_kg,height_cm,width_cm,depth_cm,market_price_reference).- Preferencia de título: usa el nombre del producto canónico (si existe) como entrada del prompt; si no hay canónico, usa el título del producto interno.
- Si la respuesta incluye “Fuentes”, se escribe un
.txtbajo/media/enrichment_logs/y se exponeenrichment_sources_url. - Metadatos de trazabilidad:
last_enriched_atyenriched_byse setean al enriquecer y se limpian al borrar. - Auditoría: acción
enrich/reenrichconprompt_hash,fields_generated,source_filey, siAI_USE_WEB_SEARCH=1,web_search_queryyweb_search_hits. - Robustez: si
AI_USE_WEB_SEARCH=1, el backend realiza un preflight aGET /healthdel MCP Web Search; si no está saludable, omite la búsqueda y continúa el enriquecimiento sin bloquear.
- Acciones masivas:
POST /products/enrich-multiple(máximo 20 IDs por solicitud) con validaciones de título y omitidos si ya enriquecidos (a menos queforce). - Flags relevantes:
AI_USE_WEB_SEARCH(0/1): activa búsqueda web MCP para anexar contexto al prompt.AI_WEB_SEARCH_MAX_RESULTS(default 3): máxima cantidad de resultados anexados.ai_allow_external(settings): debe estar entruepara permitir llamadas externas.
- El backend escribe en
logs/backend.log(configurable conLOG_DIR). En el stack Docker, el volumen./logs:/app/logsya centraliza estos archivos en tu host. - Para un log específico de IA (útil al depurar prompts y uso de MCP), activá
AI_LOG_FILE=1y se generarálogs/ai.logcon formato JSON rotativo.
- Python 3.11+
- Node.js LTS
- PostgreSQL 15
- Opcional (dev/pruebas): SQLite 3 con
aiosqlite(ya incluido en dependencias) - Opcional: Docker y Docker Compose
Para entornos Windows con Docker Desktop/WSL2, el arranque por defecto usa un modo seguro que evita tocar el engine cuando ya hay contenedores activos:
USE_DOCKER_STACK=1(por defecto): el script de inicio se acopla al stack Docker ya levantado, valida puertos (API 8000, DB 5433, FE 5173) y omite levantar uvicorn local o compilar el frontend.DB_NO_TOUCH_IF_PRE_OK=1: si el PRE‑FLIGHT detectó la DB OK, no intentacompose up dbante flaps momentáneos.DB_FLAP_BACKOFF_SEC=10: backoff entre reintentos si la DB flapea (ajustable a 30–60 en entornos más lentos).
Consejo: si el engine WSL/Docker Desktop está inestable, reiniciar Docker Desktop y reintentar. Los snapshots forenses del arranque quedan en logs/start.log (incluyen docker info/ps, probes de puertos y pg_isready).
- El backend usa httpx para llamadas a proveedores (Ollama / APIs); ya viene incluido.
Para la funcionalidad completa de importación de remitos en PDF, que incluye Reconocimiento Óptico de Caracteres (OCR), se requieren las siguientes dependencias de sistema:
- ocrmypdf: para aplicar la capa de OCR a los PDFs.
- tesseract: el motor de OCR. Se recomienda instalar el idioma español.
- ghostscript: para procesar archivos PDF y PostScript.
- poppler: utilidades para renderizar PDFs (usado por
pdf2image).
En Windows, se pueden instalar con scoop o choco. En Debian/Ubuntu:
sudo apt-get update
sudo apt-get install -y ocrmypdf tesseract-ocr tesseract-ocr-spa ghostscript poppler-utilsPara verificar que todas las dependencias están correctamente instaladas y accesibles en el PATH del sistema, se puede usar el script "doctor":
python tools/doctor.pyO a través del endpoint de la API (disponible solo para administradores en entorno de desarrollo): GET /admin/import/doctor.
Growen usa Dramatiq con Redis como broker para procesar tareas asíncronas en segundo plano sin bloquear la API. Existen tres colas principales:
- Cola
images: procesamiento de imágenes (descarga, conversión, thumbnails) - Cola
market: scraping de precios de mercado con Playwright/requests - Cola
drive_sync: sincronización de imágenes desde Google Drive
Redis se gestiona exclusivamente a través de docker-compose.yml:
# Iniciar Redis
docker compose up -d redis
# Verificar estado
docker ps --filter "name=growen-redis"Nota: Los scripts (start.bat, start_stack.ps1) migran automáticamente contenedores Redis creados manualmente (con docker run) a docker-compose. Si encuentras conflictos de nombre, los scripts eliminarán el contenedor antiguo y crearán uno nuevo con la configuración correcta de volúmenes.
Opción 1 - Workers separados (mayor control):
# Worker de imágenes (cola images)
scripts\start_worker_images.cmd
# Worker de mercado (cola market)
scripts\start_worker_market.cmd
# Worker de sincronización Drive (cola drive_sync)
scripts\start_worker_drive_sync.cmdOpción 2 - Worker unificado (recomendado para desarrollo):
# Procesa todas las colas (images + market + drive_sync) con 3 threads
scripts\start_worker_all.cmd
# O especificar cola específica
scripts\start_worker_all.cmd images
scripts\start_worker_all.cmd market
scripts\start_worker_all.cmd drive_syncOpción 3 - Modo desarrollo sin Redis (sin persistencia):
# Usar StubBroker en memoria
set RUN_INLINE_JOBS=1
python services/main.pyREDIS_URL: URL de Redis (default:redis://localhost:6379/0)RUN_INLINE_JOBS: Si es1, usa StubBroker (sin Redis, solo desarrollo)
logs/worker_images.log: worker de imágeneslogs/worker_market.log: worker de mercadologs/worker_drive_sync.log: worker de sincronización Drivelogs/worker_all.log: worker unificado
# Ver estado de Redis, colas y workers
curl http://localhost:8000/health/dramatiq
# Ver resumen completo del sistema
curl http://localhost:8000/health/summaryPara más detalles sobre:
- Scraping de mercado: ver docs/API_MARKET.md
- Sincronización Drive: ver docs/GOOGLE_DRIVE_SYNC.md y docs/DRIVE_SYNC_DRAMATIQ.md
Antes de instalar dependencias, pyproject.toml debe listar los paquetes o usar un directorio src/.
Este repositorio mantiene sus módulos en la raíz, así que es necesario declararlos explícitamente:
[tool.setuptools.packages.find]
include = ["agent_core", "ai", "cli", "adapters", "services", "db"]Si se prefiere un layout src/, trasladá las carpetas anteriores a src/ y añadí where = ["src"] en la misma sección.
# Crear entorno virtual
python -m venv .venv
# Activar entorno virtual (OBLIGATORIO - usar SIEMPRE)
# Windows PowerShell:
.venv\Scripts\Activate.ps1
# Windows CMD:
.venv\Scripts\activate.bat
# Linux/Mac:
source .venv/bin/activate
# Instalar dependencias
pip install -e .[dev]
# Configurar variables de entorno
cp .env.example .env
# en producción reemplazar los placeholders SECRET_KEY, ADMIN_USER y ADMIN_PASS
# en desarrollo se usan valores de prueba si se omiten
# Crear base de datos growen en PostgreSQL
# Aplicar migraciones
alembic -c ./alembic.ini upgrade head
# Iniciar API
uvicorn services.api:app --reloadpython no usa .venv, los módulos instalados no estarán disponibles y fallará la importación (ej: ModuleNotFoundError: No module named 'pgvector').
# Crear una nueva revisión a partir de los modelos
alembic -c ./alembic.ini revision -m "descripcion" --autogenerate
# Aplicar las migraciones pendientes
alembic -c ./alembic.ini upgrade head
# Revertir la última migración
alembic -c ./alembic.ini downgrade -1start.sh, scripts/start.bat y scripts/run_api.cmd invocan scripts\stop.bat, luego scripts\fix_deps.bat y posteriormente scripts\run_migrations.cmd, que ejecuta alembic upgrade head con logging detallado.
Si la migración falla, run_migrations.cmd muestra la ruta del log en logs\migrations y el proceso se detiene para evitar correr con un esquema desactualizado.
De esta forma la base siempre está en el esquema más reciente sin comandos manuales.
El script python scripts/debug_migrations.py genera un reporte en logs/migrations/report_<timestamp>.txt con:
alembic currentalembic headsalembic history --verbose -n 30
También verifica la conexión a la base y avisa si hay múltiples heads. El código de salida es 0 si todo está correcto o 1 si detecta anomalías.
Los logs detallados de Alembic se guardan en logs/migrations/alembic_<timestamp>.log.
El nivel de detalle se ajusta con ALEMBIC_LOG_LEVEL en .env. DEBUG_MIGRATIONS=1 agrega verbosidad al reporte.
Para evitar errores como permiso denegado al esquema public, el usuario de la base de datos debe contar con permisos sobre el esquema public:
ALTER DATABASE growen OWNER TO growen;
GRANT USAGE, CREATE ON SCHEMA public TO growen;Cuando existen tablas creadas manualmente o por otras ramas, las migraciones detectan el esquema real y agregan columnas, claves foráneas e índices faltantes en lugar de fallar con errores como DuplicateTable o UndefinedColumn. Esto vuelve a las migraciones seguras e idempotentes.
La revisión inicial init_schema usa sa.inspect para crear tablas solo cuando faltan y eliminarlas únicamente si existen, evitando fallas en upgrades o downgrades.
Comandos útiles en psql para verificar el estado de una tabla:
\d supplier_price_history
SELECT column_name FROM information_schema.columns
WHERE table_name='supplier_price_history'
ORDER BY ordinal_position;- Endpoints:
POST /purchases,PUT /purchases/{id},POST /purchases/{id}/validate,POST /purchases/{id}/confirm,POST /purchases/{id}/cancel,GET /purchases,GET /purchases/{id},POST /purchases/import/santaplanta,GET /purchases/{id}/unmatched/export. - Importación Santa Planta (PDF): parser heurístico que crea una compra en estado BORRADOR, adjunta el PDF y realiza matching preferente por SKU proveedor (fallback por título a futuro).
- Confirmación: incrementa stock de producto, actualiza
current_purchase_pricedelsupplier_producty registraprice_history(entity_typesupplier). Audita la operación.
La app puede enviar notificaciones por Telegram, por ejemplo al confirmar una compra.
Variables de entorno (ver .env):
TELEGRAM_ENABLED:1para habilitar la integración (por defecto0).TELEGRAM_BOT_TOKEN: token del bot emitido por@BotFather.TELEGRAM_DEFAULT_CHAT_ID: chat ID numérico por defecto (usuario, grupo o canal).PURCHASE_TELEGRAM_TOKENyPURCHASE_TELEGRAM_CHAT_ID(opcionales): overrides específicos para notificaciones de Compras; si están vacíos, se usan los valores globales.
Cómo obtener el chat_id:
- Escribí a tu bot y luego consultá
https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates(en dev) para ver elchat.idnumérico del último mensaje. - En grupos, asegurate de que el bot esté agregado y que la privacidad permita leer los mensajes necesarios.
Notas de seguridad:
- No publiques el token del bot. Si se filtra, revocalo con
@BotFathery generá uno nuevo. - Mantené
.envfuera del control de versiones y usá gestores de secretos en entornos de despliegue.
Podés hablarle al bot de Telegram y que responda con el mismo pipeline del chat HTTP:
- Endpoint:
POST /telegram/webhook/{TELEGRAM_WEBHOOK_TOKEN} - Variables:
TELEGRAM_ENABLED=1TELEGRAM_BOT_TOKEN=<tu token>TELEGRAM_WEBHOOK_TOKEN=<token de path>(elige una cadena difícil de adivinar)TELEGRAM_WEBHOOK_SECRET=<opcional>para validar el headerX-Telegram-Bot-Api-Secret-Token
Pasos para configurar:
- Publicá temporalmente la API o usá un túnel (ngrok/localtunnel).
- Registrá el webhook en Telegram (opcionalmente con secret):
- URL base:
https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook - Query:
url=<PUBLIC_URL>/telegram/webhook/<TELEGRAM_WEBHOOK_TOKEN>ysecret_token=<TELEGRAM_WEBHOOK_SECRET>(si lo definiste).
- URL base:
- Escribí al bot: invocará el endpoint y responderá con el pipeline actual (intents de precio + fallback IA).
Seguridad:
- El path token más el secret header hacen que el webhook no sea invocable por terceros.
- El servicio no responde a updates sin texto/chat_id.
- Tabla de productos: ahora cuenta con scroll horizontal para navegar todas las columnas cuando la suma de anchos excede el viewport. Se añadió un contenedor con
overflow-x: autoy una barra inferior siempre accesible. - Alta manual: botón "Nuevo producto" (roles admin / colaborador) dentro de la vista de productos abre un modal para crear y refresca la lista inmediatamente.
- Importación Santa Planta: en el detalle de compra, cada línea
SIN_VINCULARincluye un botón "Crear producto" que abre un diálogo ligero inline para generar el producto y vincularlo a la línea en un paso. El estado de la línea pasa aOKy, al confirmar la compra, se aplica el ajuste de stock. - Optimización UX: no es necesario salir de la pantalla de la compra para dar de alta productos nuevos derivados del PDF.
- Creación masiva en Compras: seleccionar múltiples líneas
SIN_VINCULARy usar "Crear productos seleccionados" para generar productos en lote. Cada producto toma el título existente (con prefijo opcional) y el stock inicial igual a la cantidad de la línea. - Auto‐link por SKU: al tipear un SKU exacto que coincide con un
supplier_product_idexistente, la línea se vincula automáticamente (estado pasa aOK). - Badge
NUEVO: líneas cuyo producto fue creado en la sesión actual muestran un sello visual sobre el título. - (Pendiente) creación simultánea de supplier_item cuando se cree el producto manualmente (requiere exponer endpoint backend). Por ahora sólo se crea el producto interno.
Próximos pasos sugeridos:
- Creación masiva para múltiples líneas
SIN_VINCULARseleccionadas. - Sugerencia automática de categoría a partir de tokens frecuentes del título.
- Etiqueta visual para productos creados durante la sesión (p.ej. badge "NUEVO").
- Opción de precargar precio de venta canónico si existe heurística de margen.
- Filtros: proveedor, fecha (rango), estado, depósito, remito y búsqueda por nombre de producto.
- Export: líneas
SIN_VINCULARen CSV o XLSX (si está disponibleopenpyxl). - Frontend:
/compras,/compras/nueva,/compras/:id; autoguardado cada 30s; atajos Enter/Ctrl+S/Esc; tips y chicanas visibles. - Notificaciones opcionales por Telegram en confirmación: variables
PURCHASE_TELEGRAM_TOKENyPURCHASE_TELEGRAM_CHAT_ID.
Archivo de ejemplo: samples/santaplanta_compra.csv (cabeceras: supplier_name,remito_number,remito_date,supplier_sku,title,qty,unit_cost,line_discount,global_discount,vat_rate,note).
- Prompt global:
ai/persona.pyimpone estilo rioplatense, directo y con humor sarcástico leve, con salvaguardas (sin insultos ni odio/violencia). - Router de IA:
ai/router.pyinyectaSYSTEM_PROMPTpor defecto para todos los proveedores. - Errores del sistema: middleware en
services/api.pydevuelve mensajes de error con tono breve y claro.
- Múltiples heads: ejecutar
python scripts/debug_migrations.pypara identificar las revisiones y crear una migración de merge si es necesario. - UndefinedTable / UndefinedColumn: revisar
logs/migrations/alembic_<timestamp>.log; puede indicar que falta una migración previa. - DuplicateTable / DuplicateIndex: las migraciones actuales son idempotentes; reejecutarlas no debería fallar.
- Seeds inválidos: asegurarse de que las columnas requeridas existan antes de insertar datos.
- Acceso denegado al iniciar en Windows:
start.batabre procesos constartycmd /k. Para que Windows respete rutas con espacios, las líneas usan comillas dobles consecutivas, por ejemplo:- API:
cmd /k ""%VENV%\python.exe" ... >> "%LOG_DIR%\backend.log" 2>&1" - Frontend:
cmd /k "pushd ""%ROOT%frontend"" && npm run dev >> "%LOG_DIR%\frontend.log" 2>&1"Quitar alguna de esas comillas provoca errores como “Acceso denegado” o que el comando se ejecute en el directorio equivocado. Mantené el patrón intacto y ejecutá el script desde la raíz del proyecto.
- API:
Orden de ejecución recomendado:
scripts\stop.batscripts\fix_deps.batscripts\run_migrations.cmd- Inicio de backend y frontend
- Imagen base:
postgres:15.10-bookworm, reforzada conapt-get dist-upgradeeninfra/Dockerfile.postgres(ejecutádocker compose build db && docker compose up -d dbtras cambios). - En Windows suele estar ocupado el puerto 5432 por otra instalación. El docker-compose mapea Postgres del contenedor al puerto 5433 del host para evitar conflictos.
- Verificá que
.envtenga una URL válida, por ejemplo:DB_URL=postgresql+psycopg://<user>:<pass>@127.0.0.1:5433/growen(no publiques credenciales reales).
- Verificá que
- Si se reutiliza un volumen previo del contenedor y la contraseña del usuario
growenno coincide, podés ajustarla sin borrar datos:docker exec -it growen-postgres shpsql -U growen -d growen -c "ALTER USER growen WITH PASSWORD 'NuevaPass';"- Actualizá
.envcon la contraseña nueva y reiniciá la API.
- Aplicá migraciones con
python -m alembic upgrade headpara crear/actualizar el esquema.
Si al ejecutar start.bat Postgres no está disponible en 127.0.0.1:5433 y no es posible iniciarlo con Docker (por ejemplo, Docker Desktop apagado), el script activa un modo de desarrollo con SQLite usando dev.db:
- Se establece
DB_URL=sqlite+aiosqlite:///./dev.dbsolo para esa sesión. - Las migraciones se ejecutan contra SQLite para crear el esquema mínimo.
- Se ejecuta un seed idempotente del usuario administrador (usuario
admin, password por defectoadmin1234si no se defineADMIN_PASS). - La API se inicia normalmente y podés validar pantallas y flujos básicos sin depender de Postgres.
Notas importantes:
- Este fallback es solo para desarrollo local. Algunas funciones que dependen de características específicas de PostgreSQL o de jobs de fondo pueden estar limitadas.
- Cuando Postgres vuelva a estar disponible, volvés al modo normal simplemente arrancando Docker y re-ejecutando
start.bat.
-
Chequeo rápido de stack (Windows):
- Usa
scripts/status_stack.ps1para verificar DB, API y frontend. - Ejemplo (PowerShell):
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/status_stack.ps1"
- Salida esperada:
DB (127.0.0.1:5433): OK/health: OK/app: OK
- Código de salida:
0: DB y/healthOK (frontend opcional).1: DB o/healthfallan.
- Parámetros opcionales:
-ApiUrl(defaulthttp://127.0.0.1:8000),-DbHostName(default127.0.0.1),-DbPort(default5433).
- Si DB marca FAIL:
- Asegurá Docker Desktop corriendo.
- Levantá la DB:
docker compose up -d db(mapea 5433→5432).
- Si
/healthmarca FAIL:- Relanzá el backend y confirmá que
DB_URLapunta a Postgres. - Reintenta cuando
/healthdevuelva 200.
- Relanzá el backend y confirmá que
- Usa
-
Login devuelve 503: Base de datos no disponible.
- La API devuelve 503 si la DB está temporalmente indisponible (timeout, reinicio, backup).
- Esperá unos segundos y reintentá; revisá
scripts/status_stack.ps1.
Al iniciar la API con scripts\run_api.cmd, el script registra cada paso en logs\run_api.log y Uvicorn redirige su salida a logs\backend.log. Estos archivos permiten diagnosticar fallas de arranque y pueden inspeccionarse con type o cualquier editor de texto:
type logs\run_api.log
type logs\backend.logPara iniciar una sesión de depuración limpia:
python scripts/cleanup_logs.py --dry-run # muestra acciones
python scripts/cleanup_logs.py # elimina rotaciones y trunca backend.log
python scripts/cleanup_logs.py --skip-truncate # no intenta truncar backend.log (útil si está bloqueado por el proceso)
python scripts/cleanup_logs.py --keep-days 2Acciones del script:
- Elimina
backend.log.*y.bak(no borrabackend.logprincipal; lo trunca). - Borra logs de diagnósticos y jobs de imágenes si coinciden con patrones.
- Conserva estructura de carpetas. Usa
--keep-days Npara preservar archivos recientes. - Opcional: limpieza de capturas del botón de reporte según política:
--screenshots-keep-days N(por defecto 30; 0 = sin límite por días)--screenshots-max-mb M(por defecto 200; 0 = sin límite)
Recomendado antes de reproducir un escenario (confirmar compra, probar WebSocket de chat, etc.) para aislar el nuevo output.
Notas en Windows:
- Si
backend.logestá bloqueado por el proceso de la API, el script registrará el error de permiso y creará el marcadorbackend.log.clearedpara indicar que se intentó limpiar. Usá--skip-truncatepara omitir el truncado y aun así limpiar rotaciones.
- Este repositorio ya incluye el árbol de Alembic; no ejecutes
alembic init. alembic.inidefinescript_location = %(here)s/db/migrations, por lo que las rutas se resuelven respecto al archivo y no al directorio actual.- Si
alembic_version.version_numquedó enVARCHAR(32), el arranque la ensancha automáticamente aVARCHAR(255)para soportar identificadores de revisión largos. - Cada ejecución de
scripts\run_migrations.cmdgenera un archivo enlogs\migrations\alembic_YYYYMMDD_HHMMSS.logcon todo elstdoutystderrde Alembic. - Si el arranque se detiene por un error de migración, revisar la ruta indicada y solucionar el problema antes de volver a ejecutar
scripts\start.bat. - Al invocar Alembic manualmente, las opciones globales como
--raiseerry-x log_sql=1deben ubicarse antes del subcomando.log_sql=1activasqlalchemy.echopara registrar cada consulta. Ejemplo:
alembic --raiseerr -x log_sql=1 -c alembic.ini upgrade head
cd frontend
npm install
npm run devEn desarrollo, Vite proxya /ws, /chat y /actions hacia http://localhost:8000, evitando errores de CORS. Durante el arranque pueden mostrarse errores de proxy WebSocket si la API aún no está disponible; una vez arriba, la conexión se restablece sola. El chat abre un WebSocket en /ws y, si no está disponible, utiliza POST /chat, que admite la variante con o sin barra final para evitar redirecciones 307. El servidor envía un ping cada 30 s y corta la sesión tras 60 s sin recibir datos; el frontend ignora esos pings, cierra limpiamente y reintenta con backoff exponencial si la conexión se pierde. Para modificar las URLs se puede crear frontend/.env.development con VITE_WS_URL y VITE_API_BASE.
- La UI incluye un botón flotante global (abajo a la derecha) para enviar reportes manuales de errores o problemas.
- Opcionalmente adjunta una captura de pantalla del estado actual (guardada como archivo en
logs/bugreport_screenshots/). - Los reportes se registran en
logs/BugReport.logdel backend mediantePOST /bug-report. - Más info en
docs/BUG_REPORTS.md.
El backend sirve el build de Vite directamente y aplica un fallback de SPA para que al refrescar rutas del cliente (por ejemplo /productos o /stock) no se produzca 404.
- Activos estáticos del bundle:
GET /assets/*(montados conStaticFiles). - Rutas API y documentación se registran antes; el fallback no las intercepta.
- Fallback: cualquier ruta no API ni estática devuelve
index.html.
Requisitos del build:
vite.configconbase: '/'.- El
index.htmlreferencia los activos bajo/assets/.
Pruebas manuales:
- Abrir
/productosy presionar F5: debe renderizar sin404. - Abrir
/stocky presionar F5: debe renderizar sin404. - Solicitar
/assets/<archivo>.jsdevuelve el asset. - Endpoints como
/products,/auth/me,/docsdeben seguir funcionando normalmente.
- Edición inline de Precio de venta (canónico) con guardado en
Enter/onBluryEscpara cancelar. Soloadminycolaboradorven controles de edición. - Panel de Comparativa por producto con ofertas de proveedores, ordenadas por menor precio de compra; permite editar precio de compra inline (roles
admin|colaborador). - Preferencias de columnas por usuario (orden, visibilidad, anchos) persistidas en backend. Botón “Diseño” para configurar y “Restaurar diseño” para volver a valores por defecto.
- Edición masiva de precio de venta con modos
set|inc|dec|inc_pct|dec_pct(modal). Requiere CSRF.
Endpoints relevantes (precio y compras), prefijo /products-ex para precios:
PATCH /products/{product_id}/sale-price(admin, colaborador; CSRF)PATCH /supplier-items/{supplier_item_id}/buy-price(admin, colaborador; CSRF)
DELETE /catalog/products(CSRF; rolesadmin|colaborador). Cuerpo{ "ids": number[] }.- Reglas de negocio:
- 400 si el producto tiene
stock > 0. - 409 si el producto posee referencias en compras (
purchase_lines.product_id). - Si no hay bloqueos: se eliminan dependencias compatibles sin ON DELETE CASCADE:
supplier_products,variants,inventorieseimages; luego elproduct.
- 400 si el producto tiene
- Respuesta:
{ requested, deleted, blocked_stock?: number[], blocked_refs?: number[] }. - Auditoría:
AuditLog { action: "product_delete" }por cada producto con metadatos de cascada.
- Se filtran líneas duplicadas por
supplier_skuy portitlenormalizado (trim, lower, sin tildes). - Se registran métricas en
ImportLogcon nivelWARN:ignored_duplicates_by_skuyignored_duplicates_by_title.
- La respuesta de
POST /purchases/import/santaplantaincluyelines_unique,ignored_by_skueignored_by_titleen metadatos y encabezadoX-Correlation-Idpara trazar logs. - Auditoría de import con estos campos para trazabilidad.
POST /products/bulk-sale-price(admin, colaborador; CSRF)GET /products/{product_id}/offeringsGET/PUT /users/me/preferences/products-table(PUT requiere CSRF)
Soporte para crear productos internos manualmente (roles admin y colaborador).
Backend:
POST /catalog/products(CSRF) JSON mínimo (modal rápido):{ "title": "Nombre", "initial_stock": 0, "supplier_id": 1, "supplier_sku": "OPCIONAL", "sku": "SKU-INTERNO-OPC", "purchase_price": 100.0, "sale_price": 150.0 }- Requiere proveedor y precios de compra/venta.
- Campo
sku(opcional) permite forzar un SKU interno distinto del del proveedor; si se omite usasupplier_skuo eltitlenormalizado (truncado a 50). Validación regex:[A-Za-z0-9._\-]{2,50}. - Crea
Product+Variant(SKU =sku_root), valida SKU único global antes de insertar (pre-check) y el constraint garantiza consistencia. - Crea
SupplierProducty registracurrent_purchase_price/current_sale_pricee historial ensupplier_price_history. - Respuesta incluye
id,sku_root,supplier_item_id. - Compatibilidad: el endpoint
/productscompleto sigue disponible para flujos avanzados (categoría, estado, enlaces canónicos). - Si el SKU ya existe, devuelve 409 con detalle.
Notas adicionales:
- Se registra un
SupplierPriceHistoryinicial con los precios enviados. initial_stock> 0 crea registro eninventoryy sincronizaproducts.stock.
Eliminación segura (DELETE /catalog/products):
- Reglas single-id: 400 si stock > 0; 409 si referencias en
purchase_lines. - Éxito: elimina en orden manual dependencias sin ON DELETE CASCADE:
supplier_price_history,supplier_products,variants,inventory,images, luegoproducty registraaudit_logcon conteos. - Respuesta:
{ requested, deleted, blocked_stock, blocked_refs }.
Frontend:
- Botón "Nuevo producto" en panel Productos abre modal.
- Formulario: Nombre (requerido), Categoría (lazy load al enfocar), Stock inicial (≥0).
- Al crear reinicia a página 1 y refresca lista, muestra toast.
Validaciones:
initial_stock >= 0.category_iddebe existir si se envía.
Limitaciones actuales / posibles mejoras:
- Sin variantes automáticas ni carga de imágenes en el modal.
- Futuro: clonación, importación CSV, set de atributos iniciales.
- Preguntá "¿cuánto sale ?" o "¿tenés en stock?" para obtener precio y disponibilidad con badge de stock.
- Usá
/stock <sku>o mencioná SKUs internos/proveedor para coincidencias exactas. - La respuesta del bot incluye proveedor, SKU y variantes relevantes; si no encuentra nada, ofrece abrir el listado de Productos.
- Arrastrá y soltá un archivo
.xlsxo.csvsobre la zona punteada encima del chat para abrir el modal de carga. - También podés usar el botón Adjuntar Excel.
- El modal muestra nombre y tamaño del archivo y habilita Subir solo cuando hay proveedor seleccionado.
- Se validan formato y tamaño antes de enviar. El límite se define con
VITE_MAX_UPLOAD_MB. - Solo los roles
proveedor,colaboradoryadminven la opción de adjuntar. Si el usuario esproveedor, susupplier_idqueda preseleccionado.
La API implementa sesiones mediante la cookie growen_session y un token CSRF almacenado en csrf_token. Cada vez que se inicia o cierra sesión se generan nuevos valores para ambas cookies, evitando la fijación de sesiones. Todas las mutaciones deben enviar el encabezado X-CSRF-Token coincidiendo con dicha cookie. Las rutas que modifican datos añaden dependencias require_roles para comprobar que el usuario posea el rol autorizado.
Si no hay cookie de sesión y el entorno es dev, se asume rol admin por defecto para agilizar pruebas; en otros entornos el rol por omisión es guest.
El login acepta identificador o email junto con la contraseña. Una migración idempotente agrega la columna identifier si falta y la rellena a partir del correo; esto permite que bases antiguas sigan funcionando. Al ejecutar las migraciones se crea, si no existe, un usuario administrador usando ADMIN_USER y ADMIN_PASS definidos en .env (ver .env.example). En producción el servidor se niega a iniciar si ADMIN_PASS queda en el placeholder REEMPLAZAR_ADMIN_PASS.
Nota sobre fallback en desarrollo: si ADMIN_PASS está en placeholder y el entorno es dev, el sistema (config, migración y script seed_admin.py) usa la contraseña temporal admin1234. Esta contraseña SOLO es válida para entornos locales y debe reemplazarse siempre en producción definiendo un valor seguro en .env antes de iniciar la aplicación. Cualquier entorno distinto de dev abortará el arranque si persiste el placeholder.
POST /auth/loginvalida credenciales por identificador o email y genera una sesión nueva.POST /auth/guestcrea una sesión con rolguestsin usuario, regenerando el token.POST /auth/logoutcierra la sesión, crea una nueva sesión de invitado y regenera el token (requiere CSRF).GET /auth/meinforma el estado actual.GET /auth/userslista usuarios (solo admin).POST /auth/userscrea usuarios (solo admin, requiere CSRF).PATCH /auth/users/{id}actualiza usuarios (solo admin, requiere CSRF).POST /auth/users/{id}/reset-passwordregenera la contraseña (solo admin, requiere CSRF).
| Rol | Permisos principales |
|---|---|
| invitado | Solo lectura |
| cliente | Solo lectura |
| proveedor | Subir Excel de su proveedor asignado |
| colaborador | Subir Excel y aplicar importaciones de cualquier proveedor |
| admin | Todos los permisos, incluyendo registrar usuarios |
La lista completa de rutas y roles se encuentra en docs/roles-endpoints.md.
SECRET_KEY=REEMPLAZAR_SECRET_KEY
# ADMIN_USER y ADMIN_PASS se definen en .env (ver .env.example);
# en producción cambie los placeholders
SESSION_EXPIRE_MINUTES=1440 # duración de la sesión en minutos (1 día recomendado)
AUTH_ENABLED=true
# se ignora en producción; allí siempre es true
COOKIE_SECURE=false
COOKIE_DOMAIN=# Token del bot obtenido de @BotFather en Telegram
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
# Habilitar integración de Telegram (1, true o yes)
TELEGRAM_ENABLED=1
# Chat ID numérico por defecto para notificaciones
# Obtener escribiendo al bot y consultando:
# https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates
TELEGRAM_DEFAULT_CHAT_ID=123456789Opcionales (para webhook en producción):
# Token secreto para proteger el endpoint del webhook
TELEGRAM_WEBHOOK_TOKEN=token_secreto_dificil_de_adivinar
# Secret opcional para validar el header X-Telegram-Bot-Api-Secret-Token
TELEGRAM_WEBHOOK_SECRET=secret_opcionalOpcionales (para polling en desarrollo local):
# Timeout en segundos para long polling (default: 30)
TELEGRAM_POLLING_TIMEOUT=30
# Delay en segundos entre reintentos en caso de error (default: 5)
TELEGRAM_POLLING_RETRY_DELAY=5Notas:
TELEGRAM_BOT_TOKENes obligatorio para que funcione el chatbot y las notificaciones.- Si
TELEGRAM_ENABLED=0(o no está definido), toda la funcionalidad de Telegram se desactiva. - Para obtener el
TELEGRAM_BOT_TOKEN: crear un bot con @BotFather en Telegram. - Para obtener el
TELEGRAM_DEFAULT_CHAT_ID: escribir al bot y consultargetUpdatescon el token.
SECRET_KEY y las credenciales iniciales (ADMIN_USER y ADMIN_PASS, definidas en .env) deben reemplazarse por valores robustos en producción.
En entornos de desarrollo se usarán valores de prueba si se dejan en los placeholders, pero conviene ajustarlos igualmente.
Mantener estas claves fuera del control de versiones y rotarlas periódicamente.
SESSION_EXPIRE_MINUTES define cuánto tiempo permanece válida una sesión.
El valor recomendado de 1440 mantiene la sesión durante un día. Al expirar,
el usuario debe volver a autenticarse. Valores más altos reducen la frecuencia
de inicio de sesión pero incrementan el riesgo ante robo de cookies; valores
más bajos obligan a reautenticarse con mayor frecuencia y elevan la seguridad.
La interfaz presenta una botonera fija sobre el chat con accesos rápidos:
- Adjuntar Excel abre el modal de carga de listas de precios.
- Proveedores muestra la gestión básica de proveedores (listar y crear).
- Productos abre un panel para buscar en la base, ajustar stock y gestionar canónicos: permite editar fichas canónicas y vincular equivalencias manualmente. Los resultados se cargan bajo demanda al desplazarse gracias a
react-window. - Usuarios despliega el panel de administración para listar, crear, editar y restablecer contraseñas. Solo es visible para el rol
admin.
La barra queda visible al hacer scroll y usa un estilo mínimo con sombreado suave.
El panel accesible en /admin consume los endpoints de autenticación para gestionar cuentas:
GET /auth/userslista los usuarios existentes con su rol.POST /auth/userscrea nuevas cuentas asignando nombre, email y rol.PATCH /auth/users/{user_id}permite actualizar el rol o desactivar usuarios.POST /auth/users/{user_id}/reset-passwordgenera una contraseña temporal y la devuelve en la respuesta.
Todas estas operaciones requieren el rol admin y envían encabezado X-CSRF-Token.
El frontend define un esquema de color gris con acentos violeta (#7C4DFF) y verde (#22C55E).
Un botón en la barra permite alternar el tema y, por defecto, se respeta prefers-color-scheme del sistema.
- HTTP:
POST /chatcon cuerpo{ "text": "hola" }→ responde{ "role": "assistant", "text": "..." }. - WebSocket: se envía texto plano y cada mensaje recibido es un JSON
{ "role": "assistant", "text": "..." }. El servidor agrega pings periódicos{ "role": "ping" }para mantener viva la conexión y la cierra tras 60 s sin actividad; el cliente los descarta y reintenta con backoff exponencial si se pierde el canal. - Sesión: si la cookie
growen_sessionestá presente, el backend incluye el nombre y rol del usuario en el prompt para personalizar la respuesta de la IA. - Proveedor: Ollama es el motor por defecto (
OLLAMA_MODEL=llama3.1). El backend intenta primero constream=Falsey, si la API falla, cae a modo streaming acumulando las partes. En ambos casos normaliza la respuesta y remueve prefijos comoollama:antes de reenviarla.
La interfaz muestra las respuestas del asistente con la etiqueta visual Growen.
Flujo básico: upload → preview → commit.
La API permite subir archivos de proveedores en formato .xlsx para revisar y aplicar nuevas listas de precios.
POST /suppliers/{supplier_id}/price-list/uploadrecibe el archivo del proveedor (campofileenmultipart/form-data) y un parámetrodry_run(por defectotrue). Es obligatorio que el proveedor exista y tenga un parser registrado.GET /imports/{job_id}/preview?status=new,changed&page=1&page_size=50lista las filas normalizadas filtradas porstatusy paginadas. La respuesta devuelve{items, summary, total, pages, page}y permite inspeccionar tambiénstatus=error,duplicate_in_filepara los fallos. Durante esta vista previa es posible crear o editar productos canónicos y vincular equivalencias manualmente desde cada fila.POST /imports/{job_id}/commitaplica los cambios, creando categorías, productos y relaciones ensupplier_products.
Cada proveedor define su mapeo en config/suppliers/*.yml. Por cada archivo se genera automáticamente un GenericExcelParser.
También pueden agregarse parsers especializados instalando paquetes que expongan un entry_point en el grupo growen.suppliers.parsers.
Para depurar los parsers habilitados se puede llamar a GET /debug/imports/parsers, disponible solo para administradores y deshabilitado en producción.
| Proveedor | Configuración |
|---|---|
santa-planta |
config/suppliers/santa-planta.yml |
En modo dry-run se puede revisar el contenido antes de confirmar los cambios definitivos.
Las tablas import_jobs e import_job_rows guardan cada archivo cargado y sus filas normalizadas.
supplier_price_history registra los cambios de precios para auditoría.
GET /price-history permite consultar ese historial filtrando por supplier_product_id o product_id y admite paginación. Solo está disponible para los roles cliente, proveedor, colaborador y admin.
GET /suppliers/price-list/template devuelve una plantilla genérica con la hoja data y los encabezados:
ID, Agrupamiento, Familia, SubFamilia, Producto, Compra Minima, Stock, PrecioDeCompra, PrecioDeVenta.
GET /suppliers/{supplier_id}/price-list/template genera la misma estructura pero permite personalizar el nombre del archivo según el proveedor.
Ambas rutas requieren un rol válido (cliente, proveedor, colaborador o admin).
La celda A1 incluye una nota con instrucciones y la fila 2 trae un ejemplo. En el modal de carga hay un botón Descargar plantilla genérica que llama a GET /suppliers/price-list/template y otro Descargar plantilla que usa GET /suppliers/{supplier_id}/price-list/template para obtener el archivo específico antes de completar los datos.
La interfaz de chat incluye un botón + y la opción de la botonera Adjuntar Excel para subir listas de precios sin pasar por la IA.
- Hacer clic en Adjuntar Excel o arrastrar un archivo
.xlsxsobre la ventana. - El modal exige elegir un proveedor; si no existen proveedores se muestra un estado vacío con el botón Crear proveedor.
- Tras seleccionar proveedor y archivo, el frontend llama a
POST /suppliers/{supplier_id}/price-list/upload?dry_run=true. - Growen envía un mensaje de sistema con el
job_idy abre un visor que pagina las filas llamando aGET /imports/{job_id}/preview, mostrando el total de filas, la página actual y el número de páginas devueltos por la API. - El visor abre la pestaña Cambios por defecto para resaltar las variaciones y muestra el recuento en cada pestaña; desde allí se pueden filtrar errores y finalmente ejecutar
POST /imports/{job_id}/commit.
Errores comunes:
- 400 columnas faltantes.
- 413 tamaño excedido (límite
MAX_UPLOAD_MB).
El visor trabaja de forma paginada llamando a GET /imports/{job_id}/preview. Como atajo, GET /imports/{job_id} devuelve status, summary y las primeras filas para hidratar vistas iniciales sin paginación.
- La pestaña Cambios solicita
status=new,changedpara concentrar las filas a aplicar. - Errores y Duplicados en archivo reutilizan el mismo endpoint variando
status. - Cada respuesta entrega
{items, summary, total, pages, page}con los totales por tipo de fila. - Desde cada fila pueden crearse productos canónicos o equivalencias antes de confirmar.
- Al finalizar la revisión se envía
POST /imports/{job_id}/commitpara persistir los ajustes.
Para comparar precios entre proveedores se mantiene un catálogo propio de productos canónicos. Cada oferta puede asociarse a uno de ellos mediante equivalencias (ver sección siguiente).
-
El frontend incluye el formulario CanonicalForm para crear o editar estos registros. El SKU propio (
sku_custom) puede:- Autogenerarse con el botón "Auto" usando el patrón
XXX_####_YYY(prefijo/sufijo derivados de categoría y subcategoría, secuencia por categoría). - Ser editado manualmente con validación de unicidad en backend.
- Autogenerarse con el botón "Auto" usando el patrón
-
Crear canónico:
POST /canonical-productsconname,brandyspecs_jsonopcional. El sistema generang_skucon el formatoNG-000001y, si no se proveesku_custom, genera uno canónico como se indicó arriba. -
Buscar canónicos:
GET /canonical-products?q=&page=permite paginar y filtrar. -
Detalle/edición:
GET /canonical-products/{id}yPATCH /canonical-products/{id}devuelven y actualizan un canónico. -
Comparador:
GET /canonical-products/{id}/offersordena las ofertas por precio de venta y marca la mejor conmejor_precio.
Notas de UX:
- Al crear un canónico desde la lista de productos (columna "Canónico" → "Nuevo"), el formulario se abre con el nombre del producto del proveedor prellenado. Al guardar, se autovincula una equivalencia con esa oferta del proveedor (si existe
supplier_item_id).
Las equivalencias enlazan una oferta de proveedor (supplier_product) con un producto canónico para habilitar la comparación de precios.
El componente EquivalenceLinker permite gestionar estos vínculos desde la interfaz.
- Vincular oferta:
POST /equivalencesune unsupplier_productexistente con uncanonical_product. - Listar equivalencias:
GET /equivalences?supplier_id=&canonical_product_id=soporta filtros y paginación. - Eliminar equivalencia:
DELETE /equivalences/{id}.
El endpoint GET /canonical-products/{id}/offers devuelve todas las ofertas vinculadas a un canónico ordenadas por precio, destacando el mejor con el campo mejor_precio. Desde la interfaz se accede a esta tabla desde el visor de importación y el panel de productos cuando el artículo tiene una equivalencia canónica.
Variables de entorno relevantes:
AUTO_CREATE_CANONICAL=true
FUZZY_SUGGESTION_THRESHOLD=0.87
SUGGESTION_CANDIDATES=3Estas opciones controlan la creación automática y las sugerencias durante la
importación de listas. Las coincidencias se calculan con rapidfuzz y solo se
aceptan si superan el umbral FUZZY_SUGGESTION_THRESHOLD.
GET /products lista los productos disponibles con filtros, orden y paginación. Requiere los roles cliente, proveedor, colaborador o admin.
Parámetros soportados:
supplier_id: filtra por proveedor.category_id: filtra por categoría interna.q: búsqueda parcial por nombre del producto o título del proveedor.pageypage_size: paginación (por defecto1y20).sort_by:updated_at,precio_venta,precio_compraoname.order:ascodesc.type:all(default),canonicalosupplier. Permite alternar entre ver solo filas con canónico, solo ofertas de proveedor o todo.
Si se envían otros valores en sort_by u order, la API responde 400 Bad Request.
Ejemplo de respuesta:
{
"page": 1,
"page_size": 20,
"total": 1,
"items": [
{
"product_id": 1,
"name": "Carpa Indoor 80x80",
"supplier": {"id": 1, "slug": "santa-planta", "name": "Santa Planta"},
"precio_compra": 10000.0,
"precio_venta": 12500.0,
"compra_minima": 1,
"category_path": "Carpas>80x80",
"stock": 0,
"updated_at": "2025-08-15T20:33:00Z"
}
]
}Este endpoint se utiliza para consultar el catálogo existente desde el frontend.
Comportamiento de campos (fallback canónico → proveedor):
- Si un producto está vinculado a un canónico, la UI prioriza
canonical_sale_priceycanonical_namecuando están presentes; si no, cae aprecio_ventaysupplier_titledel proveedor.
Para modificar el stock manualmente existe PATCH /products/{id}/stock con cuerpo { "stock": <int> }.
GET /price-history devuelve el historial de precios ordenado por fecha.
Debe indicarse supplier_product_id o product_id y se puede paginar con page y page_size.
La respuesta incluye purchase_price, sale_price y sus variaciones porcentuales (delta_purchase_pct, delta_sale_pct).
Solo los roles cliente, proveedor, colaborador o admin pueden consultarlo y el panel de productos enlaza a esta vista para auditoría.
Levanta API y frontend al mismo tiempo.
Ejecutar desde CMD con doble clic en scripts\start.bat. El script realiza estas etapas:
- Llama a
scripts\stop.batpara liberar los puertos 8000 y 5173. - Aplica las migraciones mediante
scripts\migrate.baty guarda el log enlogs\migrations\alembic_YYYYMMDD_HHMMSS.log. - Abre dos ventanas:
- Growen API (Uvicorn) en http://127.0.0.1:8000/docs
- Growen Frontend (Vite) en http://127.0.0.1:5173/
Requisitos previos:
- Python 3.11 (si no existe un virtualenv,
scripts\start.batintentará crearlo automáticamente) - Node.js/npm instalados (si faltan paquetes de frontend,
scripts\start.batejecutaránpm installenfrontendcuando sea necesario) .envcompletado (DB_URL, IA, etc.)frontend/.envcreado a partir defrontend/.env.examplesi se necesita ajustarVITE_API_URL.
Comportamiento de auto-configuración de scripts\start.bat:
- Si no existe
.venv, el script intentará crear un entorno virtual en.venvy actualizarpip/setuptools. - Tras crear el virtualenv, se ejecuta
python -m tools.doctor. Si la variable de entornoALLOW_AUTO_PIP_INSTALL=trueestá definida, el doctor intentará instalarrequirements.txtautomáticamente. - Si
tools.doctordetecta problemas críticos, el script pausará y te dará la opción de abortar o continuar. - Si
frontend/node_modulesno existe,scripts\start.batejecutaránpm installdentro defrontend.
Esto facilita un inicio de desarrollo “1‑clic” en máquinas nuevas.
Para detener manualmente los servicios, ejecutar scripts\stop.bat desde CMD.
PowerShell no requerido (los scripts son CMD puro).
Para iniciar solo el backend en Windows se puede ejecutar scripts\run_api.cmd, que detiene procesos previos, instala dependencias, aplica migraciones y guarda la salida de Uvicorn en logs/backend.log. El script también escribe información de depuración, como rutas base y códigos de retorno, en logs/run_api.log.
Los .bat están preparados para ejecutarse desde rutas como C:\\Nice Grow\\Agentes\\Growen sin errores de sintaxis:
- Todas las rutas se envuelven entre comillas.
- Se usa
pushd/popden lugar decdpara cambiar de directorio. scripts\start.batencadenastop→migrate→api + frontenden ventanas separadas.- Para registrar cada consulta SQL en el log de migraciones ejecutar
scripts\start.bat /sql.
Nota de compatibilidad (psycopg asíncrono): en Windows la aplicación establece WindowsSelectorEventLoopPolicy al iniciar para evitar errores del conector asíncrono de PostgreSQL.
chmod +x start.sh
./start.shRequisitos previos: entorno virtual creado (python -m venv .venv), pip install -e ., Node.js instalado y .env con DB_URL y OLLAMA_MODEL=llama3.1. El backend escucha en http://localhost:8000 y el frontend en http://localhost:5173.
En Windows puede aparecer un aviso de firewall; permitir el acceso para ambos puertos. Si alguna de las aplicaciones no inicia, verificar que los puertos 8000 y 5173 estén libres.
Modelos Ollama: instalar Ollama y ejecutar ollama pull llama3.1. Si la descarga falla, probar con ollama pull llama3 u otra variante disponible. La variable OLLAMA_MODEL apunta por defecto a llama3.1.
docker compose up --buildLevanta PostgreSQL, API en :8000 y frontend en :5173.
Las migraciones se administran con Alembic usando la carpeta db/migrations. El archivo env.py carga automáticamente las
variables definidas en .env, por lo que no es necesario configurar la URL en alembic.ini.
cp .env.example .env # en Windows usar: copy .env.example .env
# Completar DB_URL y, en producción, definir SECRET_KEY y las credenciales ADMIN_USER/ADMIN_PASS reemplazando los placeholders
alembic -c ./alembic.ini upgrade head
# Crear una nueva revisión a partir de los modelos
alembic -c ./alembic.ini revision -m "descripcion" --autogenerate
# Aplicar las migraciones pendientes
alembic -c ./alembic.ini upgrade head
# Revertir la última migración
alembic -c ./alembic.ini downgrade -1Consulta .env.example para la lista completa. Variables destacadas:
DB_URL: URL de PostgreSQL (obligatoria; la aplicación no arranca si falta. Si la contraseña tiene caracteres reservados, encodéalos, ej.:=→%3D. Si tu contraseña tiene caracteres raros, ponela sin encodar en variables separadas y construí la URL conSQLAlchemy URL.create(); pero si usásDB_URLya encodada, elenv.pyahora la maneja bien.).ENV: entorno de ejecución (dev,production). Endevse completan orígenes locales y se flexibilizan claves por defecto para facilitar pruebas.AI_MODE:auto,openaiuollama.AI_ALLOW_EXTERNAL: si esfalse, solo se usa Ollama.OLLAMA_URL: URL base de Ollama (por defectohttp://localhost:11434).OLLAMA_MODEL: modelo de Ollama (por defectollama3.1).OPENAI_API_KEY,OPENAI_MODEL.AI_MAX_TOKENS_SHORT,AI_MAX_TOKENS_LONG: límites de tokens para respuestas cortas/largas.AI_TIMEOUT_OLLAMA_MS,AI_TIMEOUT_OPENAI_MS: timeouts de peticiones a proveedores.SECRET_KEY: clave usada para firmar sesiones; en producción reemplace el placeholderREEMPLAZAR_SECRET_KEY, rote el valor periódicamente y manténgalo fuera del control de versiones. En desarrollo se usa un valor de prueba si no se define uno propio.SESSION_EXPIRE_MINUTES: tiempo de expiración de la sesión en minutos (por defecto 1440 = 1 día). Incrementarlo prolonga las sesiones pero aumenta el riesgo ante robo de cookies; reducirlo fuerza reautenticaciones más frecuentes y eleva la seguridad.COOKIE_SECURE: activa cookies seguras; se ignora en producción donde siempre están habilitadas.ALLOWED_ORIGINS: orígenes permitidos para CORS, separados por coma. En desarrollo se completan automáticamente los pareslocalhost/127.0.0.1; en producción se debe especificar cada dominio explícitamente.LOG_LEVEL: nivel de logging de la aplicación (DEBUG,INFO, etc.).DEBUG_SQL: si vale1, SQLAlchemy mostrará cada consulta ejecutada.ADMIN_USER,ADMIN_PASS: credenciales del administrador inicial definidas en.env(copiado desde.env.example). En producción la aplicación aborta el inicio siADMIN_PASSqueda en el placeholderREEMPLAZAR_ADMIN_PASS.MAX_UPLOAD_MB: tamaño máximo de archivos a subir.AUTH_ENABLED: si estrue, requiere sesión autenticada.PRODUCTS_PAGE_MAX: límite máximo de resultados por página.PRICE_HISTORY_PAGE_SIZE: tamaño por defecto al paginar el historial de precios.
IMPORT_OCR_LANG: Idioma para Tesseract OCR (por defectospa).IMPORT_OCR_TIMEOUT: Timeout en segundos para el proceso de OCR (por defecto180).IMPORT_PDF_TEXT_MIN_CHARS: Mínimo de caracteres de texto a extraer de un PDF para considerarlo válido sin OCR (por defecto100).IMPORT_ALLOW_EMPTY_DRAFT: Si estrue(default), al importar un PDF sin líneas detectables, se crea una compra enBORRADORvacía. Si esfalse, se devuelve un error422.
TELEGRAM_BOT_TOKEN: Token del bot obtenido de @BotFather en Telegram. Formato:123456789:ABCdefGHIjklMNOpqrsTUVwxyz. Sin este token, el chatbot y las notificaciones no funcionarán.TELEGRAM_ENABLED: Habilitar integración de Telegram. Valores:1,trueoyespara habilitar;0,falseono(o no definido) para deshabilitar.TELEGRAM_DEFAULT_CHAT_ID: Chat ID numérico por defecto para notificaciones. Obtener escribiendo al bot y consultandohttps://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates.
Opcionales (para webhook en producción):
TELEGRAM_WEBHOOK_TOKEN: Token secreto para proteger el endpoint del webhook. Elegir una cadena difícil de adivinar (recomendado: generar conpython -c "import secrets; print(secrets.token_urlsafe(32))").TELEGRAM_WEBHOOK_SECRET: Secret opcional para validar el headerX-Telegram-Bot-Api-Secret-Token. Si se define, el webhook validará este header además del path token.
Opcionales (para polling en desarrollo local):
TELEGRAM_POLLING_TIMEOUT: Timeout en segundos para long polling (por defecto30).TELEGRAM_POLLING_RETRY_DELAY: Delay en segundos entre reintentos en caso de error (por defecto5).
Cómo obtener el Token del Bot:
- Abrir Telegram y buscar @BotFather.
- Enviar el comando
/newbot. - Seguir las instrucciones para darle un nombre y username al bot.
- BotFather te dará un token que debes copiar y pegar en
TELEGRAM_BOT_TOKEN.
Cómo obtener el Chat ID:
- Escribir un mensaje a tu bot (puede ser cualquier mensaje).
- Consultar:
https://api.telegram.org/bot<TU_TOKEN>/getUpdates. - Buscar en la respuesta el campo
chat.iddel mensaje que enviaste.
Rutas públicas de salud:
GET /health: responde{"status":"ok"}si la app está viva.GET /health/ai: informa los proveedores de IA disponibles.GET /healthz/db: realizaSELECT 1contra la base y devuelve{"db":"ok"}.
Rutas de diagnóstico para administradores (omitidas en producción):
GET /healthz: responde{"status":"ok"}si la app está viva.GET /debug/db: ejecutaSELECT 1contra la base de datos.GET /debug/config: muestraALLOWED_ORIGINSy laDB_URLsin contraseña.GET /debug/imports/parsers: enumera los parsers registrados para las importaciones.GET /admin/import/doctor: verifica la presencia de dependencias externas para OCR (ocrmypdf,tesseract, etc.).
La API incluye un middleware que registra cada solicitud HTTP con metodo, ruta, codigo de respuesta y tiempo de respuesta. Las excepciones se capturan y se registran con traza.
- Configuracion:
LOG_LEVELcontrola el nivel de detalle;DEBUG_SQL=1muestra las consultas SQL. - Ubicacion de logs: los scripts de arranque redirigen Uvicorn a
logs/backend.logy dejan trazas de migraciones enlogs/migrations/.
Notas adicionales de entorno:
HOST,PORT: host y puerto del servidor de desarrollo.ALEMBIC_LOG_LEVELyDEBUG_MIGRATIONS: controlan el detalle de logs de migraciones y diagnosticos.- En
ENV=dev, siSECRET_KEYyADMIN_PASSquedan en placeholders se usan valores de prueba; en produccion el arranque aborta si no se reemplazan. - SQLite opcional (dev/pruebas):
DB_URL=sqlite+aiosqlite:///ruta.db.
En el chat o vía API se pueden usar:
/help/sync pull --dry-run/sync push --dry-run/stock adjust --sku=SKU --qty=5/import archivo.xlsx --supplier=SLUG/import last --apply/search maceta
La ruta GET /actions devuelve acciones rápidas.
El endpoint de chat y el WebSocket analizan cada mensaje para detectar comandos.
- Si el texto corresponde a un intent conocido, se ejecuta el handler asociado y se retorna una respuesta estructurada.
- Cuando el intent es desconocido, se invoca
AIRouter.runcon la tareaTask.SHORT_ANSWERpara generar una contestación libre mediante IA.
El WebSocket utiliza la misma lógica para cada mensaje entrante y, ante una desconexión del cliente (WebSocketDisconnect), Starlette cierra el canal automáticamente, por lo que el servidor no invoca close() manualmente.
Cuando el proveedor de IA elegido no soporta la tarea solicitada, el ruteador registra una advertencia y cambia a Ollama como alternativa.
Permite subir archivos .csv o .xlsx de distintos proveedores para poblar el catálogo interno.
- El stock inicial siempre se crea en
0. - Los campos se normalizan según mapeos en
config/suppliers/*.yml. - Se puede ejecutar desde el chat o por CLI:
python -m cli.ng ingest file datos.xlsx --supplier default --dry-runCon --dry-run se generan reportes en data/reports/ sin tocar la base. Al aplicar sin ese flag se insertan/actualizan productos y variantes.
Si el archivo no incluye SKU ni GTIN se genera uno interno estable. Las categorías y marcas se crean si no existen y los productos quedan en estado draft por defecto.
- En el chat adjuntá el Excel
ListaPrecios_export_XXXX.xlsx. - Growen detecta automáticamente el proveedor y ejecuta un dry-run.
- Revisá los reportes generados en
data/reports/. - Para aplicar los cambios ejecutá
/import last --applyen el chat o:
python -m cli.ng ingest file ListaPrecios_export_XXXX.xlsx --supplier santa-planta --dry-run
python -m cli.ng ingest last --applyCada ingestión registra los precios de compra y venta en la tabla supplier_price_history con las variaciones porcentuales respecto del último valor conocido.
Los productos tienen la columna stock en products con valor inicial 0.
La importación de listas de precios no modifica este valor; se ajusta manualmente desde el buscador o vía API.
Desde la botonera puede abrirse un modal que lista los proveedores actuales y permite crear nuevos ingresando Nombre y Slug. El slug debe ser único y se utiliza para asociar parsers y archivos, por lo que conviene mantenerlo estable.
La API expone endpoints para administrar proveedores externos:
GET /supplierslista todos los proveedores con la cantidad de archivos cargados. Requiere rolcliente,proveedor,colaboradoroadmin.POST /supplierscrea un nuevo proveedor validando que elslugsea único.PATCH /suppliers/{id}actualiza el nombre de un proveedor existente.GET /suppliers/{id}/filesmuestra los archivos cargados por un proveedor. Requiere rolcliente,proveedor,colaboradoroadmin.
Estos recursos facilitan la organización de las distintas listas de precio y su historial.
Se puede proponer y generar la jerarquía de categorías a partir de un archivo de proveedor:
POST /categories/generate-from-supplier-file
{
"file_id": 1,
"dry_run": true
}Con dry_run=true solo se informa qué rutas de categoría se detectarían. Si se envía dry_run=false, las categorías faltantes se crean respetando la jerarquía parent_id.
Además, GET /categories lista las categorías con su ruta completa y GET /categories/search?q= permite búsquedas parciales.
La política por defecto utiliza:
- Ollama para NLU y respuestas cortas.
- OpenAI para generación de contenido.
Instala Ollama y descarga el modelo configurado. Para deshabilitar proveedores externos establece AI_ALLOW_EXTERNAL=false.
Para comprobar las mutaciones desde el navegador se documentan pruebas manuales en tests/manual/e2e-mutations.md.
python -m cli.ng db-init- M0: estructura base y stubs (este repositorio)
- M1: integraciones e-commerce adicionales
- M2: mejoras de IA y comandos
- M3: despliegue completo
Contribuciones y feedback son bienvenidos.
Feature para generar un PDF de catálogo seleccionando productos desde la vista Stock.
Endpoints (/catalogs/*, roles: admin y colaborador):
POST /catalogs/generatecuerpo{ "ids": [...] }genera un archivo timestampcatalog_YYYYMMDD_HHMMSS.pdfy actualizaultimo_catalogo.pdf(symlink o copia).GET /catalogslista catálogos existentes con paginación y filtros:- Query params:
page=1,page_size=20(<=500),from_dt=YYYY-MM-DD,to_dt=YYYY-MM-DD. - Respuesta:
{items:[{id,filename,size,modified_at,latest}], total, page, page_size, pages}ordenados desc.
- Query params:
GET /catalogs/{id}/HEAD /catalogs/{id}/GET /catalogs/{id}/downloadaccesos por id (formatoYYYYMMDD_HHMMSS).HEAD /catalogs/latestverifica existencia del alias.GET /catalogs/latestsirve inline el más reciente.GET /catalogs/latest/downloaddescarga el más reciente.GET /catalogs/export.csvexporta la lista (mismos filtrosfrom_dt,to_dt).DELETE /catalogs/{id}elimina el catálogo indicado. Si el eliminado era el que apuntabaultimo_catalogo.pdf, se reasigna el alias al siguiente más reciente (orden pormtime). Si no quedan catálogos, el alias se elimina. Respuesta:{ "deleted": "YYYYMMDD_HHMMSS" }.
Notas sobre DELETE:
- No requiere CSRF (solo roles) para alinearse con otros endpoints de lectura; si se desea endurecer, agregar
Depends(require_csrf). - Detección de "latest" contempla dos modos: (1) si
ultimo_catalogo.pdfes symlink compara el destino; (2) si es copia compara bytes. - Retención (
CATALOG_RETENTION) actúa solo en generación, no en delete manual. - Intentar borrar dos veces devuelve
404en la segunda (el archivo ya no existe).
Generación:
- Agrupa productos por categoría raíz (si no tiene, usa "Sin categoría").
- Sección 1: listado por categoría mostrando título y precio de venta (si existe). No incluye precio de compra ni stock.
- Sección 2: fichas 2×2 (4 por página) por categoría, sin mezclar categorías en una página. Cada ficha: imagen principal (si existe), título, precio de venta, descripción "blanda" (HTML sanitizado y truncado a ~1000 chars, luego 600 chars dentro de la ficha) sin tags.
- El precio de venta se toma de
product.sale_price(cuando esté disponible) o, si está ausente, de la variante conpromo_priceopricemínima (fallback). Nunca se incluyen precios de compra. - Estilo dark con acentos verde (#22C55E) y fucsia (#f0f).
- HTML → PDF vía WeasyPrint; fallback degradado ReportLab si falla la librería principal.
Dependencias:
weasyprint(opcional, agregado apyproject.toml; en Windows requiere dependencias GTK externas).reportlabcomo fallback.
Frontend:
- En
Stockse agregó selección múltiple (checkbox por fila) y botones: Generar catálogo, Ver catálogo, Descargar catálogo y Limpiar selección. - Generar exige al menos un producto seleccionado (alert si no).
- Ver/Descargar validan existencia con
HEADprimero; si 404 muestra alerta.
Ruta de guardado: archivos en ./catalogos/catalog_YYYYMMDD_HHMMSS.pdf + alias ultimo_catalogo.pdf.
Retención: configurar CATALOG_RETENTION=N (variable de entorno). Si N>0, se conservan solo los N catálogos más nuevos (no afecta ultimo_catalogo.pdf). 0 = ilimitado.
Logs:
- Sistema de logging ampliado (observabilidad fina): por cada generación se producen (a) un log detallado JSONL con pasos y (b) un resumen JSON.
- Pasos registrados (orden típico):
start(count, user)products_loaded(products)images_loaded(images)groups_built(groups)html_built(size)pdf_rendered(bytes)pdf_written(file, bytes)latest_updated(mode=symlink|copy) ólatest_update_failedretention_applied
summary_written(Si ocurre un error en symlink/copy se agregalatest_update_failed).
- Ruta de logs:
- Resumen:
logs/catalogs/summary_YYYYMMDD_HHMMSS.json - Detallado:
logs/catalogs/detail/catalog_YYYYMMDD_HHMMSS.log(cada línea JSON independiente)
- Resumen:
- Contenido del resumen:
{ generated_at, file, size, count, duration_ms }. - Retención de PDFs: controlada por
CATALOG_RETENTION(N más recientes; 0 = ilimitado). - Retención de logs detallados: se conservan los últimos 40 (
MAX_DETAIL_LOGS=40en código). Los resúmenes actualmente no se purgan automáticamente. - Ya NO se borran todos los
.logal final: solo se aplica política de recorte a detallados antiguos; esto asegura trazabilidad forense reciente sin crecimiento descontrolado. - Logging estructurado adicional en el logger Python (
[catalog] start / ok).
Diagnóstico (endpoints nuevos, roles admin|colaborador):
GET /catalogs/diagnostics/status→{ active_generation: {running, started_at, ids}, detail_logs, summaries }.GET /catalogs/diagnostics/summaries?limit=20→ últimos resúmenes parseados.GET /catalogs/diagnostics/log/{id}→ devuelve el log detallado (listaitems+count).idformatoYYYYMMDD_HHMMSS.
Concurrencia:
- Si ya hay una generación activa,
POST /catalogs/generateresponde409{ "detail": "Ya hay una generación en curso" }para evitar solapamientos (protección simple en memoria).
Errores típicos de generación y diagnóstico:
404 Productos no encontradossi todos los IDs suministrados no existen.500 No se pudo generar el PDFante fallo de render (WeasyPrint + fallback ReportLab agotados).500 No se pudo escribir log detallado de catálogosolo afecta observabilidad; el PDF igual puede generarse.
Uso de los logs detallados:
- Permiten medir tiempos inter-etapas (diferencia entre timestamps consecutivos) para optimización futura (ej. render HTML vs render PDF).
- Facilitan reintentos manuales si se observa cuellos en
images_loadedopdf_rendered(dependencias de librerías y fuentes).
Extensiones futuras sugeridas (no implementadas aún):
- Parametrizar
MAX_DETAIL_LOGSpor variable de entorno (CATALOG_DETAIL_LOG_RETENTION). - Endpoint para métricas agregadas (p95/p99
duration_ms). - Flag
dry_runpara validar estructura sin escribir archivos.
Notas futuras:
- Resumen de logs previo a la limpieza para auditoría opcional. Frontend: incluye modal de Histórico que lista catálogos con marca 'latest', links Ver / Descargar y tamaños.
- Generar 2 catálogos (seleccionar conjuntos distintos de productos) con ~5 s de diferencia para asegurar timestamps distintos.
GET /catalogsdebe listar ambos ordenados (más nuevo primero) y exactamente uno conlatest=true.- Borrar el MÁS ANTIGUO (
DELETE /catalogs/{id_antiguo}):
- Respuesta
200 {"deleted": id}. GET /catalogssolo muestra el restante y siguelatest=true.HEAD /catalogs/{id_antiguo}devuelve404.
- Generar un tercer catálogo. Confirmar que ahora el nuevo tiene
latest=true. - Borrar el que figura como
latestactualmente:
DELETEdebe reasignarultimo_catalogo.pdfal inmediatamente anterior.HEAD /catalogs/latestsigue devolviendo200.- En Windows sin permisos de symlink puede usarse copia; validar que el contenido (tamaño) coincide con el archivo esperado.
- Borrar el último catálogo restante y verificar:
HEAD /catalogs/latest=>404.GET /catalogs=> lista vacía.
- Generar 3 catálogos con
CATALOG_RETENTION=2(ajustar variable y reiniciar backend): tras el tercero, el primero (más viejo) debe haber desaparecido automáticamente;DELETEsobre uno de los dos restantes debe seguir funcionando y actualizar alias según corresponda. - CSV export: con varios catálogos presentes llamar
GET /catalogs/export.csv?from_dt=YYYY-MM-DD&to_dt=YYYY-MM-DD; abrir el CSV y corroborar que las filas coinciden conGET /catalogsfiltrado. - Filtros de fecha: usar
from_dtdel día actual yto_dtanterior (debe devolver vacío); invertir para ver resultados. - Concurrencia: lanzar dos borrados casi simultáneos (ej. ejecutar DELETE dos veces); segunda debe devolver
404. - Frontend: abrir modal Histórico, usar botón "Borrar" y confirmar toasts de éxito y refresco de la lista; borrar el
latesty validar que la marca "latest" migra al siguiente. - Error handling: intentar
DELETE /catalogs/valor_malformado(longitud distinta a 15) =>400 ID inválido.
Checklist rápido post-borrado de latest:
ultimo_catalogo.pdfapunta (symlink) o contiene (copia) el nuevo más reciente.- No quedan referencias a un archivo inexistente.
- La paginación sigue consistente (
page_size,total,pages).
El sistema incluye un pipeline robusto para importar remitos en formato PDF del proveedor Santa Planta, creando una compra en estado BORRADOR.
- Endpoint:
POST /purchases/import/santaplanta?supplier_id=ID&force_ocr=0|1 - Pipeline de parsing: El sistema intenta extraer datos secuencialmente con
pdfplumber(para PDFs con texto) ycamelot(para PDFs basados en tablas). Si no se obtiene un mínimo de texto (IMPORT_PDF_TEXT_MIN_CHARS), se invoca automáticamente aocrmypdfpara aplicar OCR. La opciónforce_ocr=1fuerza la ejecución de OCR desde el inicio. - Política de borrador vacío: Si tras todos los intentos no se detectan líneas, el comportamiento depende de la variable
IMPORT_ALLOW_EMPTY_DRAFT:true(default): Se crea una compra vacía en estadoBORRADORy se devuelve un status200 OK. La UI mostrará una advertencia.false: Se devuelve un error422 Unprocessable Entitycon un mensaje explicativo.
- Respuesta: La respuesta de la API incluye
purchase_id, uncorrelation_idpara seguimiento, y los totales parseados (parsed.totals). - Logs de importación: Cada paso del proceso de importación (ej. "iniciando ocr", "parseando con camelot") se registra en
ImportLog. El resultado final (éxito o fracaso) se guarda enAuditLog. - UI: Desde la interfaz de compras, un botón "Ver logs" permite abrir un panel con el timeline de eventos, copiar el
correlation_idy descargar el log completo en formato JSON (GET /purchases/{id}/logs?format=json).
- Importación de PDF
- Crawler de imágenes
- Seguridad
- Gestión de proveedores
- Flujo de Compras y Reenvío de Stock
Consulta AGENTS.md para la estructura de prompts, el uso del encabezado NG-HEADER y el checklist de PRs.