# 🧾 CONTEXTO — Sistema de Cotizaciones Chile

> Pega este documento al inicio de una conversación con Claude o cualquier IA antes de pedir modificaciones o correcciones al sistema.

---

## ¿Qué es este sistema?

Sistema de **gestión de cotizaciones comerciales** para empresas chilenas. Conecta con una base de datos MySQL/MariaDB existente (de un ERP propio), corre en Windows en un servidor local y permite a vendedores crear cotizaciones, ver historial unificado (sistema nuevo + legado), generar PDFs, administrar clientes, vendedores, productos e informes.

---

## Stack tecnológico

| Capa | Detalle |
|------|---------|
| **Backend** | Python 3 + FastAPI + PyMySQL + ReportLab + Cryptography (Fernet AES) + **requests** (subida HTTP/WebDAV/Script PHP) + **python-multipart** (subida de logo) |
| **Frontend** | Single-file HTML `cotizaciones.html` (~21.000 líneas), sin frameworks. **PWA** (manifest + service worker) |
| **Base de datos** | MySQL / MariaDB — múltiples conexiones (allen, gamspa MySQL 5.5.62, villacor, …) |
| **Servidor** | Windows, carpeta `C:\gestion2026\siscot\`, puerto 8000 |
| **Arranque** | `uvicorn app:app --host 0.0.0.0 --port 8000` |
| **IVA** | 19% (`IVA = 0.19` en `app.py`) |
| **Moneda** | CLP — formato `$1.234.567` |
| **RUT** | Formato `12.345.678-9` |
| **Nombre sistema** | **GESTION WEB** (branding fijo) |
| **Versión actual** | **v1.0.380** (10 junio 2026) |

> ⚠️ **Dependencia nueva obligatoria**: `requests` (para WebDAV/Script PHP). Si falta: `python -m pip install requests`. Ya está en `requirements.txt`.

---

## Archivos principales

| Archivo | Descripción |
|---------|-------------|
| `app.py` | Backend FastAPI (~3.400 líneas) |
| `cotizaciones.html` | Frontend single-file (~5.200 líneas) |
| `conexiones.json` | Conexiones BD cifradas con Fernet AES |
| `.secret.key` | Clave Fernet AES |
| `.superpin` | Hash SHA256 del PIN superadmin |
| `empresa_bds.json` | Datos empresa por BD (host:port:database) |
| `config_sistema.json` | Configuración: días validez cotización |
| `version.json` | Versión actual del sistema |
| `version_router.py` | Router FastAPI: `GET /api/version` |
| `configurador.py` | Utilidades de configuración |
| `reparar.py` | Script de reparación |
| `requirements.txt` | Dependencias Python |

---

## Base de datos — Tablas

### `gecli` — Clientes
| Campo | Descripción |
|-------|-------------|
| `CLIRUT` | RUT sin DV (PK) |
| `CLINOM` | Nombre / Razón social |
| `CLIGIRO` | Giro comercial |
| `CLIDIR` | Dirección |
| `CLICIU` | Código ciudad → JOIN `geciu.CIUCOD` → `CIUDES` |
| `CLIMAIL` | Email |
| `CLITEL` / `CLICEL` | Teléfono / Celular |
| `CLICONT` | Contacto |
| `CLITIPREC` | Lista de precios asignada (1–15) |
| `CLIDESC` | Descuento % |
| `CLITERM` | Condición de pago |
| `CLIVEN` | Código vendedor asignado → FK `gevend.VENCOD` |

### `geitems_mae` — Productos / Inventario
| Campo | Descripción |
|-------|-------------|
| `ITCOD` | Código (PK) |
| `ITDES` | Descripción |
| `ITPREC` | Precio lista 1 |
| `ITPREC2`…`ITPREC15` | Precios listas 2–15 |
| `ITCOSTO` | Costo (para cálculo de utilidad) |
| `ITSTOCK` | Stock total unitario |
| `ITBO1`…`ITBO20` | Stock por bodega |
| `ITUNIMED` | Unidad de medida |
| `ITFAM` | Familia → FK `geitems_fam.ITFA_COD` |
| `ITESTADO` | 1=activo, 0=inactivo |
| `ITCAMPO1/2/3` | Bodega / Ubicación / Estante |

### `geitems_cod` — Códigos alternativos de productos
| Campo | Descripción |
|-------|-------------|
| `CCODORI` | Código original del producto = `geitems_mae.ITCOD` (un producto puede tener varias filas) |
| `CCODALT` | Código alternativo |

> Listado/export en **Inventario → "🔀 Códigos alternativos"** (cruza `CCODORI` con `ITCOD`). Endpoints: `GET /api/inventario/codigos-alternativos` (JSON), `/pdf` (agrupado por producto), `/excel` (openpyxl, hojas "Por producto" y "Por código"). Por defecto solo productos activos. Las descargas en el frontend usan `fetch`+blob (el wrapper global de `window.fetch` agrega el token Bearer; `window.open` daría 401).

### `geitems_fam` — Familias de productos
| Campo | Descripción |
|-------|-------------|
| `ITFA_COD` | Código PK (jerarquía por longitud: 2=nivel1, 4=nivel2…) |
| `ITFA_DES` | Descripción |
| `ITFA_NRO` | Número de orden |
| `ITFA_COM` | Comisión % vendedor (creada automáticamente si no existe) |

### `gecot_enc` — Cotizaciones legado (sistema anterior)
| Campo | Descripción |
|-------|-------------|
| `CONRO` | Número (PK) |
| `COFEC` | Fecha |
| `COCLI` | RUT cliente |
| `CONOM` | Nombre cliente |
| `COTOTAL` | Total con IVA |
| `CONETO` | Neto |
| `COIVA` | Monto IVA |
| `COESTADO` | ACTIVA / MODIFICADA / PENDIENTE / COMPLETA / CERRADA / PARCIAL / ANULADA |
| `COVEN` | Código vendedor |
| `COREF` | Referencia / OC |
| `CONOTA1/2/3` | Notas (mostrar en líneas separadas; cada una varchar(100)) |
| `COCREAFEC` | Fecha/hora creación (columna opcional, se detecta al arrancar) |
| `COCREAUSER` | Código de usuario que crea (varchar 6) |
| `COTIPO` | Tipo de documento (NOT NULL) → FK `getipos` (tipo=1): CO, D/T, EMAIL, NV, TI |
| `COTIPOMOD` | Tipo del documento modificado (vacío en cotización nueva; va con `CONROMOD`) |
| `COTERMS` / `COREF` | Condición de pago (código / texto) |
| `CODESC_POR` | Descuento global % (NOT NULL) |
| `COIVAF` | Tasa IVA, ej. 19.00 (NOT NULL) |
| `CODESPACHO` / `CORETIRA` / `COTRANSPORTE` | Modo de entrega (flags 0/1: despacho / retira / transporte) |
| `COENVIO` | Código transportista → FK `getipos` (tipo=4) |
| `COREF2` / `COREF3` | Orden de compra / Destino |

> **Columnas NOT NULL** de `gecot_enc`: `CONRO, CONETO, CODESC, COIVA, COTOTAL, COIVAF, CODESC_POR, COTIPO`.
> **Numeración (`CONRO`)**: correlativo en `gedescriptores` (PK compuesta `des_pk`+`codigo`; la fila `des_pk=1` ES `codigo='GECOT_ENC'`). El ERP no actualiza el contador de forma confiable, por lo que el web toma `MAX(CONRO)+1` real (blindado con `max(MAX+1, contador+1)`), con `FOR UPDATE` y retry anti-duplicado.

### `gecot_det` — Detalle cotizaciones legado
| Campo | Descripción |
|-------|-------------|
| `CODNRO` | N° cotización → FK `gecot_enc.CONRO` (no hay PK propia) |
| `CODTIPO` | Tipo (igual al `COTIPO` de la cabecera) |
| `CODITEM` | Código producto |
| `CODCANT` | Cantidad |
| `CODPREUNIT` | Precio unitario |
| `CODSUBTOT` | Subtotal = `round(CODPREUNIT × (1-d1/100) × (1-d2/100)) × CODCANT` (convención ERP) |
| `CODDESC` | Descuento 1 % |
| `CODDESC2` | Descuento 2 % |
| `CODPOS` | Posición de la línea (1..n) |
| `CODFACT` | N° de factura asociada; NULL/0 = pendiente de facturar |

### `cot_cabecera` — Cotizaciones sistema nuevo
| Campo | Descripción |
|-------|-------------|
| `id` | Auto-increment PK |
| `numero` | Número generado (ej: COT-2026-00001) |
| `fecha` | Date |
| `clirut` | RUT cliente |
| `cliente_nombre`, `cliente_dir`, `cliente_ciudad`… | Datos cliente al momento de cotizar |
| `total`, `neto_final`, `iva_monto` | Montos |
| `estado` | ACTIVA / FACTURADO / RECHAZADA / VENCIDA |
| `vendedor_cod`, `vendedor_nom` | Vendedor |
| `orden_compra` | OC del cliente |
| `recargo_total`, `recargo_monto` | Recargo % o $ |

### `cot_detalle` — Detalle cotizaciones sistema nuevo
| Campo | Descripción |
|-------|-------------|
| `id` | PK |
| `cot_id` | FK `cot_cabecera.id` |
| `itcod`, `itdes`, `itunimed` | Producto |
| `cantidad`, `precio_unitario` | |
| `descuento_item`, `descuento_item2` | Descuentos % |
| `subtotal` | |

### `cot_solicitudes_cierre` — Solicitudes de cierre
| Campo | Descripción |
|-------|-------------|
| `id` | PK (Auto-increment) |
| `cot_nro` | Número de cotización |
| `cot_origen` | 'nuevo' o 'legado' |
| `estado` | pendiente / aprobada / rechazada |
| `solicitado_por` | Vendedor que solicitó el cierre |
| `monto` | Monto de la cotización |

### Guardar cotización en el ERP (gecot_enc / gecot_det) — desde v1.0.216
El formulario de Nueva Cotización tiene **dos** botones de guardado:
- **"Guardar Cotización"** → sistema nuevo (`cot_cabecera` / `cot_detalle`), endpoint `POST /api/cotizaciones`.
- **"Guardar en ERP"** → tablas legadas del ERP (`gecot_enc` / `gecot_det`), endpoint `POST /api/cotizaciones/legado`. Se integra con el ERP en marcha sin conflictos.

Campos extra capturados para el ERP: **Modo de Entrega** (Despacho/Retira/Transporte → `CODESPACHO`/`CORETIRA`/`COTRANSPORTE`), **Transportista** (`COENVIO`), y `COTIPO` desde el select "Tipo Cotización". `COVEN` sale del código de vendedor (`window._vendedorCotCod` o `usuarioActual.cod_vendedor`), `COCREAUSER` de `usuarioActual.codigo`. Tras guardar, PDF/Email/WhatsApp usan los endpoints legados (`/api/cotizacion-legado/{conro}/...`).

### `gevend` — Vendedores
| Campo | Descripción |
|-------|-------------|
| `VENCOD` | Código (PK) |
| `VENNOM` | Nombre |
| `VENCEL` / `VENTEL` | Celular / Teléfono |
| `VENEST` | 1=activo, 0=inactivo |

### `cot_usuarios` — Usuarios del sistema
| Campo | Descripción |
|-------|-------------|
| `codigo` | Login (PK) |
| `clave` | Contraseña (texto plano) |
| `nombre` | Nombre completo |
| `rol` | vendedor / supervisor / admin / superadmin |
| `activo` | 1=activo |

### Base de fecha de los informes (emisión vs creación) — desde v1.0.217
Configurable en *Parámetros → Documentos* (`app_config.json` → `base_fecha_informes`: `emision` | `creacion`, default `emision`). Define la columna de fecha para filtrar/agrupar **todos** los informes de facturación:
- `emision` (default, contablemente correcto): `gefac_enc.FAFEC` / `gebol_enc.BOLFEC` / `genc_enc.NCFEC` (fecha del documento).
- `creacion` (comportamiento anterior): `FAFECHAHORA` / `BOLFECHAHORA` / `NCFECHAHORA` (fecha-hora de digitación).

En el código (`app.py`): helper `_fcol(prefix, alias="")` con `prefix` ∈ {`FA`,`BOL`,`NC`} devuelve la columna según la config; `_base_fecha()` lee el modo. Endpoints `GET/POST /api/config/base-fecha`. **Excepción:** el heatmap día×hora de Ventas siempre usa `FAFECHAHORA`/`BOLFECHAHORA` (la fecha de emisión no tiene hora). Al cambiar la base conviene borrar `estadisticas_anuales_cache.json` (caché 24h).

### `getipos` — Tipos generales
- `tipo=1` → tipos de cotización (`COTIPO`): CO, D/T, EMAIL, NV, TI → `GET /api/tipos-cotizacion`
- `tipo=4` → transportistas (`COENVIO`) → `GET /api/transportistas`

### `geciu` — Ciudades
- `CIUCOD` → código | `CIUDES` → nombre ciudad

---

## Roles de usuario y acceso
| Rol | Funciones principales |
|---|---|
| `vendedor` | Nueva Cotización + Historial (solo sus cotizaciones) + Inventario (sin columna Costo) |
| `supervisor` | Todo lo del vendedor + Historial completo + Informes básicos |
| `admin` | Todo + Usuarios + Empresa + Parámetros + Inventario (con Costo) |
| `superadmin` | Todo lo anterior + Conexiones BD |

### Permisos granulares y Restricciones de Datos Sensibles
Desde la versión 1.0.201, los administradores pueden parametrizar qué datos sensibles ve cada vendedor. En la sección "Restricciones de datos sensibles", pueden activar opciones para ocultar:
- Utilidad en cotizaciones (`ocultar_utilidad`)
- Costo en inventario (`ocultar_costo`)
- Costo en la pestaña de reposición (`ocultar_costo_reposicion`)
- La pestaña completa de reposición (`ocultar_reposicion`)
- El panel de Estadísticas Anuales y Proyección (`ocultar_estadisticas`)
Además, se pueden restringir los paneles para que muestren *solo sus comisiones* y *solo sus ventas*.

> **Columna Costo** en Inventario: oculta para `vendedor`, visible para `admin/superadmin`.  
> **Columna Conexiones** en Informes: solo `superadmin`.

> **Autorizar cotizaciones** (PENDIENTE → ACTIVA, botón "🔐 Autorizar" en el historial y "Aprobar" en el dashboard): exclusivo de `admin`/`superadmin`. Un vendedor **no puede** autorizar (botón oculto + acción bloqueada en `abrirAutorizacion()`); el backend además exige la `clave_admin` de un usuario admin/superadmin (`POST /api/cotizaciones/autorizar-pendiente` y `rechazar-pendiente`).

---

## Estados de cotizaciones

### Sistema nuevo (`cot_cabecera.estado`)
| Estado | Badge | Color |
|--------|-------|-------|
| ACTIVA | A | Verde |
| FACTURADO | F | Azul |
| RECHAZADA | R | Rojo |
| VENCIDA | V | Naranja |
| PENDIENTE_CIERRE | PC | Amarillo |
| CERRADA | C | Gris |

### Sistema legado (`gecot_enc.COESTADO`)
Se muestra tal cual, excepto: `COMPLETA` → se muestra como **FACTURADO**

### Badges completos en historial
`A`=Activa(verde) `F`=Facturado(azul) `M`=Modificada(amarillo) `C`=Cerrada(gris/azul) `P`=Parcial(azul) `R`=Rechazada(rojo) `V`=Vencida(naranja) `P`=Pendiente(amarillo) `PC`=Pendiente Cierre(amarillo)

---

## Menú de navegación (según rol)

```
Nueva Cotización | Historial | Inventario* | Usuarios* | Informes* | Parámetros* | Mi Empresa* | Conexiones**

