Initial commit: PymesBot Demo with IA integration
- FastAPI backend with WebSocket chat - SQLite database for products - Z.AI (GLM-4.7) integration for AI responses - Docker deployment ready - Caddy proxy configuration
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
|
data/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
1262
01_PYMESBOT_PROJECT_SPEC.md
Normal file
1262
01_PYMESBOT_PROJECT_SPEC.md
Normal file
File diff suppressed because it is too large
Load Diff
1476
02_PYMESBOT_INSTALLER_SPEC.md
Normal file
1476
02_PYMESBOT_INSTALLER_SPEC.md
Normal file
File diff suppressed because it is too large
Load Diff
176
README.md
Normal file
176
README.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# PymesBot Demo - Asistente de Ventas con IA
|
||||||
|
|
||||||
|
Demo funcional de asistente de ventas para librería con integración de IA.
|
||||||
|
|
||||||
|
## Características
|
||||||
|
|
||||||
|
- Chat en tiempo real con WebSocket
|
||||||
|
- Búsqueda de productos en base de datos SQLite
|
||||||
|
- Confirmación de ventas
|
||||||
|
- IA integrada con Z.AI (GLM-4.7)
|
||||||
|
- Interfaz web responsive
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Backend**: FastAPI + Python 3.11
|
||||||
|
- **Base de datos**: SQLite
|
||||||
|
- **Frontend**: HTML/JS vanilla
|
||||||
|
- **IA**: Z.AI API (GLM-4.7)
|
||||||
|
- **Proxy**: Caddy
|
||||||
|
|
||||||
|
## Estructura
|
||||||
|
|
||||||
|
```
|
||||||
|
pymesbot/
|
||||||
|
├── backend/
|
||||||
|
│ ├── main.py # FastAPI app
|
||||||
|
│ ├── Dockerfile # Imagen Docker
|
||||||
|
│ ├── requirements.txt # Dependencias
|
||||||
|
│ └── templates/
|
||||||
|
│ └── chat.html # Interfaz
|
||||||
|
└── data/ # Base de datos SQLite
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Prerrequisitos
|
||||||
|
|
||||||
|
- Docker y Docker Compose
|
||||||
|
- Acceso a API de Z.AI (u otro provider)
|
||||||
|
- Servidor con Caddy (u otro proxy)
|
||||||
|
|
||||||
|
### Variables de Entorno
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# API de Z.AI (requerido)
|
||||||
|
ZAI_API_KEY=tu_api_key_aqui
|
||||||
|
ANTHROPIC_MODEL=glm-4.7
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 1: Clonar el repositorio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.cbcren.online/renato97/demo.git
|
||||||
|
cd demo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 2: Configurar variables de entorno
|
||||||
|
|
||||||
|
Crear archivo `.env`:
|
||||||
|
```bash
|
||||||
|
ZAI_API_KEY=tu_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 3: Iniciar contenedores
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paso 4: Configurar Caddy (proxy)
|
||||||
|
|
||||||
|
Agregar al Caddyfile:
|
||||||
|
```caddy
|
||||||
|
demo.tudominio.com {
|
||||||
|
reverse_proxy localhost:8201
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reiniciar Caddy:
|
||||||
|
```bash
|
||||||
|
docker exec caddy-ingress caddy reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/health` | Health check |
|
||||||
|
| GET | `/stock/search?q=producto` | Buscar productos |
|
||||||
|
| POST | `/venta/confirmar` | Confirmar venta |
|
||||||
|
| GET | `/stats/ventas?periodo=hoy` | Estadísticas |
|
||||||
|
| WS | `/chat/ws/{session_id}` | Chat en tiempo real |
|
||||||
|
|
||||||
|
### Ejemplo de búsqueda de productos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8201/stock/search?q=birome"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejemplo de confirmar venta
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8201/venta/confirmar" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"producto_id": 1,
|
||||||
|
"cantidad": 2,
|
||||||
|
"precio_vendido": 850,
|
||||||
|
"vendedor": "test"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credenciales
|
||||||
|
|
||||||
|
- **PIN vendedor**: 1234
|
||||||
|
- **Admin**: /admin (password: admin123)
|
||||||
|
|
||||||
|
## Configuración de IA
|
||||||
|
|
||||||
|
El sistema usa la API de Z.AI directamente. Para cambiar el modelo o API:
|
||||||
|
|
||||||
|
Editar `main.py` línea ~40:
|
||||||
|
```python
|
||||||
|
ZAI_API_KEY = "tu_api_key"
|
||||||
|
ZAI_API_URL = "https://api.z.ai/api/anthropic/v1"
|
||||||
|
MODEL = "glm-4.7"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Issues / Bugs
|
||||||
|
|
||||||
|
### 1. Búsqueda limitada
|
||||||
|
- **Problema**: La búsqueda en DB solo soporta términos simples
|
||||||
|
- **Solución**: Usar la IA para búsquedas más complejas
|
||||||
|
|
||||||
|
### 2. Plurales no detectados
|
||||||
|
- **Problema**: "lápices" no encuentra "lápiz"
|
||||||
|
- **Workaround**: El sistema ahora tiene mapeo de plurales básico
|
||||||
|
|
||||||
|
### 3. Timeout en WebSocket
|
||||||
|
- **Problema**: La conexión puede cerrarse por inactividad
|
||||||
|
- **Solución**: El frontend reconecta automáticamente
|
||||||
|
|
||||||
|
### 4. Scope operator.write en OpenClaw
|
||||||
|
- **Problema**: OpenClaw Gateway requiere scopes específicos para usar el agente
|
||||||
|
- **Solución actual**: Se usa la API de Z.AI directamente en lugar de OpenClaw Gateway
|
||||||
|
- **Nota**: Para usar OpenClaw Gateway, configurar con `scopes: ["operator.read", "operator.write", "agent"]`
|
||||||
|
|
||||||
|
### 5. PicoClaw no conecta
|
||||||
|
- **Problema**: PicoClaw no reconoce modelos MiniMax
|
||||||
|
- **Solución**: Usar OpenClaw o API directa
|
||||||
|
|
||||||
|
## Mantenimiento
|
||||||
|
|
||||||
|
### Ver logs
|
||||||
|
```bash
|
||||||
|
docker logs pygmesbot_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reiniciar servicio
|
||||||
|
```bash
|
||||||
|
docker restart pygmesbot_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver productos en DB
|
||||||
|
```bash
|
||||||
|
docker exec pygmesbot_backend python3 -c "
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/app/data/stock.db')
|
||||||
|
for row in conn.execute('SELECT nombre, stock FROM productos LIMIT 5'):
|
||||||
|
print(row)
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
##Licencia
|
||||||
|
|
||||||
|
MIT
|
||||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./pymesbot/backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: pymesbot_backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8201:8000"
|
||||||
|
volumes:
|
||||||
|
- ./pymesbot/data:/app/data
|
||||||
|
environment:
|
||||||
|
- OPENCLAW_TOKEN=wlillidan1-demo-token-12345
|
||||||
|
- OPENCLAW_WS_URL=ws://openclaw_gateway:18789
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- caddy
|
||||||
|
- openclaw_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: pygmesbot_demo_net
|
||||||
|
external: false
|
||||||
|
caddy:
|
||||||
|
external: true
|
||||||
|
openclaw_net:
|
||||||
|
name: openclaw-config_pymesbot_net
|
||||||
|
external: true
|
||||||
52
inventario_ejemplo.csv
Normal file
52
inventario_ejemplo.csv
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
nombre,marca,categoria,precio,stock,variantes,codigo
|
||||||
|
Birome Bic Cristal,Bic,escritura,850.0,45,"{""color"": [""azul"", ""rojo"", ""negro"", ""verde""]}",7501031311309
|
||||||
|
Birome Bic Cristal Fine,Bic,escritura,920.0,32,"{""color"": [""azul"", ""negro""]}",7501031311408
|
||||||
|
Birome Faber Castell Trilux,Faber Castell,escritura,780.0,28,"{""color"": [""azul"", ""rojo"", ""negro""]}",7891360302456
|
||||||
|
Lápiz Grafito HB,Faber Castell,escritura,350.0,120,"{""talle"": [""standard"", ""jumbo""]}",7891360301121
|
||||||
|
Marcador Permanente Sharpie,Sharpie,escritura,1200.0,18,"{""color"": [""negro"", ""azul"", ""rojo""]}",071641005024
|
||||||
|
Corrector Líquido Paper Mate,Paper Mate,escritura,650.0,24,,071641023456
|
||||||
|
Goma de Borrar Staedtler,Staedtler,escritura,480.0,56,,4007817331515
|
||||||
|
Marcadores de Colores,Faber Castell,escritura,2800.0,15,"{""cantidad"": [""12 colores"", ""24 colores""]}",7891360306782
|
||||||
|
Lápices de Colores Largos,Maped,escritura,1850.0,22,"{""cantidad"": [""12 unidades"", ""24 unidades""]}",3154140256123
|
||||||
|
Resaltadores Pastel,Stabilo,escritura,1450.0,19,"{""cantidad"": [""6 unidades"", ""8 unidades""]}",4006381567890
|
||||||
|
Cuaderno Rivadavia A4 Rayado,Rivadavia,cuadernos,2500.0,80,"{""color"": [""azul"", ""rojo"", ""negro""]}",7798021234567
|
||||||
|
Cuaderno Rivadavia A4 Cuadriculado,Rivadavia,cuadernos,2500.0,75,"{""color"": [""azul"", ""verde""]}",7798021234574
|
||||||
|
Cuaderno ABC A4 48h,ABC,cuadernos,1800.0,95,"{""tipo"": [""rayado"", ""cuadriculado""]}",7796543210001
|
||||||
|
Cuaderno Tilibra 80h,Tilibra,cuadernos,3200.0,40,"{""tipo"": [""rayado"", ""cuadriculado"", ""blanco""]}",7891023345678
|
||||||
|
Libreta Anotador Chico,Sin Marca,cuadernos,450.0,200,,
|
||||||
|
Block de Dibujo A4,Filgo,cuadernos,2200.0,30,"{""hojas"": [""20 hojas"", ""40 hojas""]}",7796578912345
|
||||||
|
Papel A4 Resma (500h),Chamex,cuadernos,8500.0,12,,7896321456987
|
||||||
|
Papel A4 Resma (500h),Catalyst,cuadernos,7900.0,15,,7897412589632
|
||||||
|
Carpeta A4 2 Anillos,Genérica,cuadernos,1200.0,45,"{""color"": [""azul"", ""rojo"", ""verde"", ""negro""]}",
|
||||||
|
Separadores A4,Genéricos,cuadernos,650.0,38,,
|
||||||
|
Regla 30cm Plástica,Maped,geometria,650.0,50,"{""color"": [""transparente"", ""azul"", ""rosa""]}",3154140151234
|
||||||
|
Regla 20cm Metálica,Maped,geometria,1200.0,25,,3154140151241
|
||||||
|
Escuadra 45° + Cartabón,Maped,geometria,1850.0,30,,3154140256789
|
||||||
|
Compás Metálico con Tiralíneas,Maped,geometria,3500.0,15,,3154140351234
|
||||||
|
Compás Escolar Plástico,Genérico,geometria,850.0,40,,
|
||||||
|
Transportador 180°,Maped,geometria,480.0,35,,3154140451234
|
||||||
|
Juego de Geometría Escolar,Maped,geometria,2800.0,20,,3154140551234
|
||||||
|
Lapicera Tiralíneas,Rotring,geometria,4200.0,8,"{""tamaño"": [""0.3mm"", ""0.5mm"", ""0.7mm""]}",3501179256123
|
||||||
|
Tijera Escolar Punta Roma,Maped,geometria,950.0,42,,3154140651234
|
||||||
|
Tijera Oficina 21cm,Maped,geometria,1850.0,18,,3154140751234
|
||||||
|
Plastilina 12 Colores,Faber Castell,colores,2800.0,25,,7891360309875
|
||||||
|
Plastilina 24 Colores,Faber Castell,colores,4500.0,12,,7891360309882
|
||||||
|
Temperas 6 Colores,Alba,colores,1800.0,35,"{""tamaño"": [""12ml"", ""25ml""]}",7798029876543
|
||||||
|
Temperas 12 Colores,Alba,colores,3200.0,20,"{""tamaño"": [""12ml"", ""25ml""]}",7798029876550
|
||||||
|
Pinceles Set x3,Alba,colores,950.0,40,"{""tamaño"": [""n°4, n°6, n°8"", ""n°6, n°8, n°10""]}",7798029876567
|
||||||
|
Pinceles Set x5,Alba,colores,1450.0,28,,7798029876574
|
||||||
|
Papel Glacé 10h,Filgo,colores,650.0,60,,7796578912346
|
||||||
|
Papel Crepe,Filgo,colores,450.0,80,"{""color"": [""rojo"", ""azul"", ""amarillo"", ""verde"", ""blanco"", ""negro"", ""rosa""]}",7796578912347
|
||||||
|
Cartulina 50x70,Filgo,colores,180.0,150,"{""color"": [""blanco"", ""negro"", ""rojo"", ""azul"", ""verde"", ""amarillo""]}",7796578912348
|
||||||
|
Fibras Maped 12 unidades,Maped,colores,1950.0,32,,3154140851234
|
||||||
|
Fibras Maped 24 unidades,Maped,colores,3200.0,18,,3154140951234
|
||||||
|
Grapadora Chica,Staples,accesorios,1200.0,22,,0071641098765
|
||||||
|
Grapas Caja x1000,Genéricas,accesorios,350.0,85,,
|
||||||
|
Sacagrapas,Genérico,accesorios,180.0,50,,
|
||||||
|
Engrapadora Oficina,Rapid,accesorios,2500.0,12,,7313460156123
|
||||||
|
Cinta Adhesiva 18mm,Scotch,accesorios,450.0,70,"{""tipo"": [""transparente"", ""papel marrón""]}",0511319876543
|
||||||
|
Cinta Adhesiva Doble Faz,Scotch,accesorios,850.0,35,,0511319876544
|
||||||
|
Pegamento Barra,Pritt,accesorios,680.0,55,"{""tamaño"": [""11g"", ""22g""]}",0791469876543
|
||||||
|
Pegamento Líquido Escolar,Pritt,accesorios,520.0,48,,0791469876544
|
||||||
|
Silicona Líquida 30ml,Genérica,accesorios,380.0,60,,
|
||||||
|
Lápices de Cera Gruesos,Filgo,colores,1600.0,28,"{""cantidad"": [""12 colores"", ""24 colores""]}",7796578912355
|
||||||
|
508
inventario_ejemplo.html
Normal file
508
inventario_ejemplo.html
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Inventario Librería El Rincón del Saber</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 40px; }
|
||||||
|
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
|
||||||
|
h2 { color: #34495e; margin-top: 30px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||||
|
th { background-color: #3498db; color: white; padding: 12px; text-align: left; }
|
||||||
|
td { padding: 10px; border-bottom: 1px solid #ddd; }
|
||||||
|
tr:nth-child(even) { background-color: #f2f2f2; }
|
||||||
|
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ccc;
|
||||||
|
font-size: 12px; color: #666; text-align: center; }
|
||||||
|
.category-header { background-color: #ecf0f1; font-weight: bold; }
|
||||||
|
.price { text-align: right; }
|
||||||
|
.stock { text-align: center; }
|
||||||
|
.total { font-weight: bold; background-color: #e8f6f3; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📚 Librería El Rincón del Saber</h1>
|
||||||
|
<h2>Inventario de Productos - Febrero 2026</h2>
|
||||||
|
<p><strong>Total de productos:</strong> 51 | <strong>Valor total aproximado:</strong> $225,080</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Producto</th>
|
||||||
|
<th>Marca</th>
|
||||||
|
<th>Categoría</th>
|
||||||
|
<th class="price">Precio</th>
|
||||||
|
<th class="stock">Stock</th>
|
||||||
|
<th>Código</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="category-header"><td colspan="6">📁 ESCRITURA (10 productos)</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>Birome Bic Cristal</td>
|
||||||
|
<td>Bic</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$850.00</td>
|
||||||
|
<td class="stock">45</td>
|
||||||
|
<td>7501031311309</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Birome Bic Cristal Fine</td>
|
||||||
|
<td>Bic</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$920.00</td>
|
||||||
|
<td class="stock">32</td>
|
||||||
|
<td>7501031311408</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Birome Faber Castell Trilux</td>
|
||||||
|
<td>Faber Castell</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$780.00</td>
|
||||||
|
<td class="stock">28</td>
|
||||||
|
<td>7891360302456</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Lápiz Grafito HB</td>
|
||||||
|
<td>Faber Castell</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$350.00</td>
|
||||||
|
<td class="stock">120</td>
|
||||||
|
<td>7891360301121</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Marcador Permanente Sharpie</td>
|
||||||
|
<td>Sharpie</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$1,200.00</td>
|
||||||
|
<td class="stock">18</td>
|
||||||
|
<td>071641005024</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Corrector Líquido Paper Mate</td>
|
||||||
|
<td>Paper Mate</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$650.00</td>
|
||||||
|
<td class="stock">24</td>
|
||||||
|
<td>071641023456</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Goma de Borrar Staedtler</td>
|
||||||
|
<td>Staedtler</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$480.00</td>
|
||||||
|
<td class="stock">56</td>
|
||||||
|
<td>4007817331515</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Marcadores de Colores</td>
|
||||||
|
<td>Faber Castell</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$2,800.00</td>
|
||||||
|
<td class="stock">15</td>
|
||||||
|
<td>7891360306782</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Lápices de Colores Largos</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$1,850.00</td>
|
||||||
|
<td class="stock">22</td>
|
||||||
|
<td>3154140256123</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Resaltadores Pastel</td>
|
||||||
|
<td>Stabilo</td>
|
||||||
|
<td>Escritura</td>
|
||||||
|
<td class="price">$1,450.00</td>
|
||||||
|
<td class="stock">19</td>
|
||||||
|
<td>4006381567890</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="category-header"><td colspan="6">📁 CUADERNOS (10 productos)</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>Cuaderno Rivadavia A4 Rayado</td>
|
||||||
|
<td>Rivadavia</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$2,500.00</td>
|
||||||
|
<td class="stock">80</td>
|
||||||
|
<td>7798021234567</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Cuaderno Rivadavia A4 Cuadriculado</td>
|
||||||
|
<td>Rivadavia</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$2,500.00</td>
|
||||||
|
<td class="stock">75</td>
|
||||||
|
<td>7798021234574</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Cuaderno ABC A4 48h</td>
|
||||||
|
<td>ABC</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$1,800.00</td>
|
||||||
|
<td class="stock">95</td>
|
||||||
|
<td>7796543210001</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Cuaderno Tilibra 80h</td>
|
||||||
|
<td>Tilibra</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$3,200.00</td>
|
||||||
|
<td class="stock">40</td>
|
||||||
|
<td>7891023345678</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Libreta Anotador Chico</td>
|
||||||
|
<td>Sin Marca</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$450.00</td>
|
||||||
|
<td class="stock">200</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Block de Dibujo A4</td>
|
||||||
|
<td>Filgo</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$2,200.00</td>
|
||||||
|
<td class="stock">30</td>
|
||||||
|
<td>7796578912345</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Papel A4 Resma 500h (Chamex)</td>
|
||||||
|
<td>Chamex</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$8,500.00</td>
|
||||||
|
<td class="stock">12</td>
|
||||||
|
<td>7896321456987</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Papel A4 Resma 500h (Catalyst)</td>
|
||||||
|
<td>Catalyst</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$7,900.00</td>
|
||||||
|
<td class="stock">15</td>
|
||||||
|
<td>7897412589632</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Carpeta A4 2 Anillos</td>
|
||||||
|
<td>Genérica</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$1,200.00</td>
|
||||||
|
<td class="stock">45</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Separadores A4</td>
|
||||||
|
<td>Genéricos</td>
|
||||||
|
<td>Cuadernos</td>
|
||||||
|
<td class="price">$650.00</td>
|
||||||
|
<td class="stock">38</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="category-header"><td colspan="6">📁 GEOMETRÍA (10 productos)</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>Regla 30cm Plástica</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$650.00</td>
|
||||||
|
<td class="stock">50</td>
|
||||||
|
<td>3154140151234</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Regla 20cm Metálica</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$1,200.00</td>
|
||||||
|
<td class="stock">25</td>
|
||||||
|
<td>3154140151241</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Escuadra 45° + Cartabón</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$1,850.00</td>
|
||||||
|
<td class="stock">30</td>
|
||||||
|
<td>3154140256789</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Compás Metálico c/ Tiralíneas</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$3,500.00</td>
|
||||||
|
<td class="stock">15</td>
|
||||||
|
<td>3154140351234</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Compás Escolar Plástico</td>
|
||||||
|
<td>Genérico</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$850.00</td>
|
||||||
|
<td class="stock">40</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Transportador 180°</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$480.00</td>
|
||||||
|
<td class="stock">35</td>
|
||||||
|
<td>3154140451234</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Juego de Geometría Escolar</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$2,800.00</td>
|
||||||
|
<td class="stock">20</td>
|
||||||
|
<td>3154140551234</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Lapicera Tiralíneas</td>
|
||||||
|
<td>Rotring</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$4,200.00</td>
|
||||||
|
<td class="stock">8</td>
|
||||||
|
<td>3501179256123</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Tijera Escolar Punta Roma</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$950.00</td>
|
||||||
|
<td class="stock">42</td>
|
||||||
|
<td>3154140651234</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Tijera Oficina 21cm</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Geometría</td>
|
||||||
|
<td class="price">$1,850.00</td>
|
||||||
|
<td class="stock">18</td>
|
||||||
|
<td>3154140751234</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="category-header"><td colspan="6">📁 ARTE Y COLORES (11 productos)</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>Plastilina 12 Colores</td>
|
||||||
|
<td>Faber Castell</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$2,800.00</td>
|
||||||
|
<td class="stock">25</td>
|
||||||
|
<td>7891360309875</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Plastilina 24 Colores</td>
|
||||||
|
<td>Faber Castell</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$4,500.00</td>
|
||||||
|
<td class="stock">12</td>
|
||||||
|
<td>7891360309882</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Temperas 6 Colores</td>
|
||||||
|
<td>Alba</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$1,800.00</td>
|
||||||
|
<td class="stock">35</td>
|
||||||
|
<td>7798029876543</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Temperas 12 Colores</td>
|
||||||
|
<td>Alba</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$3,200.00</td>
|
||||||
|
<td class="stock">20</td>
|
||||||
|
<td>7798029876550</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Pinceles Set x3</td>
|
||||||
|
<td>Alba</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$950.00</td>
|
||||||
|
<td class="stock">40</td>
|
||||||
|
<td>7798029876567</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Pinceles Set x5</td>
|
||||||
|
<td>Alba</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$1,450.00</td>
|
||||||
|
<td class="stock">28</td>
|
||||||
|
<td>7798029876574</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Papel Glacé 10h</td>
|
||||||
|
<td>Filgo</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$650.00</td>
|
||||||
|
<td class="stock">60</td>
|
||||||
|
<td>7796578912346</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Papel Crepe</td>
|
||||||
|
<td>Filgo</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$450.00</td>
|
||||||
|
<td class="stock">80</td>
|
||||||
|
<td>7796578912347</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Cartulina 50x70</td>
|
||||||
|
<td>Filgo</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$180.00</td>
|
||||||
|
<td class="stock">150</td>
|
||||||
|
<td>7796578912348</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Fibras Maped 12 unidades</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$1,950.00</td>
|
||||||
|
<td class="stock">32</td>
|
||||||
|
<td>3154140851234</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Fibras Maped 24 unidades</td>
|
||||||
|
<td>Maped</td>
|
||||||
|
<td>Arte Y Colores</td>
|
||||||
|
<td class="price">$3,200.00</td>
|
||||||
|
<td class="stock">18</td>
|
||||||
|
<td>3154140951234</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="category-header"><td colspan="6">📁 ACCESORIOS (10 productos)</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>Grapadora Chica</td>
|
||||||
|
<td>Staples</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$1,200.00</td>
|
||||||
|
<td class="stock">22</td>
|
||||||
|
<td>0071641098765</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Grapas Caja x1000</td>
|
||||||
|
<td>Genéricas</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$350.00</td>
|
||||||
|
<td class="stock">85</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Sacagrapas</td>
|
||||||
|
<td>Genérico</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$180.00</td>
|
||||||
|
<td class="stock">50</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Engrapadora Oficina</td>
|
||||||
|
<td>Rapid</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$2,500.00</td>
|
||||||
|
<td class="stock">12</td>
|
||||||
|
<td>7313460156123</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Cinta Adhesiva 18mm</td>
|
||||||
|
<td>Scotch</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$450.00</td>
|
||||||
|
<td class="stock">70</td>
|
||||||
|
<td>0511319876543</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Cinta Adhesiva Doble Faz</td>
|
||||||
|
<td>Scotch</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$850.00</td>
|
||||||
|
<td class="stock">35</td>
|
||||||
|
<td>0511319876544</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Pegamento Barra</td>
|
||||||
|
<td>Pritt</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$680.00</td>
|
||||||
|
<td class="stock">55</td>
|
||||||
|
<td>0791469876543</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Pegamento Líquido Escolar</td>
|
||||||
|
<td>Pritt</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$520.00</td>
|
||||||
|
<td class="stock">48</td>
|
||||||
|
<td>0791469876544</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Silicona Líquida 30ml</td>
|
||||||
|
<td>Genérica</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$380.00</td>
|
||||||
|
<td class="stock">60</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Lápices de Cera Gruesos</td>
|
||||||
|
<td>Filgo</td>
|
||||||
|
<td>Accesorios</td>
|
||||||
|
<td class="price">$1,600.00</td>
|
||||||
|
<td class="stock">28</td>
|
||||||
|
<td>7796578912355</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p><strong>Documento generado para PymesBot</strong></p>
|
||||||
|
<p>Este inventario puede importarse al sistema mediante la función de importación de Excel/CSV</p>
|
||||||
|
<p>Fecha de generación: Febrero 2026 | Total: 51 productos</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
99
inventario_ejemplo.sql
Normal file
99
inventario_ejemplo.sql
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
-- Inventario de ejemplo para PymesBot
|
||||||
|
-- Este es un archivo SQL que puede importarse a la base de datos SQLite
|
||||||
|
-- Ejecutar: sqlite3 stock.db < inventario_ejemplo.sql
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════
|
||||||
|
-- DATOS DE EJEMPLO: Librería "El Rincón del Saber"
|
||||||
|
-- ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- Productos de escritura
|
||||||
|
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
|
||||||
|
('Birome Bic Cristal', 'Bic', 'escritura', 850.00, 45, '{"color": ["azul", "rojo", "negro", "verde"]}', '7501031311309'),
|
||||||
|
('Birome Bic Cristal Fine', 'Bic', 'escritura', 920.00, 32, '{"color": ["azul", "negro"]}', '7501031311408'),
|
||||||
|
('Birome Faber Castell Trilux', 'Faber Castell', 'escritura', 780.00, 28, '{"color": ["azul", "rojo", "negro"]}', '7891360302456'),
|
||||||
|
('Lápiz Grafito HB', 'Faber Castell', 'escritura', 350.00, 120, '{"talle": ["standard", "jumbo"]}', '7891360301121'),
|
||||||
|
('Marcador Permanente Sharpie', 'Sharpie', 'escritura', 1200.00, 18, '{"color": ["negro", "azul", "rojo"]}', '071641005024'),
|
||||||
|
('Corrector Líquido Paper Mate', 'Paper Mate', 'escritura', 650.00, 24, NULL, '071641023456'),
|
||||||
|
('Goma de Borrar Staedtler', 'Staedtler', 'escritura', 480.00, 56, NULL, '4007817331515'),
|
||||||
|
('Marcadores de Colores', 'Faber Castell', 'escritura', 2800.00, 15, '{"cantidad": ["12 colores", "24 colores"]}', '7891360306782'),
|
||||||
|
('Lápices de Colores Largos', 'Maped', 'escritura', 1850.00, 22, '{"cantidad": ["12 unidades", "24 unidades"]}', '3154140256123'),
|
||||||
|
('Resaltadores Pastel', 'Stabilo', 'escritura', 1450.00, 19, '{"cantidad": ["6 unidades", "8 unidades"]}', '4006381567890');
|
||||||
|
|
||||||
|
-- Cuadernos y papel
|
||||||
|
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
|
||||||
|
('Cuaderno Rivadavia A4 Rayado', 'Rivadavia', 'cuadernos', 2500.00, 80, '{"color": ["azul", "rojo", "negro"]}', '7798021234567'),
|
||||||
|
('Cuaderno Rivadavia A4 Cuadriculado', 'Rivadavia', 'cuadernos', 2500.00, 75, '{"color": ["azul", "verde"]}', '7798021234574'),
|
||||||
|
('Cuaderno ABC A4 48h', 'ABC', 'cuadernos', 1800.00, 95, '{"tipo": ["rayado", "cuadriculado"]}', '7796543210001'),
|
||||||
|
('Cuaderno Tilibra 80h', 'Tilibra', 'cuadernos', 3200.00, 40, '{"tipo": ["rayado", "cuadriculado", "blanco"]}', '7891023345678'),
|
||||||
|
('Libreta Anotador Chico', 'Sin Marca', 'cuadernos', 450.00, 200, NULL, NULL),
|
||||||
|
('Block de Dibujo A4', 'Filgo', 'cuadernos', 2200.00, 30, '{"hojas": ["20 hojas", "40 hojas"]}', '7796578912345'),
|
||||||
|
('Papel A4 Resma (500h)', 'Chamex', 'cuadernos', 8500.00, 12, NULL, '7896321456987'),
|
||||||
|
('Papel A4 Resma (500h)', 'Catalyst', 'cuadernos', 7900.00, 15, NULL, '7897412589632'),
|
||||||
|
('Carpeta A4 2 Anillos', 'Genérica', 'cuadernos', 1200.00, 45, '{"color": ["azul", "rojo", "verde", "negro"]}', NULL),
|
||||||
|
('Separadores A4', 'Genéricos', 'cuadernos', 650.00, 38, NULL, NULL);
|
||||||
|
|
||||||
|
-- Geometría
|
||||||
|
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
|
||||||
|
('Regla 30cm Plástica', 'Maped', 'geometria', 650.00, 50, '{"color": ["transparente", "azul", "rosa"]}', '3154140151234'),
|
||||||
|
('Regla 20cm Metálica', 'Maped', 'geometria', 1200.00, 25, NULL, '3154140151241'),
|
||||||
|
('Escuadra 45° + Cartabón', 'Maped', 'geometria', 1850.00, 30, NULL, '3154140256789'),
|
||||||
|
('Compás Metálico con Tiralíneas', 'Maped', 'geometria', 3500.00, 15, NULL, '3154140351234'),
|
||||||
|
('Compás Escolar Plástico', 'Genérico', 'geometria', 850.00, 40, NULL, NULL),
|
||||||
|
('Transportador 180°', 'Maped', 'geometria', 480.00, 35, NULL, '3154140451234'),
|
||||||
|
('Juego de Geometría Escolar', 'Maped', 'geometria', 2800.00, 20, NULL, '3154140551234'),
|
||||||
|
('Lapicera Tiralíneas', 'Rotring', 'geometria', 4200.00, 8, '{"tamaño": ["0.3mm", "0.5mm", "0.7mm"]}', '3501179256123'),
|
||||||
|
('Tijera Escolar Punta Roma', 'Maped', 'geometria', 950.00, 42, NULL, '3154140651234'),
|
||||||
|
('Tijera Oficina 21cm', 'Maped', 'geometria', 1850.00, 18, NULL, '3154140751234');
|
||||||
|
|
||||||
|
-- Artículos de arte y colores
|
||||||
|
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
|
||||||
|
('Plastilina 12 Colores', 'Faber Castell', 'colores', 2800.00, 25, NULL, '7891360309875'),
|
||||||
|
('Plastilina 24 Colores', 'Faber Castell', 'colores', 4500.00, 12, NULL, '7891360309882'),
|
||||||
|
('Temperas 6 Colores', 'Alba', 'colores', 1800.00, 35, '{"tamaño": ["12ml", "25ml"]}', '7798029876543'),
|
||||||
|
('Temperas 12 Colores', 'Alba', 'colores', 3200.00, 20, '{"tamaño": ["12ml", "25ml"]}', '7798029876550'),
|
||||||
|
('Pinceles Set x3', 'Alba', 'colores', 950.00, 40, '{"tamaño": ["n°4, n°6, n°8", "n°6, n°8, n°10"]}', '7798029876567'),
|
||||||
|
('Pinceles Set x5', 'Alba', 'colores', 1450.00, 28, NULL, '7798029876574'),
|
||||||
|
('Papel Glacé 10h', 'Filgo', 'colores', 650.00, 60, NULL, '7796578912346'),
|
||||||
|
('Papel Crepe', 'Filgo', 'colores', 450.00, 80, '{"color": ["rojo", "azul", "amarillo", "verde", "blanco", "negro", "rosa"]}', '7796578912347'),
|
||||||
|
('Cartulina 50x70', 'Filgo', 'colores', 180.00, 150, '{"color": ["blanco", "negro", "rojo", "azul", "verde", "amarillo"]}', '7796578912348'),
|
||||||
|
('Fibras Maped 12 unidades', 'Maped', 'colores', 1950.00, 32, NULL, '3154140851234'),
|
||||||
|
('Fibras Maped 24 unidades', 'Maped', 'colores', 3200.00, 18, NULL, '3154140951234');
|
||||||
|
|
||||||
|
-- Accesorios y otros
|
||||||
|
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo) VALUES
|
||||||
|
('Grapadora Chica', 'Staples', 'accesorios', 1200.00, 22, NULL, '0071641098765'),
|
||||||
|
('Grapas Caja x1000', 'Genéricas', 'accesorios', 350.00, 85, NULL, NULL),
|
||||||
|
('Sacagrapas', 'Genérico', 'accesorios', 180.00, 50, NULL, NULL),
|
||||||
|
('Engrapadora Oficina', 'Rapid', 'accesorios', 2500.00, 12, NULL, '7313460156123'),
|
||||||
|
('Cinta Adhesiva 18mm', 'Scotch', 'accesorios', 450.00, 70, '{"tipo": ["transparente", "papel marrón"]}', '0511319876543'),
|
||||||
|
('Cinta Adhesiva Doble Faz', 'Scotch', 'accesorios', 850.00, 35, NULL, '0511319876544'),
|
||||||
|
('Pegamento Barra', 'Pritt', 'accesorios', 680.00, 55, '{"tamaño": ["11g", "22g"]}', '0791469876543'),
|
||||||
|
('Pegamento Líquido Escolar', 'Pritt', 'accesorios', 520.00, 48, NULL, '0791469876544'),
|
||||||
|
('Silicona Líquida 30ml', 'Genérica', 'accesorios', 380.00, 60, NULL, NULL),
|
||||||
|
('Lápices de Cera Gruesos', 'Filgo', 'colores', 1600.00, 28, '{"cantidad": ["12 colores", "24 colores"]}', '7796578912355');
|
||||||
|
|
||||||
|
-- Configuración inicial del negocio
|
||||||
|
INSERT OR REPLACE INTO config (clave, valor) VALUES
|
||||||
|
('nombre_negocio', 'Librería El Rincón del Saber'),
|
||||||
|
('moneda', 'ARS'),
|
||||||
|
('moneda_simbolo', '$'),
|
||||||
|
('combo_categorias', '["escritura","cuadernos","geometria","colores"]'),
|
||||||
|
('alerta_stock_minimo', '5'),
|
||||||
|
('vendedor_pin', '1234'),
|
||||||
|
('rubro', 'libreria');
|
||||||
|
|
||||||
|
-- Ejemplo de promociones
|
||||||
|
INSERT INTO promociones (nombre, tipo, valor, categorias, activa, fecha_inicio, fecha_fin) VALUES
|
||||||
|
('Vuelta al Cole 15% off en Cuadernos', 'descuento_pct', 15, '["cuadernos"]', 1, '2026-02-01', '2026-03-31'),
|
||||||
|
('Día del Padre - 10% off en Escritura', 'descuento_pct', 10, '["escritura"]', 0, '2026-06-15', '2026-06-16');
|
||||||
|
|
||||||
|
-- Notas adicionales:
|
||||||
|
-- Este inventario incluye 51 productos distribuidos en 5 categorías:
|
||||||
|
-- - escritura: 10 productos
|
||||||
|
-- - cuadernos: 10 productos
|
||||||
|
-- - geometria: 10 productos
|
||||||
|
-- - colores: 11 productos
|
||||||
|
-- - accesorios: 10 productos
|
||||||
|
--
|
||||||
|
-- Para usar: importar este archivo a un cliente de prueba con:
|
||||||
|
-- sqlite3 /opt/pymesbot/{cliente}/data/stock.db < inventario_ejemplo.sql
|
||||||
23
inventario_ejemplo.xls
Normal file
23
inventario_ejemplo.xls
Normal file
File diff suppressed because one or more lines are too long
18
pymesbot/backend/Dockerfile
Normal file
18
pymesbot/backend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /app/data /app/templates
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
||||||
688
pymesbot/backend/main.py
Normal file
688
pymesbot/backend/main.py
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
FastAPI,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketDisconnect,
|
||||||
|
HTTPException,
|
||||||
|
UploadFile,
|
||||||
|
File,
|
||||||
|
Depends,
|
||||||
|
)
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
OPENCLAW_WS_URL = os.getenv("OPENCLAW_WS_URL", "ws://openclaw_gateway:18789")
|
||||||
|
OPENCLAW_TOKEN = os.getenv("OPENCLAW_TOKEN", "wlillidan1-demo-token-12345")
|
||||||
|
|
||||||
|
|
||||||
|
async def chat_with_ai(message: str, session_id: str = "pymesbot") -> Optional[str]:
|
||||||
|
"""Send message to Z.AI API directly"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
ZAI_API_KEY = "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS"
|
||||||
|
ZAI_API_URL = "https://api.z.ai/api/anthropic/v1/messages"
|
||||||
|
|
||||||
|
system_prompt = """Sos el asistente de ventas de Demo Librería en Argentina.
|
||||||
|
Productos: biromes, lápices, cuadernos, colores, reglas, etc.
|
||||||
|
Precios en pesos argentinos.
|
||||||
|
Respondé de forma útil, breve y siempre preguntá si se concretó la venta."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = httpx.post(
|
||||||
|
ZAI_API_URL,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {ZAI_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"max_tokens": 200,
|
||||||
|
"system": system_prompt,
|
||||||
|
"messages": [{"role": "user", "content": message}],
|
||||||
|
},
|
||||||
|
timeout=30.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
content = data.get("content", [])
|
||||||
|
if content and len(content) > 0:
|
||||||
|
return content[0].get("text", "")
|
||||||
|
else:
|
||||||
|
logger.error(f"[Z.AI] API error: {response.status_code} - {response.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Z.AI] Error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Alias para compatibilidad
|
||||||
|
chat_with_openclaw = chat_with_ai
|
||||||
|
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).parent / "data"
|
||||||
|
DB_PATH = DATA_DIR / "stock.db"
|
||||||
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if not DB_PATH.exists():
|
||||||
|
init_db()
|
||||||
|
return DB_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS productos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nombre TEXT NOT NULL,
|
||||||
|
marca TEXT,
|
||||||
|
categoria TEXT NOT NULL DEFAULT 'general',
|
||||||
|
precio REAL NOT NULL,
|
||||||
|
stock INTEGER NOT NULL DEFAULT 0,
|
||||||
|
variantes TEXT,
|
||||||
|
codigo TEXT,
|
||||||
|
activo INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS ventas (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
producto_id INTEGER NOT NULL,
|
||||||
|
cantidad INTEGER NOT NULL,
|
||||||
|
precio_vendido REAL NOT NULL,
|
||||||
|
vendedor TEXT,
|
||||||
|
notas TEXT,
|
||||||
|
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
FOREIGN KEY (producto_id) REFERENCES productos(id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
clave TEXT PRIMARY KEY,
|
||||||
|
valor TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
default_config = [
|
||||||
|
("nombre_negocio", "Demo Librería"),
|
||||||
|
("moneda", "ARS"),
|
||||||
|
("moneda_simbolo", "$"),
|
||||||
|
("alerta_stock_minimo", "5"),
|
||||||
|
("vendedor_pin", "1234"),
|
||||||
|
("admin_password", "admin123"),
|
||||||
|
("rubro", "libreria"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for clave, valor in default_config:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR IGNORE INTO config (clave, valor) VALUES (?, ?)", (clave, valor)
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_products = [
|
||||||
|
(
|
||||||
|
"Birome Bic Cristal Azul",
|
||||||
|
"Bic",
|
||||||
|
"escritura",
|
||||||
|
850,
|
||||||
|
50,
|
||||||
|
'{"color": ["azul"]}',
|
||||||
|
"7501031311309",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Birome Bic Cristal Rojo",
|
||||||
|
"Bic",
|
||||||
|
"escritura",
|
||||||
|
850,
|
||||||
|
30,
|
||||||
|
'{"color": ["rojo"]}',
|
||||||
|
"7501031311316",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Birome Bic Cristal Negro",
|
||||||
|
"Bic",
|
||||||
|
"escritura",
|
||||||
|
850,
|
||||||
|
45,
|
||||||
|
'{"color": ["negro"]}',
|
||||||
|
"7501031311323",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Cuaderno Rivadavia 48 Hojas",
|
||||||
|
"Rivadavia",
|
||||||
|
"cuadernos",
|
||||||
|
2500,
|
||||||
|
20,
|
||||||
|
'{"tipo": ["rayado", "blanco"]}',
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
("Lápiz Faber Castell 2B", "Faber Castell", "escritura", 450, 100, None, None),
|
||||||
|
("Goma de borrar Staedtler", "Staedtler", "escritura", 320, 25, None, None),
|
||||||
|
("Regla 30cm", "Maped", "geometria", 650, 15, None, None),
|
||||||
|
("Compás Prisma", "Prisma", "geometria", 2500, 8, None, None),
|
||||||
|
("Caja de colores 12", "Maped", "colores", 3200, 18, None, None),
|
||||||
|
("Papel glasé x 20", "Laprida", "colores", 850, 40, None, None),
|
||||||
|
]
|
||||||
|
|
||||||
|
for nombre, marca, categoria, precio, stock, variantes, codigo in sample_products:
|
||||||
|
v = variantes if variantes else None
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO productos (nombre, marca, categoria, precio, stock, variantes, codigo)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(nombre, marca, categoria, precio, stock, v, codigo),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
class BuscarStockRequest(BaseModel):
|
||||||
|
query: str
|
||||||
|
limit: int = 5
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmarVentaRequest(BaseModel):
|
||||||
|
producto_id: int
|
||||||
|
cantidad: int
|
||||||
|
precio_vendido: float
|
||||||
|
vendedor: str = "vendedor"
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
pin: str
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="PymesBot Backend")
|
||||||
|
|
||||||
|
if TEMPLATES_DIR.exists():
|
||||||
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||||
|
else:
|
||||||
|
templates = None
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup():
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT valor FROM config WHERE clave = 'nombre_negocio'")
|
||||||
|
nombre = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return {"status": "ok", "negocio": nombre[0] if nombre else "Demo"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/stock/search")
|
||||||
|
async def buscar_stock(q: str, limit: int = 5):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
q_lower = q.lower()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM productos
|
||||||
|
WHERE activo = 1 AND (
|
||||||
|
LOWER(nombre) LIKE ? OR
|
||||||
|
LOWER(marca) LIKE ? OR
|
||||||
|
LOWER(categoria) LIKE ?
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(nombre) LIKE ? THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END,
|
||||||
|
stock DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(f"%{q_lower}%", f"%{q_lower}%", f"%{q_lower}%", f"%{q_lower}%", limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
resultados = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
resultados.append(
|
||||||
|
{
|
||||||
|
"id": row["id"],
|
||||||
|
"nombre": row["nombre"],
|
||||||
|
"marca": row["marca"],
|
||||||
|
"categoria": row["categoria"],
|
||||||
|
"precio": row["precio"],
|
||||||
|
"precio_formateado": f"${row['precio']:.0f}",
|
||||||
|
"stock": row["stock"],
|
||||||
|
"variantes": json.loads(row["variantes"]) if row["variantes"] else {},
|
||||||
|
"hay_stock": row["stock"] > 0,
|
||||||
|
"promo_activa": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"resultados": resultados, "total": len(resultados), "query": q}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/stock/producto/{producto_id}")
|
||||||
|
async def get_producto(producto_id: int):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM productos WHERE id = ?", (producto_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"Producto con id {producto_id} no encontrado"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": row["id"],
|
||||||
|
"nombre": row["nombre"],
|
||||||
|
"marca": row["marca"],
|
||||||
|
"categoria": row["categoria"],
|
||||||
|
"precio": row["precio"],
|
||||||
|
"stock": row["stock"],
|
||||||
|
"variantes": json.loads(row["variantes"]) if row["variantes"] else {},
|
||||||
|
"codigo": row["codigo"],
|
||||||
|
"activo": bool(row["activo"]),
|
||||||
|
"created_at": row["created_at"],
|
||||||
|
"updated_at": row["updated_at"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/venta/confirmar")
|
||||||
|
async def confirmar_venta(req: ConfirmarVentaRequest):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT * FROM productos WHERE id = ? AND activo = 1", (req.producto_id,)
|
||||||
|
)
|
||||||
|
producto = cursor.fetchone()
|
||||||
|
|
||||||
|
if not producto:
|
||||||
|
conn.close()
|
||||||
|
raise HTTPException(status_code=404, detail="Producto no encontrado")
|
||||||
|
|
||||||
|
if producto[4] < req.cantidad:
|
||||||
|
conn.close()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Stock insuficiente. Stock actual: {producto[4]}, solicitado: {req.cantidad}",
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO ventas (producto_id, cantidad, precio_vendido, vendedor)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(req.producto_id, req.cantidad, req.precio_vendido, req.vendedor),
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE productos SET stock = stock - ?, updated_at = datetime('now')
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(req.cantidad, req.producto_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.execute("SELECT stock FROM productos WHERE id = ?", (req.producto_id,))
|
||||||
|
nuevo_stock = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
venta_id = cursor.lastrowid
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"venta_id": venta_id,
|
||||||
|
"producto": producto[1],
|
||||||
|
"cantidad": req.cantidad,
|
||||||
|
"precio_vendido": req.precio_vendido,
|
||||||
|
"total": req.cantidad * req.precio_vendido,
|
||||||
|
"stock_anterior": producto[4],
|
||||||
|
"stock_nuevo": nuevo_stock,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/stats/ventas")
|
||||||
|
async def ventas_stats(periodo: str = "hoy"):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
if periodo == "hoy":
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
|
||||||
|
FROM ventas
|
||||||
|
WHERE date(timestamp) = date('now')
|
||||||
|
""")
|
||||||
|
elif periodo == "semana":
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
|
||||||
|
FROM ventas
|
||||||
|
WHERE timestamp >= datetime('now', '-7 days')
|
||||||
|
""")
|
||||||
|
else:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*), SUM(cantidad * precio_vendido), AVG(cantidad * precio_vendido)
|
||||||
|
FROM ventas
|
||||||
|
WHERE timestamp >= datetime('now', '-30 days')
|
||||||
|
""")
|
||||||
|
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"periodo": periodo,
|
||||||
|
"total_ventas": row[0] or 0,
|
||||||
|
"total_pesos": row[1] or 0,
|
||||||
|
"ticket_promedio": row[2] or 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
if templates:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"chat.html", {"request": {}, "nombre_negocio": "Demo Librería"}
|
||||||
|
)
|
||||||
|
return HTMLResponse("""
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>PymesBot - Demo</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||||
|
.chat-box { background: white; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); height: 70vh; display: flex; flex-direction: column; }
|
||||||
|
.chat-header { padding: 16px 20px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 12px; }
|
||||||
|
.chat-header h1 { font-size: 18px; color: #333; }
|
||||||
|
.chat-messages { flex: 1; overflow-y: auto; padding: 20px; }
|
||||||
|
.message { margin-bottom: 16px; max-width: 80%; }
|
||||||
|
.message.bot { margin-right: auto; }
|
||||||
|
.message.user { margin-left: auto; text-align: right; }
|
||||||
|
.message .bubble { display: inline-block; padding: 12px 16px; border-radius: 18px; }
|
||||||
|
.message.bot .bubble { background: #f0f0f0; color: #333; }
|
||||||
|
.message.user .bubble { background: #007bff; color: white; }
|
||||||
|
.typing { color: #888; font-style: italic; padding: 8px; }
|
||||||
|
.chat-input { padding: 16px 20px; border-top: 1px solid #eee; display: flex; gap: 12px; }
|
||||||
|
.chat-input input { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 24px; outline: none; font-size: 16px; }
|
||||||
|
.chat-input button { padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 24px; cursor: pointer; font-size: 16px; }
|
||||||
|
.chat-input button:hover { background: #0056b3; }
|
||||||
|
.sidebar { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||||
|
.sidebar h2 { font-size: 16px; margin-bottom: 12px; color: #333; }
|
||||||
|
.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||||
|
.stat-value { font-weight: 600; color: #007bff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
||||||
|
<div class="chat-box">
|
||||||
|
<div class="chat-header">
|
||||||
|
<h1>🛒 Demo Librería</h1>
|
||||||
|
</div>
|
||||||
|
<div class="chat-messages" id="messages">
|
||||||
|
<div class="message bot">
|
||||||
|
<div class="bubble">¡Hola! Soy el asistente de ventas de Demo Librería. ¿Qué estás buscando?</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-input">
|
||||||
|
<input type="text" id="msgInput" placeholder="Escribí tu consulta..." autocomplete="off">
|
||||||
|
<button onclick="sendMessage()">Enviar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<h2>📊 Hoy</h2>
|
||||||
|
<div class="stat">
|
||||||
|
<span>Ventas</span>
|
||||||
|
<span class="stat-value" id="ventas-hoy">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span>Total</span>
|
||||||
|
<span class="stat-value" id="total-hoy">$0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<h2>⚠️ Stock bajo</h2>
|
||||||
|
<div id="alertas-stock"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
const sessionId = Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
ws = new WebSocket(`wss://${window.location.host}/chat/ws/${sessionId}`);
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.msg) {
|
||||||
|
addMessage(data.msg, 'bot');
|
||||||
|
}
|
||||||
|
if (data.stats) {
|
||||||
|
document.getElementById('ventas-hoy').textContent = data.stats.total_ventas;
|
||||||
|
document.getElementById('total-hoy').textContent = '$' + data.stats.total_pesos.toLocaleString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(text, type) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `message ${type}`;
|
||||||
|
div.innerHTML = `<div class="bubble">${text}</div>`;
|
||||||
|
document.getElementById('messages').appendChild(div);
|
||||||
|
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
const input = document.getElementById('msgInput');
|
||||||
|
const text = input.value.trim();
|
||||||
|
if (!text || !ws) return;
|
||||||
|
addMessage(text, 'user');
|
||||||
|
ws.send(JSON.stringify({ msg: text, session_id: sessionId }));
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('msgInput').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') sendMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect();
|
||||||
|
|
||||||
|
fetch('/stats/ventas?periodo=hoy')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('ventas-hoy').textContent = data.total_ventas;
|
||||||
|
document.getElementById('total-hoy').textContent = '$' + data.total_pesos.toLocaleString();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
chat_sessions = {}
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/chat/ws/{session_id}")
|
||||||
|
async def websocket_chat(websocket: WebSocket, session_id: str):
|
||||||
|
await websocket.accept()
|
||||||
|
logger.info(f"[WS] Accepted connection for session: {session_id}")
|
||||||
|
|
||||||
|
if session_id not in chat_sessions:
|
||||||
|
chat_sessions[session_id] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_json()
|
||||||
|
logger.info(f"[WS] Received: {data}")
|
||||||
|
mensaje = data.get("msg", "")
|
||||||
|
session_id = data.get("session_id", session_id)
|
||||||
|
|
||||||
|
await websocket.send_json({"typing": True})
|
||||||
|
|
||||||
|
logger.info(f"[WS] Processing message: {mensaje}")
|
||||||
|
|
||||||
|
# Mejorar búsqueda: manejar plurales y buscar en nombre, marca y categoría
|
||||||
|
mensaje_lower = mensaje.lower()
|
||||||
|
|
||||||
|
# Mapeo de plurales a singular
|
||||||
|
plurales = {
|
||||||
|
"lapices": "lapiz",
|
||||||
|
"lápices": "lápiz",
|
||||||
|
"lápices": "lápiz",
|
||||||
|
"cuadernos": "cuaderno",
|
||||||
|
"biromes": "birome",
|
||||||
|
"gomas": "goma",
|
||||||
|
"marcadores": "marcador",
|
||||||
|
"colores": "colores",
|
||||||
|
"fibras": "fibras",
|
||||||
|
"reglas": "regla",
|
||||||
|
"tijeras": "tijera",
|
||||||
|
}
|
||||||
|
|
||||||
|
for plural, singular in plurales.items():
|
||||||
|
mensaje_lower = mensaje_lower.replace(plural, singular)
|
||||||
|
|
||||||
|
# Extraer palabras clave de búsqueda (más de 3 letras)
|
||||||
|
palabras = mensaje_lower.split()
|
||||||
|
stopwords = {
|
||||||
|
"hola",
|
||||||
|
"buenos",
|
||||||
|
"buenas",
|
||||||
|
"que",
|
||||||
|
"tiene",
|
||||||
|
"tenes",
|
||||||
|
"busco",
|
||||||
|
"quiero",
|
||||||
|
"necesito",
|
||||||
|
"para",
|
||||||
|
"una",
|
||||||
|
"clienta",
|
||||||
|
"vino",
|
||||||
|
"buscar",
|
||||||
|
"necesita",
|
||||||
|
"quanto",
|
||||||
|
"cuanto",
|
||||||
|
"cuántos",
|
||||||
|
"cuántas",
|
||||||
|
}
|
||||||
|
terminos = [p for p in palabras if len(p) > 3 and p not in stopwords]
|
||||||
|
|
||||||
|
if not terminos:
|
||||||
|
# Si no hay términos claros, usar el mensaje completo para buscar
|
||||||
|
terminos = [mensaje_lower]
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Buscar todos los términos juntos
|
||||||
|
all_results = []
|
||||||
|
for term in terminos:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM productos
|
||||||
|
WHERE activo = 1 AND (
|
||||||
|
LOWER(nombre) LIKE ? OR
|
||||||
|
LOWER(marca) LIKE ? OR
|
||||||
|
LOWER(categoria) LIKE ?
|
||||||
|
)
|
||||||
|
ORDER BY stock DESC
|
||||||
|
LIMIT 10
|
||||||
|
""",
|
||||||
|
(f"%{term}%", f"%{term}%", f"%{term}%"),
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
if row["id"] not in [r["id"] for r in all_results]:
|
||||||
|
all_results.append(dict(row))
|
||||||
|
|
||||||
|
rows = all_results[:5]
|
||||||
|
|
||||||
|
logger.info(f"[WS] Found {len(rows)} rows")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if rows:
|
||||||
|
resultados = []
|
||||||
|
for row in rows:
|
||||||
|
resultados.append(
|
||||||
|
f"• {row['nombre']} - ${row['precio']:.0f} (stock: {row['stock']})"
|
||||||
|
)
|
||||||
|
|
||||||
|
respuesta = f"Encontre estos productos:\n" + "\n".join(resultados)
|
||||||
|
respuesta += "\n\n¿Se concretó la venta? ¿Cuántas unidades?"
|
||||||
|
logger.info(f"[WS] Respuesta: {respuesta[:50]}")
|
||||||
|
else:
|
||||||
|
# Usar OpenClaw cuando no hay match en DB
|
||||||
|
logger.info(
|
||||||
|
f"[WS] No products found, trying OpenClaw for: {mensaje}"
|
||||||
|
)
|
||||||
|
respuesta = await chat_with_openclaw(mensaje, session_id)
|
||||||
|
if not respuesta:
|
||||||
|
respuesta = "No encontré productos con esa descripción. ¿Podés ser más específico?"
|
||||||
|
logger.info(
|
||||||
|
f"[WS] OpenClaw response: {respuesta[:50] if respuesta else 'None'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_sessions[session_id].append({"role": "user", "content": mensaje})
|
||||||
|
chat_sessions[session_id].append(
|
||||||
|
{"role": "assistant", "content": respuesta}
|
||||||
|
)
|
||||||
|
|
||||||
|
await websocket.send_json({"typing": False})
|
||||||
|
await websocket.send_json({"msg": respuesta})
|
||||||
|
logger.info("[WS] Response sent")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WS] Error sending response: {e}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
10
pymesbot/backend/requirements.txt
Normal file
10
pymesbot/backend/requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
fastapi==0.110.0
|
||||||
|
uvicorn[standard]==0.29.0
|
||||||
|
jinja2==3.1.3
|
||||||
|
python-multipart==0.0.9
|
||||||
|
aiosqlite==0.20.0
|
||||||
|
openpyxl==3.1.2
|
||||||
|
pandas==2.2.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
aiofiles==23.2.1
|
||||||
|
httpx==0.27.0
|
||||||
217
pymesbot/backend/templates/chat.html
Normal file
217
pymesbot/backend/templates/chat.html
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Demo Librería - PymesBot</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
gap: 20px;
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
}
|
||||||
|
.chat-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.chat-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.chat-header h1 { font-size: 18px; color: #333; }
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.message { margin-bottom: 16px; max-width: 80%; }
|
||||||
|
.message.bot { margin-right: auto; }
|
||||||
|
.message.user { margin-left: auto; text-align: right; }
|
||||||
|
.message .bubble {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.message.bot .bubble { background: #f0f0f0; color: #333; }
|
||||||
|
.message.user .bubble { background: #007bff; color: white; }
|
||||||
|
.typing { color: #888; font-style: italic; padding: 8px; }
|
||||||
|
.chat-input {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.chat-input input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 24px;
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.chat-input input:focus { border-color: #007bff; }
|
||||||
|
.chat-input button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.chat-input button:hover { background: #0056b3; }
|
||||||
|
.sidebar { display: flex; flex-direction: column; gap: 20px; }
|
||||||
|
.sidebar-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.sidebar-box h2 { font-size: 16px; margin-bottom: 12px; color: #333; }
|
||||||
|
.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||||
|
.stat-value { font-weight: 600; color: #007bff; }
|
||||||
|
.product-list { max-height: 200px; overflow-y: auto; }
|
||||||
|
.product-item { padding: 8px 0; border-bottom: 1px solid #eee; font-size: 14px; }
|
||||||
|
.product-item .name { font-weight: 500; }
|
||||||
|
.product-item .price { color: #007bff; }
|
||||||
|
.product-item .stock { color: #28a745; font-size: 12px; }
|
||||||
|
.product-item .no-stock { color: #dc3545; font-size: 12px; }
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app { grid-template-columns: 1fr; }
|
||||||
|
.sidebar { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="app">
|
||||||
|
<div class="chat-box">
|
||||||
|
<div class="chat-header">
|
||||||
|
<span style="font-size: 24px;">🛒</span>
|
||||||
|
<h1>Demo Librería</h1>
|
||||||
|
</div>
|
||||||
|
<div class="chat-messages" id="messages">
|
||||||
|
<div class="message bot">
|
||||||
|
<div class="bubble">¡Hola! Soy el asistente de ventas de Demo Librería. ¿Qué estás buscando?</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="typing-indicator" class="typing" style="display: none;">
|
||||||
|
Escribiendo...
|
||||||
|
</div>
|
||||||
|
<div class="chat-input">
|
||||||
|
<input type="text" id="msgInput" placeholder="Escribí tu consulta..." autocomplete="off">
|
||||||
|
<button onclick="sendMessage()">Enviar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h2>📊 Hoy</h2>
|
||||||
|
<div class="stat">
|
||||||
|
<span>Ventas</span>
|
||||||
|
<span class="stat-value" id="ventas-hoy">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span>Total</span>
|
||||||
|
<span class="stat-value" id="total-hoy">$0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h2>📦 Productos</h2>
|
||||||
|
<div class="product-list" id="product-list">
|
||||||
|
<div style="color: #888; font-size: 14px;">Escribí para buscar...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
const sessionId = Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
ws = new WebSocket(`${protocol}//${window.location.host}/chat/ws/${sessionId}`);
|
||||||
|
|
||||||
|
ws.onopen = () => console.log('Conectado');
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.typing !== undefined) {
|
||||||
|
document.getElementById('typing-indicator').style.display = data.typing ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.msg) {
|
||||||
|
addMessage(data.msg, 'bot');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (e) => console.error('WS Error:', e);
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('Desconectado, reconectando...');
|
||||||
|
setTimeout(connect, 3000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(text, type) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `message ${type}`;
|
||||||
|
div.innerHTML = `<div class="bubble">${escapeHtml(text)}</div>`;
|
||||||
|
document.getElementById('messages').appendChild(div);
|
||||||
|
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
const input = document.getElementById('msgInput');
|
||||||
|
const text = input.value.trim();
|
||||||
|
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
addMessage(text, 'user');
|
||||||
|
ws.send(JSON.stringify({ msg: text, session_id: sessionId }));
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('msgInput').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') sendMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect();
|
||||||
|
|
||||||
|
fetch('/stats/ventas?periodo=hoy')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('ventas-hoy').textContent = data.total_ventas || 0;
|
||||||
|
document.getElementById('total-hoy').textContent = '$' + (data.total_pesos || 0).toLocaleString();
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
pymesbot/picoclaw/config.json
Normal file
18
pymesbot/picoclaw/config.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "/root/.picoclaw/workspace",
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tool_iterations": 20,
|
||||||
|
"system": "Sos el asistente de ventas de Demo PymesBot, una librería en Argentina.\n\nREGLAS:\n1. Respondé en español argentino.\n2. Sé conciso (máximo 3 líneas).\n3. Siempre preguntá si se concretó la venta."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openai": {
|
||||||
|
"api_key": "6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS",
|
||||||
|
"api_base": "https://api.z.ai/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user