Systema v1.2 — Panel Técnico Oficial
Mapa exhaustivo de toda la arquitectura: módulos, endpoints, flujos de datos, conexiones, pipeline PDF/Email, dashboard, status, base de datos, licencias y Update Engine.
Identificación del Sistema
| Nombre | Systema / Cloude |
| Versión auditada | 1.2 |
| Tipo | ERP ultra-ligero SaaS multisite |
| Repositorio actualizaciones | rafelgil.com/systema-updates |
| Servidor de licencias | rafelgil.com/licencias |
| Fecha de release | 5 de abril de 2026 |
| Autor | Rafel Gil · [email protected] |
Stack Tecnológico
| Backend | PHP nativo 7.4+ / 8.x — sin frameworks |
| BD | MySQL / MariaDB — PDO + prepared statements |
| dompdf v2.0.4 + FPDF 1.86 + FPDI | |
| PHPMailer 3.x (incluido en core) | |
| HTML Parser | Masterminds html5-php v2.7.6 |
| Frontend | Vanilla JS + CSS variables (dark/light) |
| Seguridad | HMAC-SHA256, CSRF tokens, rate limiting |
Desglose de Peso del Core
aa4a1ed478a2d73f74df6ee1
{{marcadores}}<img> del HTML del emailactualizaciones.php · Servidor de distribución: https://rafelgil.com/systema-updatesmanifest.json remoto vía cURLGET https://rafelgil.com/systema-updates/manifest.json · timeout 10s · SSL verificadoEl manifest contiene: versión latest, array de versiones, archivo ZIP, SHA256, fecha
Guardado en sys_get_temp_dir() como archivo temporal
Si NO coincide → elimina ZIP temporal + retorna error · la instalación se cancela aquí
Lista explícita de 78 archivos — nunca incluye config/settings.php
Guarda log JSON en _backups/update_log.json
EXCEPCIÓN CRÍTICA: config/settings.php nunca se sobreescribe
Registra: archivos instalados, backup_name, errores
El Update Engine implementa doble capa de autenticación completamente separada del ERP:
- Capa 1 — Sesión ERP:
$_SESSION['systema_auth']debe existir o redirige al login del ERP - Capa 2 — Contraseña propia del módulo:
$_SESSION['update_auth']requiere contraseña específica (UPDATE_PASSWORDhardcodeada), independiente del password del ERP
define('UPDATE_PASSWORD', 'Riudoms2026');
define('UPDATE_SERVER', 'https://rafelgil.com/systema-updates');
define('UPDATE_MANIFEST', UPDATE_SERVER.'/manifest.json');
// Guard doble:
if(empty($_SESSION['systema_auth'])){ header('Location: index.php'); exit; }
if(empty($_SESSION['update_auth'])){ /* mostrar login propio */ }
El servidor de distribución mantiene dos niveles de versiones:
rafelgil.com/systema-updates/
├── manifest.json ← versión latest
├── systema_core_vX.X.X.zip
└── old/
├── manifest.json ← versiones antiguas
└── systema_core_vX.X.X.zip
systema/assets/_backups/ ├── backup_v0.5.5_20260318_103045.zip ├── backup_v0.5.4_20260101_120000.zip └── update_log.json ← audit trail
El rollback usa action=rollback&backup={nombre}: extrae el ZIP de assets/_backups/ y restaura los archivos. config/settings.php nunca se toca en ninguna dirección.
{
"latest": "0.5.6",
"updated": "2026-03-18",
"versions": [{
"version": "0.5.6",
"file": "systema_core_v0.6.0.zip",
"date": "2026-03-18",
"sha256": "212c02408ba2d503e5e387b3f8948f086b99...",
"size_bytes": 332287,
"files": 78,
"desc": "Core Systema v0.6.0"
}]
}
Las versiones antiguas se cargan de /old/manifest.json y se les inyecta el prefijo old/ en la URL. Ambos manifests se fusionan en el cliente para mostrar el historial completo de versiones instalables.
El Sector I del panel status.php genera el paquete listo para subir al servidor de actualizaciones:
- Generación asíncrona en background con
ignore_user_abort(true)— no bloquea el servidor - Polling cliente cada 3 segundos hasta completarse
- Lista explícita de 78 archivos — sin
glob()ni iteradores filesystem - Calcula SHA256 automáticamente y lo escribe en
assets/core_exports/manifest.json - ZIP guardado en
assets/core_exports/para descarga directa HTTP estático
if($name === 'config/settings.php') continue;. Las credenciales del cliente están siempre protegidas.Estados de Licencia
| Estado | Descripción | Efecto en cliente |
|---|---|---|
| activa | Licencia operativa | Acceso completo al ERP |
| pausada | Suspendida manualmente desde panel | Falla tras 7 días de caché local |
| pendiente | Sin token inyectado aún | Bloqueo inmediato en login |
| expirada | fecha_fin superada | Auto-update BD central · bloqueo |
Campos en config/settings.php
// Campos de licencia 'licencia_secret' => 'Sy5t3m4_L1c_S3cr3t0_R4f3lG1l...', 'licencia_token' => '', // inyectado por activate.php 'licencia_domain' => '', // rellenado automáticamente 'licencia_cache' => 0, // timestamp Unix última OK // TTL caché: 604800s = 7 días
systemafacil.comrafelgil.com, localhost, 127.0.0.1 bypasean toda validación — desarrollo local sin licencia.El panel central inyecta el token directamente en el servidor del cliente vía HTTP. activate.php ejecuta 5 checks en cadena antes de escribir en settings.php:
Login Landing — Routing por email
La carpeta LOGIN LANDING/ contiene dos archivos para el routing de clientes desde systemafacil.com:
login.php— Pantalla de acceso con email (mismo diseño visual que el login de instalación). El cliente introduce su email y es redirigido automáticamente a su instalación.lookup.php— Endpoint POST que consultalicencias.cliente_emailen la BD central. Devuelve{"found": true, "url": "...", "nombre": "..."}o{"found": false}. Incluye delay de 400ms en respuesta negativa para prevenir timing attacks.
Este sistema no autentica al usuario — solo hace routing. La autenticación ocurre en la instalación destino.
layout.php
- Sidebar de navegación global
- Aplica
ui_zoomdesde settings.php en<head>sin FOUC - Dark/Light mode toggle nativo CSS variables
- Modal de novedades (
SYSTEMA_VERSION !== whats_new_seen) - Carga
assets/core.css+assets/core.js - Apps dinámicas (v1.2): escanea
/apps/automáticamente — si existe subcarpeta conindex.php, aparece grupo "Apps" en sidebar sin tocar el core
auth.php / Guards
$_SESSION['systema_auth']— guard ERP global$_SESSION['update_auth']— guard Update Engine$_SESSION['status_auth']— guard Status.phpauthCheck()en cada módulo REST- HTTP 401 si no autenticado en módulos AJAX
config/version.php
define('SYSTEMA_VERSION', '1.2')$WHATS_NEW— array de novedades por grupo- Dispara modal en primer login post-actualización
mark_seen.phpguarda versión en settings.php- Usado por actualizaciones.php para comparar versiones
Recuperación de contraseña
El archivo reset_password.php gestiona la recuperación de acceso. Flujo completo:
- El usuario hace clic en “He olvidado mi contraseña” en la pantalla de login
reset_password.phpbusca el email enlicencias.cliente_emailen la BD central de licencias- Genera una contraseña temporal segura de 8 caracteres con
random_int() - La hashea con
password_hash()y la guarda enconfig/settings.php - Envía la contraseña temporal al email del cliente con
mail() - Respuesta genérica — no revela si el email existe o no en el sistema
dashboard.php es la página principal post-login. Carga el layout y delega todas las queries al kpi_engine.php vía fetch AJAX. Google Charts renderiza los gráficos vía CDN.
kpi_engine.php — Motor AJAX
Datos cargados en dashboard.php (PHP inicial)
| Query | Propósito |
|---|---|
SELECT DISTINCT YEAR(fecha_emision) | Años disponibles para selector global |
SELECT f.id,f.numero,f.total,f.estado,c.nombre FROM facturas JOIN clientes LIMIT 6 | Últimas 6 facturas recientes (tabla rápida) |
Vista v_dashboard_facturas | Agregados pre-calculados por estado |
Vista v_dashboard_presupuestos | Agregados de presupuestos activos |
Cache-Control: no-store — nunca se cachea. Las dos vistas SQL (v_dashboard_facturas, v_dashboard_presupuestos) optimizan las consultas agregadas más costosas.id concreto en BD — imposible ver facturas ajenas cambiando ?id= · sin sesión requerida (v1.2 parche RGPD)Marcadores de plantilla soportados
{{numero}} | Número de factura |
{{fecha_emision}} | Fecha de emisión formateada |
{{cliente_nombre}} | Nombre del cliente |
{{emisor_nif}} | NIF del emisor desde fiscal |
{{base_imponible}} | Base imponible en € |
{{iva_porcentaje}} | % IVA aplicado |
{{irpf_fila}} | Fila IRPF (oculta si 0%) |
{{lineas_tabla}} | HTML tabla de líneas |
{{qr_code}} | QR VeriFactu base64 (img) |
{{total}} | Total factura en € |
Pipeline de Email con Tracking
<img src="track_open.php?t={id}" width="1" height="1">email_abierto=1, con timestamp de apertura.Dos motores PDF según caso de uso
Motor 1 — dompdf v2 (facturas usuario)
- Renderiza plantilla HTML personalizada del usuario
- Masterminds html5-php v2.7.6 como parser HTML5
- QR VeriFactu generado con curl_multi en paralelo
- Bulk: hasta 100 PDFs + ZIP automático
- memory_limit 512M en generate.php
Motor 2 — FPDF + FPDI (modelos AEAT)
- Superpone datos sobre plantillas PDF oficiales de la AEAT
- M303 (IVA), M130 (IRPF), M390 (resumen anual)
- Plantillas en
presentador/templates/ - Genera también TXT con formato AEAT
- Guarda snapshot JSON en
presentaciones_historial
activo · devuelve {success, nuevo_estado}nuevo / contactado / cualificado / perdido / convertidoborrador/enviado/leído/aceptado/rechazado/caducado/convertidofacturacion/get_items.phpconvertido_factura_id · devuelve {factura_id, numero} · soporta bulk conversión masivapublic_token y email_tracking_id · incluye Reply-To del emisoremail_abierto=1, email_abierto_at en presupuestos y registra en email_tracking con documento_tipo=presupuestoid concreto en BD · sin sesión · plantilla con etiqueta PRESUPUESTO en lila · botón descargar PDF (v1.2 parche RGPD)facturacion/pdf_export.phpPortal público del presupuesto — pdf_render.php
Acceso sin login mediante public_token único generado en el envío por email. El portal muestra:
- Presupuesto completo con líneas, totales e IBAN del emisor
- Etiqueta visual "PRESUPUESTO" en lila diferenciada de facturas
- Botón de descarga PDF (dompdf, mismo motor que facturación)
- Chip "Leído" en la tabla del panel cuando el cliente abre el email (pixel tracking)
resultado_publico.php?token={token} · incluye Reply-To del emisor · campos SMTP: usuario, password, puerto, cifrado (fix v0.6.0)Portal público del contable — resultado_publico.php
Acceso sin login mediante token único (?token=xxx). Nuevo en v0.6.0: pantalla de autenticación con portal_password antes de mostrar el contenido. El portal muestra:
- Pantalla de acceso con contraseña — configurada en Configuración → Seguridad · incluye datos de contacto del emisor (email, teléfono, web) para recuperación si el contable pierde la contraseña
- Datos de la empresa emisora, ejercicio y badge de año
- 4 mini-cards de estadísticas: total facturas, importe, pagadas, pendientes
- Botones de descarga de
T1.zip · T2.zip · T3.zip · T4.zip(servidos desdeassets/resultados/{año}/vía HTTP estático) - Descarga Excel client-side (xlsx-js-style, datos incrustados en HTML, sin sesión requerida)
- Estado del envío (
estado=pendiente|enviado) y observaciones del emisor - Botones en fila horizontal — bloque resumen rediseñado en v0.6.0
resultados_anuales requiere 3 columnas nuevas para tracking email del contable:
ADD COLUMN email_tracking_id VARCHAR(64) NULL AFTER enviado_at
ADD COLUMN email_abierto TINYINT(1) NOT NULL DEFAULT 0 AFTER email_tracking_id
ADD COLUMN email_abierto_at DATETIME NULL AFTER email_abierto
Genera modelos fiscales AEAT superponiéndolos sobre las plantillas PDF oficiales. Guarda snapshots JSON para historial inmutable.
reply_to — todos los emails de facturación, presupuestos y resultados incluyen Reply-To del usuario emisor. Clave para instalaciones multitenant.portal_password en tabla configuracion grupo='seguridad' · protege el portal del contable con contraseña de acceso previa al contenidopassword_hash() en config/settings.php. Acepta la contraseña maestra como “contraseña actual” para que el administrador siempre pueda cambiarla.$_SESSION['systema_auth'] + STATUS_PASSWORD independiente. Diseñado exclusivamente para el administrador del sistema.Generador de Core ZIP (Sector I)
El botón "Descargar Core ZIP" en Sector I genera el ZIP + manifest.json listo para subir al servidor de actualizaciones:
- Generación asíncrona en background con polling cliente cada 3 segundos
- Lista explícita de 94 archivos del core — sin glob, sin iteradores filesystem
- Calcula SHA256 del ZIP automáticamente y lo escribe en
assets/core_exports/manifest.json - ZIP guardado en
assets/core_exports/systema_core_v{version}.zippara descarga directa HTTP - El ZIP anterior se guarda en
assets/core_exports/old/
Archivos nuevos en v1.0 (respecto al manifest v0.6.0)
| Archivo | Grupo | Descripción |
|---|---|---|
reset_password.php | Raíz | Recuperación de contraseña vía BD de Licencias central. Genera pwd temporal, la hashea en settings.php y la envía por mail(). Posición alfabética: tras logout.php. |
modules/configuracion/save_acceso.php | modules/configuracion/ | Guarda contraseña de acceso del cliente hasheada con password_hash() en config/settings.php. Acepta la contraseña maestra como “contraseña actual”. |
modules/configuracion/delete_marca.php | modules/configuracion/ | Elimina una marca. Verifica restricción RESTRICT antes de borrar (no elimina si tiene documentos vinculados). |
| Tabla | Filas clave | Uso en sistema | Módulo principal |
|---|---|---|---|
| facturas | numero, cliente_id, estado, public_token, email_tracking_id, verifactu, email_abierto | Cabeceras de facturas emitidas + tracking email + VeriFactu | facturacion.php |
| factura_items | factura_id, descripcion, cantidad, precio, iva_pct, irpf_pct | Líneas de facturas. DELETE + INSERT en cada UPDATE | facturacion/update |
| clientes | nombre, nif_cif, email, email_facturacion, direccion, cp, activo | Base de clientes · email_facturacion con prioridad en envíos · campo activo para toggle activo/inactivo v0.6.0 | clientes.php |
| presupuestos | numero, cliente_id, estado, convertido_factura_id, public_token, email_tracking_id, email_abierto | Presupuestos completos · conversión a factura · portal público por token · tracking email | presupuestos.php |
| presupuesto_items | presupuesto_id, descripcion, cantidad, precio | Líneas de presupuestos. Misma estructura que factura_items | presupuestos/create |
| leads | nombre, empresa, email, estado, valor_estimado, cliente_id | Pipeline CRM · estado: nuevo/contactado/cualificado/perdido/convertido · bulk convert en v0.6.0 | leads.php |
| marcas | nombre, prefijo, color_hex, activa | Marcas emisoras para series de numeración múltiples | configuracion.php |
| series_numeracion | marca_id, ejercicio, ultimo_numero | Contador de números por marca y año | facturacion/create |
| configuracion | grupo (fiscal/smtp/plantilla/verifactu/ia/seguridad), clave, valor | KV Store global · INSERT ON DUPLICATE KEY UPDATE · grupo seguridad añadido en v0.6.0 (portal_password, reply_to) | configuracion.php |
| email_tracking | tracking_id, documento_tipo, documento_id, abierto_at, ip_apertura, user_agent | Log de aperturas de email via pixel 1×1 · documento_tipo: factura o presupuesto (v0.6.0) | track_open.php |
| presentaciones_historial | modelo, ejercicio, periodo, snapshot_json, created_at | Historial inmutable de modelos AEAT presentados | presentador/historial |
| resultados_anuales v0.6.0 | ejercicio, token, zip_status, zip_expires_at, email_contable, estado, email_tracking_id, email_abierto, email_abierto_at | Cierres fiscales anuales · portal contable con password · ZIPs trimestrales · tracking email contable | resultados.php |
| producto_categorias v1.0 — módulo a medida | id, nombre, margen_pct, activa, created_at | Categorías del catálogo de productos con margen de beneficio por defecto. Tabla vacía — cada instalación configura las suyas. | módulo productos (pendiente UI) |
| productos v1.0 — módulo a medida | id, sku, descripcion, unidad, coste_neto, categoria_id, proveedor, activo | Catálogo maestro de productos/materiales. Búsqueda por SKU o descripción al crear líneas en presupuestos y facturas. PVP calculado automáticamente: coste_neto × (1 + margen_pct/100) | módulo productos (pendiente UI) |
1. resultados_anuales — 3 columnas de tracking email añadidas + charset utf8mb4 + 3 índices restaurados.
2. configuracion — 7 claves SMTP obsoletas eliminadas (ids 13-19: smtp_host, smtp_puerto, smtp_usuario, smtp_password, smtp_encriptacion, smtp_from_nombre, smtp_from_email).
3. producto_categorias + productos — tablas del módulo productos creadas vacías y listas.
Vistas SQL optimizadas
v_dashboard_facturas
Pre-calcula agregados de facturas por estado (pagadas/pendientes/vencidas) y totales €. Optimiza las queries más costosas del KPI engine.
v_dashboard_presupuestos
Agrega presupuestos activos por estado. Usada en el KPI widget de presupuestos del dashboard.
VeriFactu Fase A — Activo en v0.5
| QR | {{qr_code}} → base64 via curl_multi |
| Config | save_verifactu.php → qr_activo + qr_base_url |
| BD | tabla configuracion grupo='verifactu' |
| QR incrustado en dompdf vía img base64 | |
| Fase B | Envío XML AEAT — pendiente normativa v0.7 |
ai_webhook.php — Endpoint IA (Fase 2)
POST ai_webhook.php
Headers:
Content-Type: application/json
X-Systema-Key: {api_key}
Body:
{"instruction": "Crea una factura
para cliente X por diseño 1500€"}
Planificado para v0.8 con Groq. El endpoint ya existe y acepta OPTIONS para CORS. Parsea lenguaje natural → crea factura vía modules/facturacion/create.php.
Sin framework PHP
PHP nativo con PDO. Sin Laravel, Symfony ni dependencias. Ahorro: ~15-50MB de vendor comparado con un ERP Laravel típico.
Vendor separado del core
dompdf (~6MB), Masterminds (~1MB) y FPDF están en /vendor/, excluida del core ZIP. El core solo incluye PHPMailer (3 archivos).
CSS variables nativo
Un solo core.css gestiona dark/light mode y zoom UI. Sin Tailwind ni Bootstrap en producción. Ahorro: ~300KB.
Endpoints como archivos PHP
Cada operación es un archivo independiente sin clases ni herencia. Sin overhead de instanciación ni autoloading complejo.
config/settings.php como array
return array() escrito con var_export(). OPcache lo compila directamente. 0ms de parsing YAML/XML/JSON.
Generación asíncrona
ZIPs fiscales y Core ZIP generan en background con ignore_user_abort(true). Sin timeouts PHP, sin bloqueos de servidor.
| Componente | Peso | Incluido en core ZIP |
|---|---|---|
| Core ZIP v0.1.0 | 364 KB | Sí — 94 archivos |
| vendor/dompdf | ~5.5 MB | No — instalar aparte |
| vendor/masterminds | ~1.2 MB | No — instalar aparte |
| vendor/fpdf + FPDI | ~800 KB | No — en vendor/ |
| PHPMailer (3 archivos) | ~180 KB | Sí — en core ZIP |
| presentador/templates (3 PDFs AEAT) | ~2 MB | No — en presentador/ |
Comparativa: ERP clásico vs Systema
| Aspecto | ERP Típico (Laravel/Symfony) | Systema v1.0 |
|---|---|---|
| Core distribuible | 5–50 MB | 364 KB |
| Tiempo de despliegue | 15–60 min (Composer, migraciones, .env) | FTP + importar SQL + config web |
| Actualización | Manual, CI/CD o Composer update | 1 click · backup automático · rollback |
| Dependencias runtime | Composer + 100+ paquetes | PHP 7.4+ + MySQL + cURL |
| Configuración | .env + config YAML/JSON + migrations | config/settings.php — array PHP, OPcache nativo |
| Librería incluida (aumenta tamaño core) | dompdf externo · FPDF 1.86 incluido | |
| Auth multitenant | JWT / OAuth / Passport / Sanctum | HMAC-SHA256 propio · caché 7d · fallback |
| Actualizaciones automáticas | Requiere CI/CD o acceso SSH | Update Engine integrado · SHA256 · rollback |
Funcionalidades completadas
Próximas versiones
facturacion/submit_verifactu.phpai_webhook.php como base · módulo modules/ia/chat.phpai_webhook.php— receptor de eventos IA ya presente en core- Tabla
configuracion(KV store) → claveiadocumentada getDB()centralizado — acceso universal a todos los datos- Módulos REST independientes — añadible sin modificar el core
- Design system CSS con variables — UI chatbot integrable en
layout.php
/modules/ sin modificar el núcleo. El Update Engine distribuirá estas versiones automáticamente a todos los clientes.Historial completo de versiones
| Versión | Fecha | Features principales | Estado |
|---|---|---|---|
| v0.1 | 2025 | Core ERP: facturación, clientes, leads, configuración, PDF básico | Alpha |
| v0.4 | 2025 | 20 índices BD de rendimiento, presupuestos, marcas/series numeración | Alpha |
| v0.5 | 2025 | VeriFactu Fase A — QR en facturas, dompdf v2, plantilla personalizable | Alpha |
| v0.5.1 | 2025 | Licencias multisite HMAC-SHA256, activate.php, caché 7 días | Alpha |
| v0.5.2 | 2025 | Modal novedades, importador Excel/SQL, fix marcas prefijo | Alpha |
| v0.5.3 | 2025 | Dashboard filtros inteligentes año/trimestre | Alpha |
| v0.5.4 | 2025 | Facturación filtros avanzados, export Excel, bulk PDF ZIP | Alpha |
| v0.5.5 | Mar 2026 | Módulo Resultados Anuales, Portal contable token, Zoom UI, Core ZIP v1 | Alpha |
| v0.5.6 | 19 Mar 2026 | Update Engine v0.6, SHA256, backup automático, rollback, Core ZIP v2 | Alpha |
| v0.6.0 | 19 Mar 2026 | Design System unificado · Presupuestos completo (11 endpoints, PDF/email/token/bulk) · Leads rediseño · Clientes activo/inactivo · Portal contable con password · SMTP Reply-To · Sector J Core Manifest · fix toast + localStorage | Beta — Actual |
| v0.7 | Pendiente | VeriFactu Fase B — Envío XML AEAT | Pendiente normativa |
| v0.8 | Fase 2 | IA / Groq — facturas por lenguaje natural | Planificado |
Funcionalidades diseñadas y con estructura de BD lista en producción, pendientes de implementación de UI. Disponibles como módulos a medida por cliente — la base está hecha, solo falta construir la interfaz.
📦 Módulo Productos — Catálogo Maestro
¿Qué es?
Catálogo de productos y materiales por instalación. Permite al usuario buscar un artículo por SKU o descripción al añadir líneas en Presupuestos y Facturas. El sistema calcula automáticamente el PVP aplicando el margen de la categoría.
Pensado para autónomos de oficios: un electricista tendrá "Cables", "Mecanismos", "Tubos"; un fontanero tendrá "Tubería", "Griferías", "Accesorios". Cada instalación configura sus propias categorías y productos.
producto_categorias y productos están vacías y listas. Solo falta implementar la UI y los endpoints.Lógica financiera
Todos los precios se almacenan sin IVA. El IVA se aplica solo al cerrar el documento.
coste_neto del producto (ej: 10,00 €)margen_pct de su categoría (ej: Cerrajería = 20%)10,00 × (1 + 20/100) = 12,00 €12,00 € en el campo Precio de la línea12,00 + 21% IVA = 14,52 € totalEl precio inyectado en presupuesto_items.precio_unitario queda grabado en el documento. Si mañana cambias el margen de una categoría, los documentos anteriores no se ven afectados.
Estructura de tablas (ya en producción)
producto_categorias
| Campo | Tipo | Descripción |
|---|---|---|
id | INT UNSIGNED PK | Autoincremental |
nombre | VARCHAR(100) | Ej: Cerrajería, Fontanería, Mano de obra |
margen_pct | DECIMAL(5,2) | Margen de beneficio en %. Ej: 20.00 = 20% |
activa | TINYINT(1) | Soft delete — desactivar sin borrar |
created_at | TIMESTAMP | Fecha de creación |
productos
| Campo | Tipo | Descripción |
|---|---|---|
id | INT UNSIGNED PK | Autoincremental |
sku | VARCHAR(50) UNIQUE | Código proveedor. Ej: Saltoki 1234 |
descripcion | VARCHAR(500) | Ej: Bombín T60 30x30 Latonado |
unidad | VARCHAR(20) | ud, m, m², h, kg, l... |
coste_neto | DECIMAL(12,4) | Precio de coste sin IVA |
categoria_id | INT UNSIGNED FK | → producto_categorias.id |
proveedor | VARCHAR(100) | Saltoki, Propio, etc. |
activo | TINYINT(1) | Soft delete — desactivar sin borrar |
created_at | TIMESTAMP | Fecha de creación |
updated_at | TIMESTAMP | Última modificación (auto) |
Índices y relaciones
UNIQUE KEY uq_sku (sku)— no puede haber dos productos con el mismo códigoKEY idx_descripcion (descripcion(100))— búsqueda rápida por texto parcialKEY idx_categoria (categoria_id)— filtrado por categoríaKEY idx_activo (activo)— filtrado de productos activos en búsquedaFOREIGN KEY (categoria_id) → producto_categorias(id) ON DELETE RESTRICT— no se puede borrar una categoría si tiene productos
Implementación UI pendiente — qué hay que construir
Backend — 1 endpoint
SELECT p.*, c.margen_pct, ROUND(p.coste_neto*(1+c.margen_pct/100),2) AS pvp FROM productos p JOIN producto_categorias c ON p.categoria_id=c.id WHERE p.activo=1 AND (p.sku LIKE :q OR p.descripcion LIKE :q) LIMIT :limit
Frontend — ~30 líneas JS
Al escribir en el campo descripción de una línea de presupuesto o factura:
- Trigger a partir de 2 caracteres
- Llamada al endpoint con debounce de 200ms
- Dropdown con resultados: SKU · descripción · precio
- Al seleccionar: inyecta descripción, unidad y PVP en los campos de la línea
- El usuario puede sobreescribir el precio manualmente
<datalist> nativo — demasiado limitado para mostrar SKU + descripción + precio. Se implementa con un <div> dropdown custom ligado al input.Restricciones técnicas de diseño
- Sin importador de Excel en esta fase — solo estructura de tablas y búsqueda. El importador masivo (Saltoki CSV) se planifica para una versión posterior.
- Fiscalidad — todos los cálculos en BD son sin IVA. El IVA solo se aplica al total del documento en el momento de cierre.
- Peso — el endpoint y el JS deben mantener el ERP por debajo de 300 KB en el core ZIP.
- No destructivo — no modifica ninguna tabla existente. Solo añade dos tablas nuevas y un endpoint.
- Sin datos por defecto — las tablas se entregan vacías. Cada cliente configura sus categorías y productos según su oficio.