*  admin / superadmin
** solo superadmin (requiere PIN de 6 dígitos)
```

### Parámetros — tabs
| Tab | Contenido |
|-----|-----------|
| ⚙ Configuración | Días de validez de cotización |
| 📝 Documentos | Nombre del documento (`titulo_doc`/`titulo_doc_plural`) + **Base de fecha de informes** (emisión/creación). Ver sección correspondiente. |
| 👥 Clientes | CRUD `gecli` (ciudad desde `geciu`, vendedor desde `gevend`) |
| 🤝 Vendedores | CRUD `gevend` + toggle activo/inactivo (`VENEST` 0/1) |
| 🗂 Familias | Árbol `geitems_fam` (5 niveles por largo de código) + comisión (`ITFA_COM` %, `ITFA_COM_TIPO` pct/fijo, `ITFA_COM_FIJO`) |
| 🎨 Paleta | 10 variables CSS, guardadas en `localStorage` key `paleta_colores` |

> **Nombre del documento configurable:** la palabra "Cotización" no está fija. Se define en *Parámetros → Documentos* (`app_config.json` → `titulo_doc`/`titulo_doc_plural`) y en el frontend se aplica con `_applyDocTitles()`. Dos mecanismos: (1) elementos con `id="lbl-*"` (menú, historial, botones, hero, KPI, condiciones, modales email/WA, etc.); (2) **clases CSS para palabras sueltas**: `.lbl-doc` (singular), `.lbl-doc-low` (singular minúscula), `.lbl-doc-pl` (plural), `.lbl-doc-pl-low` (plural minúscula). Al agregar textos con el nombre del documento, usar `${_appCfg.titulo_doc}` o un `<span class="lbl-doc...">`, nunca "Cotización" literal. **No se toca** el historial de versiones ni los ejemplos del propio tab Documentos.
>
> **Nombre del sistema** = "GESTION WEB" (branding fijo: login, título de pestaña, topbar por defecto). Distinto del nombre del documento y del nombre de empresa (*Mi Empresa*).

> **Códigos de familia (`ITFA_COD`)**: largo par (2=nivel1, 4=nivel2, 6=nivel3), con cero a la izquierda (`01`, `0101`, `010203`). Al importar de Excel hay que normalizar (Excel quita el cero inicial). `gamspa` usa estos códigos.

### Informe de Comisiones (tab 💰 en Informes)
- Vistas: **Por familia** / **Por vendedor** / **📈 Evolución**.
- Toggles: **Con descuentos** (neto final vs bruto) y **Heredar % COM de raíz cuando directa = 0**.
- Filtros de familia (3 botones): **Todas** · **Solo con comisión** (familias con % configurado) · **Con vendedor** (solo familias con al menos un vendedor con código real y ventas en el período; `SIN_VEN` no cuenta).
- Cascada de comisión: excepción por vendedor (`geitems_fam_vend`) → comisión de familia → comisión de raíz. Cálculo: `_calcCom(base,cant,tipo,pct,fijo)` = `cant*fijo` si tipo='fijo', si no `base*pct/100`.
- Detalle por vendedor (expandible en Por familia): tabla de movimientos con **N° Doc · Fecha · Descripción · Cant. · Subtotal · Comisión**. La columna Fecha es la fecha del documento según la base configurada. Endpoint `GET /api/informes/comisiones/lineas?fecha_desde&fecha_hasta&fam_cod&ven_cod` (filtra por el período).
- El botón **Actualizar** muestra estado de progreso (spinner + cronómetro + estimación según la última carga); la duración se guarda en `localStorage` (`com_last_ms`).
- Endpoint: `GET /api/informes/comisiones?fecha_desde&fecha_hasta&vendedor`.

---

## Endpoints API

```
GET  /api/db-status
POST /api/login
GET  /api/historial-unificado
GET  /api/productos?q=&lista=
GET  /api/catalogo?lista=
GET  /api/clientes/buscar?q=
GET  /api/clientes/{rut}/info-cotizacion
PUT  /api/clientes/{rut}/vendedor?vencod=
GET  /api/vendedores
GET  /api/tipos-cotizacion                  ← getipos tipo=1 (COTIPO)
GET  /api/transportistas                    ← getipos tipo=4 (COENVIO)
POST /api/cotizaciones                       ← sistema nuevo (cot_cabecera)
POST /api/cotizaciones/legado                ← ERP legado (gecot_enc/gecot_det)
GET  /api/cotizaciones/{id}/pdf
POST /api/cotizaciones/{id}/email
GET  /api/cotizacion-legado/{conro}
GET  /api/cotizacion-legado/{conro}/pdf
POST /api/cotizacion-legado/{conro}/email    ← email con PDF para cotización del ERP
GET  /api/producto-ubicacion?itcod=        ← query param, NO path param (los códigos pueden tener "/")
GET  /api/mantenedor/clientes
GET  /api/mantenedor/vendedores
GET  /api/mantenedor/familias
GET  /api/mantenedor/inventario?orden=&asc=&bodega=&familia=&estado=&buscar=
PATCH /api/mantenedor/inventario/{itcod}/estado
GET  /api/inventario/codigos-alternativos?buscar=&incluir_inactivos=   ← JSON (geitems_cod)
GET  /api/inventario/codigos-alternativos/pdf                          ← PDF (ReportLab)
GET  /api/inventario/codigos-alternativos/excel                        ← Excel 2 hojas (openpyxl)
GET  /api/informes/pdf-clientes
GET  /api/informes/comisiones?fecha_desde&fecha_hasta&vendedor
GET  /api/informes/comisiones/lineas?fecha_desde&fecha_hasta&fam_cod&ven_cod   ← detalle con Fecha
GET  /api/config/titulos                      ← titulo_doc + base_fecha_informes (público)
POST /api/config/titulos?titulo_doc&titulo_doc_plural
GET  /api/config/base-fecha                    ← 'emision' | 'creacion'
POST /api/config/base-fecha?base=emision|creacion
POST /api/cotizaciones/autorizar-pendiente
POST /api/cotizaciones/solicitar-cierre
GET  /api/cierres/conteo
GET  /api/cierres/pendientes
POST /api/cierres/{cid}/aprobar
POST /api/cierres/{cid}/rechazar
POST /api/cierres/procesar-vencidas
GET  /api/config/empresa-bd
PUT  /api/config/empresa-bd
POST /api/superadmin/reload-config
GET  /api/superadmin/conexiones
POST /api/superadmin/verificar-pin
POST /api/superadmin/conexiones/{idx}/activar
GET  /api/version
```

---

## Variables globales Python (`app.py`)

```python
IVA = 0.19   # Debe declararse al inicio del archivo

