# Documentación Técnica — Sistema de Reportes · Zonas Áridas **Proyecto:** Zonas Áridas — Reportes internos **Servidor:** Hostinger shared hosting (PHP 8.x + MariaDB) **URL base:** `https://zonasaridas.com.ar/reportes/` **Última actualización:** 2026-05-21 ## 📦 Versiones de módulos | Módulo | Versión | Fecha | Estado | Ubicación de versión | |---|---|---|---|---| | **Caja y Bancos** | `v2.0.0` | 2026-05-21 | ✅ Activo | `caja/version.php` → `CAJA_VERSION` | | ├ Hub | v2.0.0 | 2026-05-21 | ✅ | `caja/index.php` | | ├ Gastos | v2.0.0 | 2026-05-21 | ✅ | `caja/gastos/` | | ├ Tesorería | v2.0.0 | 2026-05-21 | ✅ | `caja/tesoreria/` (5 pestañas) | | └ Finanzas | v2.0.0 | 2026-05-21 | ✅ | `caja/finanzas/` (3 pestañas) | | **Cashflow** | `v1.1.0` | 2026-05-21 | ✅ Activo | `cashflow/version.php` → `CASHFLOW_VERSION` | | **Venta** | (sin versión formal) | 2026-05 | ✅ Activo | — | | **Artículos** | (sin versión formal) | 2026-05 | ✅ Activo | — | | **Listas** | (sin versión formal) | 2026-05 | ✅ Activo | — | > Las versiones de módulos siguen **semver** (`MAJOR.MINOR.PATCH`): > - **MAJOR** = cambios incompatibles de schema, reestructura, breaking changes. > - **MINOR** = funcionalidades nuevas retrocompatibles. > - **PATCH** = fixes / mejoras menores / docs. --- ## Índice 1. [Visión general](#1-visión-general) 2. [Estructura de carpetas](#2-estructura-de-carpetas) 3. [Base de datos](#3-base-de-datos) 4. [Configuración y credenciales](#4-configuración-y-credenciales) 5. [Landing — `reportes/index.php`](#5-landing--reportesindexphp) 6. [Módulo Venta](#6-módulo-venta) → ver doc detallado en `venta/DOCUMENTACION.md` 7. [Módulo Caja](#7-módulo-caja) → ver doc detallado en `caja/DOCUMENTACION.md` 8. [Módulo Artículos](#8-módulo-artículos) → ver doc detallado en `articulos/DOCUMENTACION.md` 9. [Fórmula ChessERP del margen](#9-fórmula-chesserp-del-margen-clave) 10. [Sistema de temas día/noche](#10-sistema-de-temas-díanoche) 11. [Logos y branding](#11-logos-y-branding) 12. [Decisiones de arquitectura](#12-decisiones-de-arquitectura) 13. [Guía de mantenimiento](#13-guía-de-mantenimiento) --- ## 1. Visión general El sistema de reportes está compuesto por **cuatro módulos**: | Módulo | URL | Tecnología | Función | |--------|-----|-----------|---------| | **Landing** | `/reportes/` | PHP puro + Chart.js | Portal con resumen de los 3 módulos | | **Venta** | `/reportes/venta/` | PHP + Chart.js + Tabulator | Análisis de ventas desde API ChessERP | | **Caja** | `/reportes/caja/` | PHP + Tabulator + Chart.js | Gastos de caja desde archivos Excel + datos raw persistidos | | **Artículos** | `/reportes/articulos/` | PHP + Tabulator | Catálogo maestro de SKUs desde Excel del ERP | Los cuatro módulos comparten: - La misma base de datos (`u120688891_chess`) - El mismo sistema de temas (clave `za-theme` en `localStorage`) - Los mismos logos (`/assets/images/logos/`) - La misma paleta visual (fuente Outfit, variables CSS, glass header) --- ## 2. Estructura de carpetas ``` public_html/ ├── assets/ │ └── images/logos/ │ ├── logo_dia.svg │ └── logo_noche.svg │ └── reportes/ ├── index.php ← Landing ├── DOCUMENTACION.md ← Este archivo │ ├── venta/ ← Módulo de ventas │ ├── config/database.php │ ├── includes/ │ │ ├── chess_api_client.php │ │ ├── data_api.php ← Núcleo de queries (margen, KPIs, etc.) │ │ ├── db_reportes.php ← Conexión PDO + bootstrap articulos_raw │ │ └── functions.php ← Formatters PHP (formatMoney, formatPercent) │ ├── assets/ │ │ ├── css/style.css │ │ └── js/app.js ← UI + helpers JS (fmtMoney, openTab) │ ├── api/filter.php ← Endpoint drill-down (cross-filter) │ ├── index.php ← Listado de períodos │ ├── dashboard.php ← Dashboard principal │ ├── sync_api.php ← Carga de un mes │ ├── sync_api_process.php ← Backend del sync (lote × lote) │ ├── reinit_import.php ← Importación masiva │ ├── monitor.php ← Monitor en vivo │ ├── sql/schema_reportes.sql │ └── DOCUMENTACION.md ← Doc detallado del módulo │ ├── caja/ ← Módulo de gastos de caja │ ├── config/database.php │ ├── api/ │ │ ├── _schema.php ← Schema centralizado (7 tablas) │ │ ├── init.php ← Crea schema + inicializa sync │ │ ├── save.php ← Inserta lotes en caja_gastos │ │ ├── save_raw.php ← Inserta lotes en tablas raw │ │ └── get_conceptos.php ← Lee catálogo persistido │ ├── index.php ← Dashboard (Tabulator árbol) │ ├── sync.php ← Carga de un mes │ ├── sync_bulk.php ← Importación masiva multi-mes │ ├── reset_db.php ← Panel admin (limpiar) │ └── DOCUMENTACION.md ← Doc detallado del módulo │ └── articulos/ ← Catálogo maestro de SKUs ├── config/database.php ├── api/ │ ├── _schema.php ← Schema (5 tablas) │ ├── init.php ← Crea schema + TRUNCATE │ └── save.php ← Inserta lotes ├── index.php ← Landing del módulo (KPIs) ├── sync.php ← Carga del Excel └── DOCUMENTACION.md ← Doc detallado del módulo ``` --- ## 3. Base de datos ### Credenciales ``` Host: localhost BD: u120688891_chess Usuario: u120688891_chess Password: t2#h*wvQ2./wZaS Charset: utf8mb4 ``` ### Tablas principales #### Módulo Venta - **`sincronizaciones`** — Control de imports. PK `id`, UK `(mes, anio)`. - **`ventas`** — ~160 columnas, una fila por línea de comprobante de la API ChessERP. FK `sync_id` con `ON DELETE CASCADE`. #### Módulo Caja - **`caja_sincronizaciones`** — Control de imports. - **`caja_gastos`** — Tabla derivada (alimenta dashboard). - **`caja_raw_egresos_caja`** — Catálogo, 34 cols. - **`caja_raw_conceptos`** — Catálogo persistente (no se reemplaza salvo nueva carga). - **`caja_raw_egresos_detalle`** — Histórico por sync (9 cols). - **`caja_raw_gastos_tipificados`** — Histórico (42 cols). - **`caja_raw_movimiento_detalle`** — Histórico (32 cols). - **`caja_raw_movimiento_resumido`** — Histórico opcional (31 cols, IVA discriminado). #### Módulo Artículos - **`articulos_raw`** — Catálogo maestro 66 cols. PK `codigo`. - **`articulos_raw_proveedores`** — Catálogo proveedores. - **`articulos_raw_tipos_mercaderia`** - **`articulos_raw_unidades_medida`** - **`articulos_raw_tipos_presentacion`** > **Bootstrap cross-module**: `venta/includes/db_reportes.php` ahora llama a `articulos_ensure_schema()` al crear el PDO. Esto garantiza que las tablas `articulos_raw` existan aunque nunca se haya subido catálogo — el JOIN del cálculo de peso no falla. --- ## 4. Configuración y credenciales Cada módulo tiene su propio `config/database.php` con las **mismas constantes** (apuntan a la misma BD): ```php define('DB_HOST', 'localhost'); define('DB_NAME', 'u120688891_chess'); define('DB_USER', 'u120688891_chess'); define('DB_PASS', 't2#h*wvQ2./wZaS'); define('DB_CHARSET', 'utf8mb4'); ``` Sólo `venta/config/database.php` agrega además: ```php define('CHESS_API_URL', 'https://zonasaridas.chesserp.com/AR1185/web/api/chess/v1'); define('CHESS_API_USER', 'api_zonas'); define('CHESS_API_PASS', 'z0n4saridas'); ``` --- ## 5. Landing — `reportes/index.php` Portal con 3 tarjetas (Venta / Caja / Artículos) más 4 stats globales: - Períodos cargados (suma venta + caja) - Ventas últimos 12 meses - Gastos de caja del último mes - **SKUs en catálogo** (nuevo) - Última actualización (max entre los 3 módulos) ### Queries clave ```sql -- Stats Venta (12 últimos períodos completados): SELECT s.mes, s.anio, s.fecha_fin, COALESCE(SUM(v.subtotalNeto),0) AS ventas_netas, CASE WHEN SUM(v.subtotalNeto)>0 THEN (SUM(v.subtotalNeto) - SUM(v.preciocomprant*v.cantidadesTotal)) / SUM(v.subtotalNeto) ELSE 0 END AS margen_general FROM sincronizaciones s LEFT JOIN ventas v ON v.sync_id=s.id AND v.anulado='NO' WHERE s.estado='completado' GROUP BY s.id ORDER BY s.anio DESC, s.mes DESC LIMIT 12; -- Stats Caja: SELECT mes, anio, total_lineas, creado_at FROM caja_sincronizaciones WHERE estado='completado' ORDER BY anio DESC, mes DESC LIMIT 12; -- Stats Catálogo (existe solo si tabla creada): SELECT COUNT(*) AS total, SUM(CASE WHEN anulado='SI' THEN 1 ELSE 0 END) AS anulados, SUM(CASE WHEN peso>0 THEN 1 ELSE 0 END) AS con_peso, MAX(updated_at) AS ultima FROM articulos_raw; ``` ### Function `fmtM(float $v): string` Formatter de dinero del landing. Devuelve `$ 18.823.974` (sin abreviaturas) o `($ 3.500.000)` para negativos. ### Function `mesNom(int $m, int $y): string` Devuelve el mes en español: `Enero 2026`. --- ## 6. Módulo Venta **Ver documentación detallada en `reportes/venta/DOCUMENTACION.md`.** Resumen rápido: ### Páginas | Archivo | Función | |---|---| | `index.php` | Listado de períodos sincronizados | | `dashboard.php` | Dashboard principal con KPIs, gráficos y 6 tabs de Rentabilidad | | `sync_api.php` | UI de sincronización de un mes | | `sync_api_process.php` | Backend del sync mensual (AJAX lote × lote) | | `reinit_import.php` | Importación masiva con opción `?resume=1` | | `monitor.php` | Monitor del estado de importación en vivo | | `api/filter.php` | Endpoint para drill-downs (cross-filter) | ### Núcleo: `includes/data_api.php` Define la **fórmula ChessERP** como constantes reutilizables: ```php const SUM_VENTAS_REALES = "SUM(CASE WHEN dsArticulo IS NOT NULL AND dsArticulo != '' THEN subtotalNeto ELSE 0 END)"; const SUM_COSTO_NETO = "SUM(preciocomprant * cantidadesTotal)"; const EXPR_CONTRIBUCION = "(SUM_VENTAS_REALES - SUM_COSTO_NETO)"; const EXPR_MARGEN_PCT = "CASE WHEN SUM_VENTAS_REALES > 0 THEN EXPR_CONTRIBUCION / SUM_VENTAS_REALES ELSE 0 END"; ``` Y las usa en todas las funciones de margen para que coincidan exactamente con ChessERP. ### Tabs nuevos en "💹 Rentabilidad Desagregada" (6 pestañas Tabulator) `Por Vendedor · Por Supervisor · Por Depósito · Por Cliente · Por Artículo · Por Proveedor` Cada tab: - Tabulator con búsqueda, sort, fila TOTAL al pie (con margen y ticket promedio ponderados) - Botón pantalla completa por tab - 1000+ filas con virtual scroll **Columnas unificadas en los 6 tabs** (orden de izquierda a derecha): `Clave · Info auxiliar · Bultos · Toneladas · Comprobantes · Ticket Prom. · Ventas · Costo · Contribución · % Margen · % Participación · [Clientes si aplica]` **Highlight Top 10 (verde) / Bottom 5 (rojo)** se aplica automáticamente cuando la tabla tiene **> 15 filas** (Cliente ~1000, Artículo ~1200, Proveedor ~70, Vendedor ~30-40). Tablas cortas (Supervisor, Depósito) no llevan highlight. El highlight se calcula una sola vez por ventas descendente y queda fijo: si el usuario reordena la tabla por otra columna, los rows top/bottom **mantienen su color**. ### Coherencia de totales entre tabs/tablas Para que los **6 tabs de Rentabilidad + las 2 tablas de Resumen Cruzado** muestren el mismo TOTAL, las queries que agrupan por campos que pueden ser NULL (proveedor, articulo, supervisor, depósito, cliente) usan **`COALESCE` en GROUP BY** en lugar de filtrar con `IS NOT NULL`: ```sql SELECT COALESCE(NULLIF(TRIM(),''), '(SIN )') AS , SUM(subtotalNeto) AS ventas, ... FROM ventas WHERE sync_id=? AND anulado='NO' -- ★ sin filtro IS NOT NULL GROUP BY ``` Las líneas sin valor (NDCONs por cheque rechazado, intereses, etc.) se agrupan bajo etiquetas `(SIN ARTÍCULO)` / `(SIN PROVEEDOR)` / etc. — preserva el TOTAL coherente y a la vez deja visible cuánto pesan los movimientos financieros. Ver detalle en `venta/DOCUMENTACION.md` §13. --- ## 7. Módulo Caja **Ver documentación detallada en `reportes/caja/DOCUMENTACION.md`.** Resumen rápido: ### Tecnología - Backend PHP 8 + PDO (MariaDB) - Frontend JS vanilla + **Tabulator 6.3** (tabla árbol RUBRO ▸ CONCEPTO) + Chart.js - Carga de Excel cliente: **SheetJS** (xlsx-0.20.3, CDN) — parseo en navegador ### Pipeline de clasificación 3-tier ``` EgresosDetalle (gastos) │ ▼ NIVEL 1: GastosTipificados → rubro/concepto desglosado │ ▼ NIVEL 2: MovimientoDetalle + ConceptosDeGastos → rubro fallback │ ▼ NIVEL 3: SIN CLASIFICAR ``` ### Archivos del pipeline (5 + 1 opcional) - EgresosDetalle (movimientos) - EgresosCaja (mapa caja → cajero → provincia) - GastosTipificados (primaria de rubro) - MovimientoDetalle (fallback) - ConceptosDeGastos (catálogo — **persistente**, solo se sube cuando hay cambios) - MovimientoResumido (opcional — para IVA discriminado) --- ## 8. Módulo Artículos **Ver documentación detallada en `reportes/articulos/DOCUMENTACION.md`.** Catálogo maestro de SKUs del ERP. 7.870 artículos × 66 columnas tipadas. ### Uso principal del catálogo `venta/data_api.php` hace LEFT JOIN con `articulos_raw` para enriquecer: ```sql LEFT JOIN (SELECT codigo, peso AS cat_peso FROM articulos_raw) a ON a.codigo = CAST(idArticulo AS UNSIGNED) ``` Y luego computa el peso de cada línea con fallback: ```sql SUM(IFNULL(NULLIF(a.cat_peso, 0) * cantidadesTotal, COALESCE(pesoTotal, 0))) AS peso_kg ``` Esto significa: - **Si el artículo está en el catálogo y tiene peso > 0** → usar `cat_peso × cantidad` - **Si no** → fallback a `pesoTotal` que vino de la API en la venta --- ## 9. Fórmula ChessERP del margen (clave) ChessERP usa una **fórmula compuesta** que diferencia `Vta mostrada` de `Vta usada para margen`: ``` Vta (mostrada) = SUM(subtotalNeto) ← TODAS las líneas Compra = SUM(preciocomprant × cantidadesTotal) ← cost real (NDCONs aportan 0) Vta_reales = SUM(subtotalNeto WHERE dsArticulo IS NOT NULL) ← sólo líneas con artículo Margen = Vta_reales − Compra % Contribución = Margen / Vta_reales ``` ### Por qué es así Los **NDCONs** (cheque rechazado, intereses, recargos) son movimientos financieros sin mercadería: - **Sí entran** en Vta (porque comercialmente son ingresos al cliente) - **No entran** en el cálculo del margen (no tienen costo de mercadería; si entraran aparecerían con margen 100 % distorsionando todo) ChessERP soluciona esto **incluyéndolos en Vta pero excluyéndolos del denominador del margen**. Nuestro dashboard ahora aplica la misma lógica. ### Verificación con caso real (SAMARELLI) | | ChessERP | Nuestro | |---|---|---| | Vta | $ 6.340.570 | $ 6.340.570 ✓ | | Compra | $ 4.743.031 | $ 4.743.031 ✓ | | Margen | $ 1.597.539 | $ 1.597.539 ✓ | | Contribución | 25,20 % | 25,20 % ✓ | ### Verificación con caso complejo (SGO | PABLO MOLINA — tiene NDCONs) | | ChessERP | Nuestro | |---|---|---| | Vta | $ 16.059.265 | $ 16.059.265 ✓ | | Compra | $ 10.587.444 | $ 10.587.444 ✓ | | Vta_reales | $ 13.417.695 | $ 13.417.695 ✓ | | Margen | $ 2.830.250 | $ 2.830.250 ✓ | | Contribución | 21,09 % | 21,09 % ✓ | ### Filtros aplicados en queries de venta Todas las funciones de agregación aplican estos filtros: ```sql WHERE sync_id = ? AND anulado = 'NO' ``` Y según el caso: - `_apiKpis`, `_apiSupervisores`, `_apiMargen*` → sin filtros adicionales (alineado ChessERP) - `_apiABCData`, `_apiClientesComportamiento` → mantienen `dsArticulo IS NOT NULL` y `subtotalNeto > 0` (definiciones específicas: "cliente activo" = compró) - `_apiAnulados` → `anulado = 'SI'` - `_apiDevoluciones` → `subtotalNeto < 0` (sólo NC) --- ## 10. Sistema de temas día/noche ### Clave de localStorage Todos los módulos usan **`za-theme`**. Valores: `'dark'` (default) | `'light'`. ### Patrón anti-flash Todos los HTML del sistema incluyen este script como **primera línea del ``**: ```html ``` ### Variables CSS ```css :root { /* MODO CLARO */ --bg-page: hsla(217, 33%, 97%, 1); --bg-card: #ffffff; --text-main: hsla(220, 40%, 12%, 1); --text-muted: hsla(220, 15%, 45%, 1); --accent: hsla(217, 91%, 50%, 1); --border: hsla(217, 20%, 88%, 1); } [data-theme='dark'] { /* MODO OSCURO */ --bg-page: #0b1120; --bg-card: #141b2d; --text-main: #f8fafc; --text-muted: #94a3b8; --accent: #3b82f6; --border: #1e293b; } ``` --- ## 11. Logos y branding ``` /assets/images/logos/logo_dia.svg /assets/images/logos/logo_noche.svg ``` Implementación con 2 `` superpuestas + CSS que muestra una u otra según `data-theme`. --- ## 12. Decisiones de arquitectura ### 1. Una sola base de datos para todo Tanto venta, caja como articulos usan `u120688891_chess`. Cross-module JOINs son posibles. ### 2. AJAX lote × lote en vez de SSE Hostinger (shared hosting) mata HTTP largas a los ~4 minutos aunque PHP tenga `set_time_limit(0)`. Por eso usamos AJAX con lotes pequeños (~2-15 segundos por POST). El navegador orquesta la secuencia. ### 3. Fórmula ChessERP compuesta para margen La columna `dsArticulo IS NOT NULL` que solía filtrar a nivel WHERE ahora se aplica adentro de `SUM(CASE WHEN ...)` para que: (a) NDCONs entren en Vta, (b) NDCONs NO inflen el margen. Esto replica exactamente la lógica de ChessERP. ### 4. Tabulator en vez de React/Recharts en caja El dashboard de caja se reescribió a JS vanilla + Tabulator (Mayo 2026). Tabulator soporta nativamente `dataTree` (RUBRO ▸ CONCEPTO) y elimina ~500 KB de bundle. ### 5. Separación tablas raw vs derivada `caja_gastos` (derivada — alimenta dashboard) coexiste con `caja_raw_*` (crudas, todas las columnas del Excel). Permite reportes futuros sin volver a subir Excel. ### 6. Catálogo `ConceptosDeGastos` persistente No se vuelve a subir cada mes; se reemplaza sólo cuando hay nuevos rubros. La UI lo marca opcional y muestra fecha de última actualización. ### 7. Cross-module bootstrap del schema `venta/includes/db_reportes.php` hace `require_once articulos/api/_schema.php` y llama `articulos_ensure_schema($pdo)` al crear el PDO. Esto garantiza que las tablas existan aunque articulos nunca se haya cargado. ### 8. ON DELETE CASCADE en ventas ```sql CONSTRAINT fk_ventas_sync FOREIGN KEY(sync_id) REFERENCES sincronizaciones(id) ON DELETE CASCADE ``` Borrar un sync elimina todas sus ventas. En caja NO se usa FK CASCADE (limpieza explícita en `init.php` y `reset_db.php`). ### 9. Clave unificada de tema en localStorage `'za-theme'` en todos los módulos. ### 10. Formato unificado del sistema - Dinero: `$ 18.823.974` (con espacio, sin decimales) - Negativo: `($ 3.500.000)` - Número: `1.625` (sin $, sin decimales) - USD: `USD 17.343` - Porcentaje: **2 decimales** (`21,09 %`) Definido en: - PHP: `venta/includes/functions.php` → `formatMoney`, `formatPercent` - JS: `venta/assets/js/app.js` → `fmtMoney`, `fmtMoneyFull`, `fmtNum`, `fmtPct` - JS Tabulator: `dashboard.php` → `tabFmtMoney`, `tabFmtTotal`, `tabFmtBultos`, `tabFmtTons`, `tabFmtPct`, `tabFmtMargenBar` --- ## 13. Guía de mantenimiento ### Agregar un nuevo mes al rango de importación masiva (venta) En `venta/reinit_import.php` (bloque JS), extender el array de meses: ```js for(let m=1; m<=5; m++) MESES.push({mes:m, anio:2026}); // Ene–May 2026 ``` Actualizar también `$meses_esperados` en `monitor.php`. ### Sincronizar un mes puntual de ventas `venta/sync_api.php` → seleccionar mes/año → "Sincronizar". ### Cargar gastos de caja de un nuevo mes - Individual: `caja/sync.php` (sube 5 archivos requeridos) - Masivo: `caja/sync_bulk.php` (un archivo de varios meses) ### Cargar/actualizar catálogo de artículos `articulos/sync.php` → arrastrar `plantillaArticulosAR.xlsx`. ### Si la importación masiva de venta se congela 1. Abrir `venta/monitor.php` para ver dónde quedó. 2. `venta/reinit_import.php?resume=1` → "⟳ Retomar importación". ### Agregar una nueva provincia a caja 1. En `caja/sync.php` y `caja/sync_bulk.php`: agregar mapping en `CAJERO_PROV`. 2. En `caja/api/data.php`: agregar al `WHERE provincia IN (...)`. ### Agregar una columna nueva de la API de ventas 1. `ALTER TABLE ventas ADD COLUMN ...` 2. Agregar en `ri_insertarFilas()` (`venta/reinit_import.php`). 3. Idem en `venta/sync_api_process.php`. ### Cambiar credenciales de BD o API Editar: - `venta/config/database.php` - `caja/config/database.php` - `articulos/config/database.php` ### Resetear todo el módulo venta `venta/reinit_import.php` (sin `?resume`) → "🚀 Borrar todo e importar desde cero". ### Borrar un período de caja sin tocar el resto `caja/reset_db.php` → "Eliminar período" → tipear `CONFIRMAR`. ### Limpiar catálogo de artículos `articulos/sync.php` truncatea las 5 tablas en cada carga (snapshot completo). ### Por qué un cliente aparece duplicado en el dashboard Si ves clientes con nombres muy similares (ej. `SAMARELLI` y `N. SAMARELLI`) es porque están duplicados en el ERP (cargados con CUITs distintos o variantes de nombre). Hay que unificarlos en ChessERP, no en el dashboard. --- *Documentación actualizada en Mayo 2026 — Sistema en producción sobre Hostinger PHP 8 + MariaDB. Alineado al milímetro con ChessERP.*