STOCK_CONFIG = {
    "modo": "unitario" | "multibodega",
    "expresion": "ITSTOCK" | "COALESCE(ITBO1,0)+...",
}

GECOT_CONFIG = {
    "tiene_cocreafec": bool,  # Si gecot_enc tiene columna COCREAFEC
    "tiene_coenvio":   bool,  # Si gecot_enc tiene columna COENVIO
}
```

---

## Variables globales JavaScript (cotizaciones.html)

`js
API              // URL base del backend (autodetectada)
usuarioActual    // { codigo, nombre, rol, permisos: '{}' } -> ¡OJO! 'permisos' es un string JSON que DEBE parsearse con JSON.parse(usuarioActual?.permisos || '{}')
items[]          // Productos en cotización activa
clienteActual    // Cliente seleccionado en cotización activa
_famFlat[]       // Caché de familias compartido entre tab Familias e Inventario
_ocCosto         // Flag ocultar costo (tab Parámetros -> Inventario)
_ocCosto2        // Flag ocultar costo (página Inventario)
```js
API              // URL base del backend (autodetectada)
usuarioActual    // { codigo, nombre, rol }
items[]          // Productos en cotización activa
clienteActual    // Cliente seleccionado en cotización activa
_famFlat[]       // Caché de familias compartido entre tab Familias e Inventario
_ocCosto         // Flag ocultar costo (tab Parámetros → Inventario)
_ocCosto2        // Flag ocultar costo (página Inventario)
```

### Claves localStorage
| Clave | Contenido |
|-------|-----------|
| `cot_borrador` | Autosave de cotización en curso |
| `paleta_colores` | Paleta de colores personalizada |

---

## Funciones JS críticas (NO eliminar)

```
// Historial
inicializarHistorial()    cargarHistorial()         estadoBadge(estado)
toggleOrigenCol()         renderTabsMes()            cargarVendedoresHistorial()
_iniciarAutoRefresh()     resetHistTimer()           toggleHistAuto()

// Inventario
cargarInventario2()       invOrdenar2()
invCambiarFamilia2()      invSetAlcance2()
toggleEstadoProducto()

// Parámetros
cargarMantClientes()      cargarMantVendedores()     cargarFamilias()
renderFamilyTree()        toggleFamNode()
toggleEstadoVendedor(v, nuevoActivo)  // v = objeto completo {cod, nom, cel, tel, activo}
switchMtab()              iniciarPaleta()            guardarPaleta()

// Nueva cotización
agregarProducto()         renderItems()              calcularTotales()
onChangeLista()           cambiarPrecioItem()        agregarConLista()
renderAC()                renderCatalogo()
guardarCotizacion()       descargarPDF()
guardarCotizacionERP()    descargarPDFLegado()       // guardar en ERP (gecot)
cargarTransportistas()    onChangeModoEntrega()      // modo entrega + transportista

// Autosave
autosaveDraft()           verificarBorrador()        recuperarBorrador()
aplicarPaletaGuardada()

// Autorización (PENDIENTE → ACTIVA)
abrirAutorizacion()       confirmarAutorizacion()
buscarPorNumero()         abrirBuscarNumero()

// Conexiones BD (dos sistemas — NO mezclar IDs)
iniciarConexiones()       verificarPin()
cargarConexionesPage()    cargarSBDConexiones()   // debe ser async (tiene await)
sbdActivarConexion()      abrirFormConexion()     guardarConexion()

// Sistema
showPage()                doLogin()               doLogout()
verificarBD()             cargarEmpresa()         recargarTrasCambiobd()
```

---

## Conexiones BD — Dos sistemas distintos (NO confundir)

| Sistema | Overlay | Contenido | Grilla |
|---------|---------|-----------|--------|
| Login superadmin | `sbd-pin-wrap` | `sbd-content` | `sbd-grid` |
| Página Conexiones | `pin-overlay` | `conn-content` | `conn-grid` |

- `verificarPin()` → llama `cargarConexionesPage()` → usa `conn-grid`
- `cargarSBDConexiones()` → usa `sbd-grid`
- `cargarSBDConexiones` **debe ser `async`** (contiene `await`)

---

## Convenciones del código

### Python (`app.py`)
- Usar `PyMySQL` con `cursorclass=DictCursor`
- Siempre `db.close()` al final de cada función
- Campos opcionales: `Optional[str] = ""`
- `format_pesos(valor)` → `$1.234.567`
- `format_rut(rut)` → `12.345.678-9`
- Todo endpoint necesita decorador `@app.get / post / put / patch / delete`

### JavaScript (`cotizaciones.html`)
- `formatPesos(v)` → formato moneda CLP
- Template literals: **nunca** `${...}` dentro de `onclick="..."` → usar `data-*` + event delegation
- Event delegation en tabla de ítems: `.ubic-btn` (ubicación), `.lista-item-sel` (cambio lista)
- `toggleEstadoVendedor(v, nuevoActivo)` → recibe objeto completo, **no** hacer GET extra

---

## ⚠️ Problemas recurrentes a evitar

1. **Variables JS duplicadas** — Antes de agregar `let X`, verificar con:
   ```bash
   grep -n "^let X\|^const X" cotizaciones.html
   ```

2. **`await` fuera de `async`** — Verificar siempre con `node --check archivo.js`

3. **Template literals anidados** — Nunca `${...}` dentro de `onclick="..."`:
   ```js
   // ❌ MAL:
   `<button onclick="fn(${JSON.stringify(obj)})">` // rompe con comillas
   // ✅ BIEN:
   `<button class="mi-btn" data-idx="${i}">` // + listener en document
   ```

4. **Decoradores faltantes** — Todo endpoint FastAPI necesita su `@app.get/post/...`

5. **Funciones perdidas** — Al hacer reemplazos grandes, verificar que sigan existiendo:
   `cargarHistorial`, `cargarInventario2`, `cargarSBDConexiones`, `abrirFormConexion`

6. **`IVA` no definido** — Declarar `IVA = 0.19` **al inicio** de `app.py`

7. **Endpoint de ubicación** — Usar `/api/producto-ubicacion?itcod=...` (query param), **nunca** path param (los códigos pueden contener `/`)

8. **Dos overlays de conexiones** — `sbd-*` (login) y `conn-*` (página). No mezclarlos.

9. **`cargarSBDConexiones` debe ser `async`** — Tiene `await` interno.

---

## Sistema de versiones (actualización automática)

- `version.json` → fuente de verdad con `latest`, `release_date`, `notes`
- `GET /api/version` → devuelve versión actual vs latest
- Si hay nueva versión → aparece toast en el frontend invitando a actualizar
- El toast se ignora sin interrumpir al usuario si el backend no responde

---

## 🆕 Funcionalidades agregadas (v1.0.216 → v1.0.250)

### Empresa, geparam y logo
- **Datos de empresa desde `geparam`** (por BD): `_empresa_de_geparam(cur)` + `_empresa_actual(cur=None)` (geparam + logo). Campos solo lectura en *Mi Empresa* (se administran en el ERP).
- **Logo por subida de archivo y por BD**: el navegador no puede mostrar rutas locales (`c:\...`), por eso el logo se **sube** (`POST /api/config/empresa/logo`, multipart) y se sirve con sesión (`GET /api/config/empresa/logo`). Se guarda **por base de datos** en `logos_bd.json` (`{dbname:{logo:ruta}}`) y archivo `logo_{dbname}.ext`. Al cambiar de conexión el logo cambia automáticamente.

### Subida de PDF de cotizaciones al hosting (`ftp_config.json`, por BD, superadmin)
Tres protocolos seleccionables en *Parámetros → Mi Empresa → Subir PDF*:
- **FTP / FTPS** (ftplib): modo pasivo/activo; override de `makepasv` para ignorar la IP interna que cPanel devuelve en PASV (evita WinError 10060).
- **WebDAV** (`requests.put`): por HTTPS, sin puertos pasivos.
- **Script PHP** (`requests.post`): sube a un `upload.php` con token secreto. El PHP se **descarga** ya configurado desde `GET /api/config/ftp/upload-php` y se sube una vez al hosting. El más confiable en hosting compartido. (cPanel no expone WebDAV en la URL web normal → por eso el script PHP.)
- Helpers: `_subir_pdf_ftp(dbname,filename,bytes,force)` enruta por `protocolo`; `_subir_webdav`, `_subir_script`, `_ftp_conectar`. Endpoints: `GET/POST /api/config/ftp`, `/probar`, `/probar-subida`, `/crear-carpeta`, `/upload-php`.
- **PDF protegido con clave** opcional = últimos 4 dígitos del RUT (cifrado ReportLab). Helper `_clave_pdf_rut(clirut)`.
- **URL pública** del PDF (para WhatsApp) se deriva del protocolo activo si no se define manualmente.
- Subida automática al guardar cotización en ERP + bajo demanda al enviar por WhatsApp (`POST /api/cotizacion/subir-pdf-ftp?id=&legado=`).

### WhatsApp y Email
- **WhatsApp** sin forzar la app: escritorio → `web.whatsapp.com`, móvil → `wa.me`. En escritorio NO usa `navigator.share` (evita el diálogo de compartir de Windows). El mensaje enlaza al **PDF público del hosting** + clave si está protegido.
- **Email y WhatsApp desde el historial también para cotizaciones legadas** (antes solo Ver/PDF). Endpoint detalle legado devuelve `clirut` y `cliente_celular`.

### PDF de la cotización (`_generar_pdf_bytes(cab, items, emp=None, encrypt_pwd=None)`)
- Empresa desde geparam (no estática). Columnas **Dto.1% / Dto.2%** separadas. Validez real (`_validez_legado` = COFECVEN−COFEC) o configurada (`_dias_validez()` de `config_sistema.json`). **Paleta azul del sistema** (no grises).

### Notas de venta offline + PWA
- Permiso por usuario `nota_venta_local`. Botón "Nota de venta (local)" → cola `localStorage` `gw_notas_offline` con `cli_uuid`. Sincroniza al ERP al recuperar conexión.
- Idempotencia: tabla `cot_notas_offline` (uuid→conro, MySQL 5.5 compatible). El `POST /api/cotizaciones/legado` acepta `cli_uuid` y no duplica.
- **PWA**: `GET /manifest.webmanifest`, `GET /sw.js`, íconos `/icon-192.png` y `/icon-512.png`. Activable en *Parámetros → Conexiones* (APP_CONFIG `pwa_offline_activo`, `GET /api/config/pwa` público + `POST /api/config/pwa/guardar`).

### Comisiones (mejoras)
- **Vendedor 2**: si un usuario tiene `cod_vendedor="0019,34"`, las facturas del código 34 usan su % especial. El backend expande el filtro automáticamente.
- **Cascada de 3 niveles**: `com_vend[fam][ven]` → `com_vend_global[ven]` (tasa única del vendedor, aplica a todas las familias) → % de la familia. Backend devuelve `com_vend` y `com_vend_global`.
- **Toggle "Por código / Por persona"** en vista Por vendedor (`persona_grupos` agrupa los códigos del mismo usuario).
- **PDF de comisiones imprimible** (`GET /api/informes/comisiones/pdf`): 2 secciones (Por persona + Por código).

### Dashboard — Proyección de ventas del mes
- `GET /api/ventas-gefac/proyeccion-mes?faven=`. Neto = fac+bol−NC (helper `_neto_mes`). Si día>1: `(acumulado ÷ días transcurridos) × días del mes`; si día≤1: promedio del mismo mes de los 2 años anteriores. Comparativos con año anterior (a la fecha y mes completo). Tarjeta en `page-inicio` con botón "¿Cómo se calcula?".

### Estado de cotizaciones (superadmin) + auditoría
- Pestaña Estados (solo superadmin): cambia estado de cualquier cotización con motivo obligatorio. Auditoría en `cot_estado_log` (quién/cuándo/IP/equipo).

### gamspa (MySQL 5.5) y cotización PENDIENTE
- En `gamspa` las cotizaciones del ERP nacen **PENDIENTE** (APP_CONFIG `cotizacion_pendiente_dbs`). DDL debe ser 5.5-compatible (`TIMESTAMP DEFAULT CURRENT_TIMESTAMP`, no `DATETIME`).

### Permisos JSON nuevos (cot_usuarios.permisos)
`nota_venta_local`, `solo_sus_comisiones`, `solo_sus_ventas`, `ocultar_utilidad`, `ocultar_costo`, `ocultar_costo_reposicion`, `ocultar_reposicion`, `ocultar_estadisticas` (además de los originales).

### Reglas de mantenimiento
- **Al cambiar algo, actualizar SIEMPRE** `HISTORIAL_ACTUALIZACIONES.md` **y** el historial hardcodeado en el HTML (tarjetas `<!-- vX.Y.Z -->`).
- Validar: `python -c "import ast; ast.parse(open('app.py',encoding='utf-8').read())"` y `node --check` sobre el JS extraído del HTML.

---

## 🆕 Funcionalidades agregadas (v1.0.251 → v1.0.296)

### Organización del proyecto e instalación
- Carpetas: **`instalacion/`** (todos los `.bat`, monitor de bandeja, `LEEME_*`, `Caddyfile.ejemplo`), **`hosting/`** (`upload.php`), **`_dev/`** (temporales). Los `.bat` detectan solos la carpeta del código (la propia o la de arriba) y se guardan en **CRLF**.
- **Instaladores robustos**: `instalar.bat`/`iniciar.bat`/`servicio_*.bat` detectan Python real (`py -3` evitando el alias de Microsoft Store). Servicio Windows vía NSSM + ícono de bandeja (`monitor_bandeja.pyw`).

### Multi-empresa (Opción A — 1 código, N instancias)
- **`GW_DATA_DIR`**: el backend lee config/datos (`.secret.key`, `conexiones.json`, cachés, logos…) desde esa carpeta (env var). Sin definirla → carpeta del código (modo mono-empresa de siempre). Helpers `_data_path()` / `_code_path()`.
- `crear_empresas.bat` (carpetas de datos por empresa) + `servicio_instalar_empresa.bat NOMBRE PUERTO CARPETA` + **Caddy** (subdominios por empresa, HTTPS automático). Guía `LEEME_MULTIEMPRESA.txt` / `LEEME_CADDY.txt`.

### Fecha en hora de Chile
- Front: helper **`_fechaLocal()`** (nunca `toISOString()`, que da UTC y corría el día por la tarde). Back: **`_now_cl()` / `_today_cl()`** con `zoneinfo("America/Santiago")` (requiere paquete **`tzdata`** en Windows) para la fecha de documentos.

### Modo offline completo (PWA, tablet)
- **`GET /api/sync/paquete`**: productos+precios+stock, clientes, vendedores, familias, condiciones, tipos, transportistas → **IndexedDB** (`gw_offline`). Auto-sync al haber red; búsqueda de productos/clientes, catálogo, últimas compras y **PDF** (vista imprimible que calca el oficial) funcionan sin conexión. Aviso al perder conexión.

### Menú y responsive
- Menú agrupado a la derecha; en móvil **☰ a la derecha** que despliega en columna. **Disposición configurable** (`menu_layout`: top/sidebar) y **módulos parametrizables** (orden + visibilidad global, `menu_orden`/`menu_oculto`) en *Parámetros → Conexiones* (superadmin). Pasada móvil en Inicio y Nueva cotización.

### Sesiones en línea (superadmin)
- JWT con `sid`; tabla **`cot_sesiones_online`** + latido (`POST /api/sesion/latido` cada 25 s) → pestaña "Conexiones activas" muestra quién está **en línea** con hora de conexión/última actividad/desconexión y permite **enviar un mensaje + desconectar** (cooperativo). `POST /api/superadmin/sesion/expulsar`, `/api/sesion/cerrar`.

### Inventario más rápido (caché)
- Snapshot en memoria por BD (`_INV_CACHE`), filtro/orden/paginado en servidor → casi instantáneo. **Parametrizable** en *Parámetros → Conexiones* (`inv_cache_activo`, `inv_cache_ttl`); si el servidor es rápido se puede desactivar (datos en vivo). Botón "↻ Actualizar" (`refresh=1`); se invalida al editar.

### Otros
- **Admin: cambiar tipo de documento** (COTIPO+CODTIPO juntos) pestaña "Tipo Doc" (`_solo_admin`), auditoría en `cot_estado_log`.
- **Inventario por `geparam.cf_tipo_bodega`** (0=ITSTOCK, 1=suma bodegas).
- **Informes Por vendedor**: monto facturado vs pendiente x facturar. **Comisiones**: % de descuento s/bruto + tab **Comparador** (vendedores/familias/periodos).
- **Fix Boletas**: nombre del cliente desde la boleta (`BOLNOM`), no de `gecli` (con RUT genérico 1-9 mostraba otro cliente).
- **Reposición con proveedores**: por cada producto, proveedores + costo de su última compra (desde **`geord_enc`**: `ORCLI`/`ORNOM`/`ORFEC` + **`geord_det`**: `ORDITEM`/`ORDPREUNIT`), filtro por proveedor (`GET /api/inventario/proveedores`); también en el PDF. Endpoint diagnóstico `GET /api/diagnostico/compras`.

## 🆕 Funcionalidades agregadas (v1.0.297 → v1.0.354)

### Reposición — categorías, pedidos y filtros
- **Categorías de productos** (parametrizable por empresa, *Parámetros → 🏷 Categorías*, `categorias_productos.json` por BD): clasifica cada producto por **Tipo** (según familia `ITFAM`) y **Calidad/Espesor/Largo** (detectados en la descripción, con límites para no confundir "304" de un rodamiento con el acero). Filtros en Reposición. Interruptor maestro `activo` por empresa.
- **Importaciones** como 2ª fuente de compras: `geitems_man`(IM)/`geitems_mand`; proveedor desde `gecli` (`CLITIPO='P'`). Front marca 🌍 IM / 🏭 OC.
- **Generar pedido por proveedor**: casillas de selección + modal con cantidad editable, último costo, Imprimir/PDF y **Excel/CSV**.
- **Búsqueda avanzada** (⚙ Más filtros): texto multi-palabra, proveedor, rango de stock/cobertura, urgencia. **Buscador de proveedor** con autocompletado; al elegirlo, pregunta si mostrar **solo sus compras** en la descripción. Tipos de venta como lista desplegable multi-selección. Fijar/liberar encabezado de la tabla.

### Inventario / Kardex
- Kardex muestra **costo del producto** (`geitems_mae.ITCOSTO`) y **costo del movimiento/factura** (`geitems_mov.ITMCOSTO`), y el **nombre del cliente del documento relacionado** por folio (facturas `gefac_enc.FANOM`, boletas `BOLNOM`, NC `NCNOM`) + nombre del tipo de doc (`getipos`).

### Menú colapsable con submenús (grupos)
- Grupos configurables (*Parámetros → Conexiones → 📂 Grupos del menú*, `menu_grupos`): acordeón en sidebar, dropdown flotante en barra superior; ícono+título por grupo (selector de iconos), módulos asignables (uno por grupo). El grupo activo se resalta/abre solo.

### Documentos emitidos (módulo nuevo, separado de Informes)
- **Facturas + Boletas + Notas de Crédito** salieron de Informes a su propio módulo `page-documentos` (con permisos respetados). Informes queda con Cotizaciones + Comisiones.
- **Nombres de pestañas configurables** (`tabs_nombres`, *Parámetros → Conexiones → 🏷 Nombres de pestañas*).
- **📚 Libro de Ventas (SII)**: une `gefac_enc`+`gebol_enc`+`genc_enc` del período. Dos vistas: **Clásica** idéntica al SII (encabezado empresa/RUT, totales y correlativos centrados, lista para imprimir/PDF) y **Pro** (KPIs + buscador). Exporta **Excel (.xlsx)**. Cod.SII por tipo (33/34/39/38/61), NC en negativo (`NCNETO−NCDESC`). Endpoints `GET /api/documentos/libro-ventas[/excel]`. **Validado: cuadra exacto con el libro real** (mayo: total neto 97.922.107).

### Otros ajustes
- **Reconexión a BD**: `get_db()` reintenta 3 veces la conexión activa antes de caer a "modo sin BD"; timeouts de lectura/escritura.
- **Cabecera**: BD conectada + estado (●) + versión en una 2ª línea bajo el nombre de la empresa.
- **Geografía de Ventas**: vendedores en lista desplegable multi-selección.

## 🆕 Funcionalidades agregadas (v1.0.355 → v1.0.380)

### Libro de Ventas — exportación y orden
- Excel del Libro incluye al final las **tablas de Totales y Correlativos** (con bordes/encabezados, formato SII). Descarga vía `fetch`→blob (con token; antes `window.open` daba 401). Vista **Pro** ordena las fechas de **mayor a menor**; la Clásica se mantiene cronológica.

### Conciliación bancaria (Opción A — subir cartola, sin guardar credenciales)
- Botón **🏦 Conciliar banco** en el Libro de Ventas. Se sube la **cartola** que el usuario descarga del banco (no se conecta al banco ni se guardan claves). Tabla `cot_concil_banco` (clase, nro, fecha_banco, monto_banco, glosa, rut_banco, **medio**, usuario).
- **Lectura de cartola** (`_parse_cartola`): detecta el tipo por **contenido** (no por extensión): `.xlsx` (openpyxl), `.csv`, y cartolas **`.xls` que en realidad son HTML** (Banco de Chile) — junta filas de **todas** las tablas con parser por pila (soporta tablas anidadas/varias páginas). Detecta columnas por palabras clave (Fecha / Abonos-Crédito / Descripción / RUT) en todo el documento; lee solo **abonos** (ingresos).
- **Cruce** (`_conciliar_propuestas`): por **monto exacto** 1:1; desempate por **RUT** (incluido el RUT **embebido en la descripción**, ej. `Pago: Proveedores 0805702001`, vía `_rut_en_texto`) y por **nombre del depositante** en la glosa. Ventana de fechas **−31 a +185 días** (la factura se emite al confirmar el pago: el abono suele ser del mismo día o anterior). Marca de **confianza** alta/revisar; se premarcar solo las seguras. Panel de **diagnóstico** (abonos leídos vs documentos pendientes) cuando no hay coincidencias.
- En el libro: columna **Pago** (✔ + medio), KPIs **Transferido/Pendiente**, filtro **Todos / Transferidos / Pendientes**.
- **Medio de pago manual**: desde la columna Pago se marca un documento como pagado por **WebPay, efectivo, tarjeta de crédito/débito, cheque, transferencia u otro** (o se quita), con **Orden de pago / Referencia** (obligatoria para WebPay). Endpoints `POST /api/documentos/cartola/previsualizar`, `.../conciliacion/guardar`, `.../conciliacion/eliminar`. Tabla `cot_concil_banco` con columnas `medio` y `referencia`.

### Pagos WebPay por correo (IMAP)
- Botón **💳 WebPay (correo)** en el Libro de Ventas: lee (solo lectura) los avisos de pago de Transbank (`webpaycl@transbank.cl`) por **IMAP**, extrae **monto, empresa, fecha y Orden de pago**, y los cruza con las facturas pendientes (reusa `_conciliar_propuestas`) para conciliarlas como medio **WebPay** con su **Orden de pago** como referencia.
- **Config IMAP** (`imap_config.json`, clave cifrada Fernet, def Gmail `imap.gmail.com`): servidor, cuenta, remitente y carpeta configurables. Gmail necesita **clave de aplicación** (2FA). Endpoints `GET/POST /api/config/imap`, `GET /api/documentos/webpay/buscar`.
