From 11d8d7ae0bb3476fda828d7f7b8ab9df42b761b9 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 31 Mar 2026 01:03:24 +0000 Subject: [PATCH] docs: add POS + Inventario complete design spec Multi-tenant POS system for Mexican auto parts stores (refaccionarias): - DB-per-tenant isolation with template versioning - PWA with offline-first via IndexedDB + Service Worker - Inventory by operations (append-only, no conflicts) - CFDI 4.0 via Horux360 API with offline queue - Accounting with auto journal entries - Role-based permissions (owner/admin/cashier/warehouse/accountant) - Themeable frontend via CSS custom properties - 22 table schemas defined, offline edge cases addressed Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-27-pos-inventario-design.md | 869 ++++++++++++++++++ 1 file changed, 869 insertions(+) create mode 100644 docs/plans/2026-03-27-pos-inventario-design.md diff --git a/docs/plans/2026-03-27-pos-inventario-design.md b/docs/plans/2026-03-27-pos-inventario-design.md new file mode 100644 index 0000000..2cf0398 --- /dev/null +++ b/docs/plans/2026-03-27-pos-inventario-design.md @@ -0,0 +1,869 @@ +# Nexus POS + Inventario — Design Spec + +**Date:** 2026-03-27 +**Status:** Approved +**Goal:** Sistema completo de Punto de Venta e Inventario para refaccionarias automotrices en Mexico, instalable como PWA con modo offline, multi-tenant con DB aislada por cliente. + +> **Nota:** Todos los montos en MXN (pesos mexicanos). Multi-moneda fuera de alcance para v1. + +--- + +## 1. Arquitectura General + +### Multi-Tenant con DB por cliente + Template versionado + +- **nexus_master DB**: tenants, subscriptions, tenant_schema_version, catalogo compartido (1.5M+ partes), marketplace (bodegas, precios, stock) +- **tenant_template DB**: schema completo vacio, usado con `CREATE DATABASE ... TEMPLATE` +- **tenant_{id} DB**: una por refaccionaria, aislamiento total de datos +- **Horux360 API**: credenciales SAT (CSD) para timbrado CFDI + +### Provisioning automatico + +1. Nueva refaccionaria se registra -> INSERT en `nexus_master.tenants` +2. `CREATE DATABASE tenant_xxx TEMPLATE tenant_template` +3. INSERT `schema_version = 'v1.0'` +4. Carga catalogo de cuentas SAT predeterminado +5. Crea empleado owner con credenciales iniciales + +### Migraciones versionadas + +- `tenant_schema_version` rastrea version por tenant +- Al actualizar schema (v1.0 -> v1.1): script recorre todos los tenants, aplica ALTER/CREATE, actualiza version +- `tenant_template` se actualiza tambien + +### Conexion offline (PWA) + +- **Service Worker**: cache-first para app, network-first para busquedas externas +- **IndexedDB**: inventario completo del cliente, clientes, operaciones pendientes, CFDIs por timbrar +- **Sync por operaciones**: nunca se sincroniza estado, solo operaciones append-only +- **Conflictos**: no existen porque las operaciones son aditivas (VENTA: -1, ENTRADA: +10), el servidor suma/resta + +#### IndexedDB Storage + +El inventario se almacena comprimido como JSON en IndexedDB. Para una refaccionaria tipica (10K-30K SKUs), esto ocupa ~5-15MB, bien dentro de los limites del navegador (~50-100MB tipico). Si un tenant tiene >50K SKUs, el sync se pagina en bloques de 10K registros. + +#### Politicas offline (edge cases) + +- **Stock negativo**: Se PERMITE vender con stock negativo mientras se esta offline. Al sincronizar, las ventas se aplican normalmente y el stock negativo se marca para revision. Se genera alerta automatica al dueno. +- **Cambios de precio mientras offline**: La sucursal offline sigue vendiendo a los precios cacheados en IndexedDB. Al sincronizar, los registros de VENTA conservan el precio al momento de la venta (sin cambios retroactivos). Los nuevos precios aplican solo a ventas futuras. +- **Limite de credito excedido**: Se PERMITE autorizar ventas a credito que excedan el limite mientras se esta offline. Al sincronizar, se genera alerta al dueno indicando el excedente. El dueno decide si cobrar inmediatamente o ajustar el limite. + +### Cola de timbrado CFDI offline + +1. Venta en POS -> genera factura con folio provisional (PRE-XXXXX) +2. Cliente recibe ticket impreso inmediato +3. Factura entra a `cfdi_queue` con status `pending` +4. Sync engine (cuando hay internet) -> envia XML a Horux API -> Horux firma con CSD y timbra +5. Retorna UUID fiscal + XML timbrado -> `cfdi_queue.status = 'stamped'` +6. Envio CFDI por email al cliente +7. Si falla: retry con backoff exponencial (5s, 30s, 2m, 10m, 1h) + +--- + +## 2. Modulo POS (Punto de Venta) + +### Flujo dual + +- **Mostrador rapido**: cliente pide pieza, se vende y se va +- **Cotizacion -> Venta**: se cotiza primero, cliente regresa a comprar + +### Funcionalidades + +**Venta:** +- Busqueda por numero de parte, nombre, o codigo de barras (lector USB) +- Agregar desde catalogo (carrito -> POS) +- Cantidades editables inline +- Descuento por item o por venta total (requiere permiso, maximo configurable por empleado) +- Margen en tiempo real visible para vendedores con permiso (costo, margen %, alerta si margen bajo) +- Descuento maximo sin perder margen calculado automaticamente + +**Clientes:** +- Venta a publico general (sin RFC) +- Seleccionar cliente registrado (autocompletado) +- Crear cliente nuevo inline sin salir del POS +- Credito: ver limite, saldo disponible. Validacion antes de autorizar venta a credito + +**Formas de cobro:** +- Efectivo (calculo de cambio) +- Transferencia (campo de referencia) +- Tarjeta (referencia de terminal bancaria) +- Pago mixto (ej: $1,000 efectivo + $1,115.84 transferencia) + +**Acciones:** +- **Cobrar** -> cierra venta, genera operaciones de inventario, ticket +- **Cotizacion** -> guarda sin cobrar, imprimible +- **Apartado** -> reserva partes, pago parcial +- **Credito** -> venta a cuenta del cliente (valida disponible) + +**Ticket/impresion:** +- Ticket termico (58mm/80mm) con datos fiscales +- PDF descargable +- Opcion de facturar (encola en cfdi_queue) + +### Modo Mostrador Rapido (F-keys) + +``` +F1 -> Buscar parte F5 -> Ultima venta +F2 -> Buscar cliente F6 -> Abrir cajon +F3 -> Cobrar +/- -> Cantidad +F4 -> Cotizacion * -> Descuento + Enter -> Agregar al carrito +``` + +100% operacion por teclado sin mouse. + +### Caja registradora + +- **Apertura**: empleado inicia turno, registra fondo inicial +- **Durante el dia**: ventas automaticas, entradas/salidas manuales con motivo +- **Corte parcial (X)**: consulta sin cerrar +- **Corte final (Z)**: cierre con desglose (efectivo, transferencias, tarjeta, credito), diferencia esperado vs contado +- **Multi-caja**: cada caja con sesion y corte independiente, corte consolidado del dia + +### Historial del vehiculo como herramienta de venta + +- Al seleccionar cliente habitual, muestra su vehiculo y ultima visita +- "Ultima vez le vendiste filtro de aceite hace 3 meses" (dato informativo) + +--- + +## 3. Modulo de Catalogo Local + +### Concepto + +El catalogo es el **inventario del cliente organizado para venta rapida**. No es un cache del catalogo Nexus. + +> **Nota sobre naming:** El modulo POS usa su propia convencion de nombres (id, name, etc.) independiente del schema del catalogo Nexus (id_brand, name_brand). El campo `catalog_part_id` en la tabla `inventory` es un entero almacenado en la DB del tenant que se resuelve via llamada API al nexus_master. No es un foreign key a nivel de base de datos (cross-DB FK). + +### Offline (siempre disponible) + +- Todo su inventario navegable por: + - Categoria (Frenos, Motor, Suspension, Electrico...) + - Marca de vehiculo (Nissan, Toyota, VW...) + - Fabricante (Bosch, Monroe, NGK...) + - Busqueda por numero de parte o nombre +- Stock en tiempo real (propio) +- Precios de venta configurados +- **Carrito**: agregar multiples partes, ver total, "Ir a cobrar" -> POS recibe carrito completo + +### Online (features adicionales) + +- "Ver disponibilidad externa" (solo lectura): + - Stock y precios en bodegas registradas + - Stock en otras refaccionarias de la red Nexus + - Alternativas aftermarket del catalogo TecDoc (1.5M+ partes) +- Cross-references OEM <-> Aftermarket + +### Flujos + +``` +Venta rapida: Catalogo -> Carrito -> "Ir a cobrar" -> POS con carrito pre-cargado -> Cobrar +Parte faltante: Catalogo -> "No tengo" -> Buscar bodegas (solo lectura) -> Ofrecer alternativa +``` + +--- + +## 4. Modulo de Inventario + +### Modelo de datos + +**inventory**: id, branch_id, part_number, barcode, name, description, category_id, brand, vehicle_compatibility (JSON), unit, cost, price_1 (mostrador), price_2 (taller), price_3 (mayoreo), tax_rate, min_stock, max_stock, location, image_url, is_active, catalog_part_id (entero opcional, referencia a catalogo Nexus via API) + +**inventory_operations** (append-only, nunca se edita): id, inventory_id, branch_id, operation_type (SALE, PURCHASE, RETURN, ADJUST, TRANSFER, INITIAL), quantity (+/-), reference_id, reference_type, cost_at_time, employee_id, device_id, notes, created_at + +**Stock actual** = `SUM(inventory_operations.quantity)` por inventory_id + branch_id + +### Operaciones + +- **PURCHASE**: entrada de mercancia (proveedor, cantidad, costo, # factura opcional). Actualiza costo promedio +- **SALE**: automatico al cobrar en POS (negativo) +- **RETURN**: devolucion de cliente -> reingresa al stock +- **ADJUST**: ajuste manual (merma, robo, error). Motivo obligatorio + audit_log +- **TRANSFER**: mover entre sucursales (operacion negativa en origen, positiva en destino) +- **INITIAL**: carga inicial de inventario + +### Toma fisica + +1. Iniciar toma (puede ser parcial por categoria/zona) +2. Empleado captura cantidades reales +3. Sistema compara esperado vs contado +4. Admin aprueba diferencias +5. Genera operaciones ADJUST automaticas + +### 3 listas de precios + +- Precio 1 -- Mostrador (publico general) +- Precio 2 -- Taller (clientes frecuentes) +- Precio 3 -- Mayoreo (volumen) +- Al seleccionar cliente en POS, aplica automaticamente segun su categoria. Editable con permiso. + +### Codigos de barras + +- Leer codigos existentes con lector USB +- Generar codigos internos: formato `NX-{tenant_short}-{secuencial}` +- Imprimible en etiquetas (compatible con impresoras de etiquetas) + +### Alertas + +- Stock en cero (agotado) +- Stock bajo minimo (reabastecer) +- Stock sobre maximo (sobreinventario) + +### Reportes de inventario + +- Inventario valorizado (stock x costo) +- Rotacion ABC (80/20) +- Productos sin movimiento (>60 dias) +- Stock bajo minimo +- Historial de movimientos por parte +- Comparativo entre sucursales + +--- + +## 5. Modulo de Facturacion (CFDI 4.0 via Horux) + +### Version y campos obligatorios + +- **Version CFDI**: 4.0 (explicito en XML) +- **Exportacion**: "01" (no aplica, venta domestica) +- **ObjetoImp**: "02" (si, objeto de impuesto) para conceptos gravados +- **InformacionGlobal**: requerido para facturas a publico en general (RFC generico XAXX010101000) +- **IEPS**: manejo diferido a v1.1 (no aplica para mayoria de autopartes) + +### Tipos de comprobante + +- **Ingreso**: venta normal (factura) +- **Egreso**: nota de credito (devolucion, descuento posterior) +- **Pago**: complemento de pago (ventas a credito pagadas despues) + +### Complemento de pago (PPD) + +Cuando una venta a credito (metodo de pago PPD) recibe un pago parcial o total a traves del modulo de pagos/cobranza: +1. Sistema auto-genera complemento de pago CFDI tipo "Pago" +2. Se encola en `cfdi_queue` con type='pago' +3. El sync engine envia a Horux para timbrado +4. Se relaciona con el CFDI de ingreso original via UUID + +### Dos momentos para facturar + +1. **Al vender**: checkbox "Facturar" en POS, genera automaticamente +2. **Despues**: desde historial de ventas, buscar por folio, generar CFDI (limite: mismo mes fiscal) + +### Datos del CFDI + +- Del tenant (config): RFC emisor, razon social, regimen fiscal, CP, CSD via Horux +- Del cliente: RFC receptor, razon social, regimen fiscal, uso CFDI, CP +- De la venta: ClaveProdServ SAT, ClaveUnidad SAT, cantidad, precio, descuento, IVA 16%, metodo de pago (PUE/PPD), forma de pago (01/03/04/99) + +### Generacion de XML + +El backend POS genera la estructura XML del CFDI usando Python (`lxml` para construccion de XML). El XML sin firmar se envia a Horux360, que firma con CSD del tenant y timbra con PAC. + +### Cancelacion de CFDI + +- **Quien puede cancelar**: solo owner o admin +- **Plazo**: preferentemente dentro del mismo mes (reglas SAT) +- **Motivos de cancelacion** (catalogo SAT): + - 01: Comprobante emitido con errores con relacion (requiere CFDI de sustitucion) + - 02: Comprobante emitido con errores sin relacion + - 03: No se llevo a cabo la operacion + - 04: Operacion nominativa relacionada en factura global +- Para motivo 01: se debe crear primero el CFDI de sustitucion, luego cancelar el original referenciando el nuevo UUID +- Cancelacion genera reverso contable automatico (poliza de egreso o cancelacion) + +### API Horux + +- `POST /api/nexus/cfdi/stamp` -- envia XML sin firmar, Horux firma y timbra +- `GET /api/nexus/cfdi/status/:uuid` -- estado del timbrado +- `POST /api/nexus/cfdi/cancel` -- cancelacion ante SAT (incluye motivo + UUID sustitucion si aplica) +- Auth: API key entre Nexus <-> Horux + +### Pendiente construir en Horux360 + +- 3 endpoints API (stamp, status, cancel) +- Auth API key +- Logica de timbrado con PAC (base ya existe con @nodecfdi) + +--- + +## 6. Modulo de Contabilidad + +### Catalogo de cuentas SAT + +Pre-cargado al crear tenant. Contador puede agregar subcuentas. Estructura estandar: Activo (100), Pasivo (200), Capital (300), Ingresos (400), Costos (500), Gastos (600). + +### Polizas automaticas + +Cada operacion genera polizas contables: +- Venta efectivo: cargo Bancos, abono Ventas + IVA trasladado + cargo Costo mercancia, abono Inventarios +- Venta credito: cargo Clientes, abono Ventas + IVA trasladado +- Cobro credito: cargo Bancos, abono Clientes +- Compra proveedor: cargo Inventarios + IVA acreditable, abono Proveedores +- Corte de caja: cargo Bancos, abono Caja + +### Cierre de periodos fiscales + +- Cerrar un periodo fiscal **impide** crear nuevas polizas contables en ese periodo +- Los CFDIs SI pueden seguir emitiendose (el SAT lo permite), pero sus polizas se registran en el periodo abierto actual +- Al cerrar un periodo se genera automaticamente la balanza de comprobacion final del periodo +- Solo el owner puede cerrar periodos + +### Reportes contables + +- Balanza de comprobacion (saldo inicial, cargos, abonos, saldo final) +- Estado de resultados (mensual) +- Balance general +- Estado de cuenta por cliente +- Antiguedad de saldos (cartera vencida: corriente, 1-30d, 31-60d, 61-90d, 90d+) + +### Tablas + +- **accounts**: id, code, name, parent_id, type, sat_code, is_system, is_active +- **journal_entries**: id, entry_number, date, type, description, reference_type, reference_id, status, created_by, is_auto +- **journal_entry_lines**: id, journal_entry_id, account_id, debit, credit, description +- **fiscal_periods**: id, year, month, status (open/closed), closed_by, closed_at + +--- + +## 7. Permisos y Empleados + +### Roles + +- **DUENO (owner)**: todo sin restricciones. Dashboard movil. +- **ADMINISTRADOR (admin)**: casi todo excepto cerrar periodos contables y eliminar admins. +- **CAJERO (cashier)**: POS (vender, cobrar, catalogo). Descuentos hasta % maximo configurable. Cancelar solo sus ventas en primeros 30 min. NO ve costos ni margenes. +- **ALMACENISTA (warehouse)**: inventario (entradas, salidas, toma fisica, transferencias). NO ve precios de venta ni contabilidad. +- **CONTADOR (accountant)**: contabilidad y facturacion. Todos los reportes. NO puede vender ni modificar inventario. + +### Permisos granulares + +Modulos: pos, inventory, catalog, customers, accounting, invoicing, reports, config. +Acciones: view, create, edit, delete, authorize. +Ejemplos: `pos.sell`, `pos.discount`, `pos.view_cost`, `inventory.adjust`, `customers.edit_credit`, `config.edit_prices` + +### Tablas + +- **employees**: id, name, email, phone, pin (4 digitos para login rapido POS), password_hash, role, branch_id, max_discount_pct, is_active +- **employee_permissions**: employee_id, permission +- **employee_sessions**: id, employee_id, device_id, token, expires_at + +### Login rapido POS + +PIN pad de 4 digitos para cambio rapido de cajero sin salir de la app. + +#### Seguridad del PIN + +- Rate limit: maximo 5 intentos por minuto por dispositivo +- Lockout: despues de 10 intentos fallidos, bloqueo de 15 minutos (cooldown) +- El PIN es por dispositivo (no global): se combina con `device_id` para binding de sesion +- PIN hasheado en DB (nunca en texto plano) + +--- + +## 8. Dashboard del Dueno + +### Vista principal (movil + web) + +- Ventas hoy vs meta diaria con progreso visual +- Cajas activas: ventas y margen por cajero +- Resumen: ticket promedio, margen, ventas a credito, efectivo en caja +- Alertas: descuentos, cancelaciones, stock bajo +- Grafica semanal de ventas +- Multi-sucursal: vista consolidada + por sucursal + +--- + +## 9. Auditoria + +### audit_log (tabla INSERT-only) + +- employee_id, action, entity_type, entity_id, old_value (JSON), new_value (JSON), device_id, ip_address, branch_id, timestamp +- Nunca UPDATE, nunca DELETE +- Acciones rastreadas: SALE, CANCEL, PRICE_CHANGE, STOCK_ADJUST, LOGIN, DISCOUNT, CREDIT_CHANGE, CONFIG_CHANGE +- Cambios de precio requieren permiso `config.edit_prices` +- Cancelaciones requieren motivo obligatorio + +--- + +## 10. Schema de Base de Datos + +### nexus_master (schema centralizado) + +```sql +CREATE TABLE tenants ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + db_name VARCHAR(100) UNIQUE NOT NULL, -- 'tenant_001' + rfc VARCHAR(13), + plan VARCHAR(50) DEFAULT 'basic', -- basic, pro, enterprise + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE subscriptions ( + id SERIAL PRIMARY KEY, + tenant_id INTEGER REFERENCES tenants(id), + plan VARCHAR(50) NOT NULL, + status VARCHAR(20) DEFAULT 'active', -- active, past_due, cancelled + started_at TIMESTAMPTZ DEFAULT NOW(), + expires_at TIMESTAMPTZ, + stripe_id VARCHAR(100) +); + +CREATE TABLE tenant_schema_version ( + tenant_id INTEGER PRIMARY KEY REFERENCES tenants(id), + version VARCHAR(20) NOT NULL DEFAULT 'v1.0', + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### tenant_{id} DB (schema completo por refaccionaria) + +```sql +-- ===================== +-- SUCURSALES +-- ===================== +CREATE TABLE branches ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + address TEXT, + phone VARCHAR(20), + is_active BOOLEAN DEFAULT TRUE +); + +-- ===================== +-- EMPLEADOS Y PERMISOS +-- ===================== +CREATE TABLE employees ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + email VARCHAR(200), + phone VARCHAR(20), + pin VARCHAR(100), -- hashed, 4 digitos + password_hash VARCHAR(200), + role VARCHAR(20) NOT NULL, -- owner, admin, cashier, warehouse, accountant + branch_id INTEGER REFERENCES branches(id), + max_discount_pct NUMERIC(5,2) DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE employee_permissions ( + employee_id INTEGER REFERENCES employees(id), + permission VARCHAR(100) NOT NULL, -- 'pos.sell', 'inventory.adjust', etc. + PRIMARY KEY (employee_id, permission) +); + +CREATE TABLE employee_sessions ( + id SERIAL PRIMARY KEY, + employee_id INTEGER REFERENCES employees(id), + device_id VARCHAR(200), + token VARCHAR(500) NOT NULL, + expires_at TIMESTAMPTZ NOT NULL +); + +-- ===================== +-- CLIENTES +-- ===================== +CREATE TABLE customers ( + id SERIAL PRIMARY KEY, + branch_id INTEGER REFERENCES branches(id), + name VARCHAR(300) NOT NULL, + rfc VARCHAR(13), + razon_social VARCHAR(300), + regimen_fiscal VARCHAR(10), -- codigo SAT regimen + uso_cfdi VARCHAR(10) DEFAULT 'G03', -- codigo SAT uso CFDI + cp VARCHAR(5), + email VARCHAR(200), + phone VARCHAR(20), + address TEXT, + price_tier SMALLINT DEFAULT 1 CHECK (price_tier IN (1,2,3)), -- 1=mostrador, 2=taller, 3=mayoreo + credit_limit NUMERIC(12,2) DEFAULT 0, + credit_balance NUMERIC(12,2) DEFAULT 0, -- saldo actual de credito + is_active BOOLEAN DEFAULT TRUE, + vehicle_info JSONB, -- [{make, model, year, vin, plates}] + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ===================== +-- INVENTARIO +-- ===================== +CREATE TABLE inventory ( + id SERIAL PRIMARY KEY, + branch_id INTEGER REFERENCES branches(id), + part_number VARCHAR(100) NOT NULL, + barcode VARCHAR(100), + name VARCHAR(300) NOT NULL, + description TEXT, + category_id INTEGER, + brand VARCHAR(100), + vehicle_compatibility JSONB, + unit VARCHAR(20) DEFAULT 'PZA', + cost NUMERIC(12,2) DEFAULT 0, + price_1 NUMERIC(12,2) DEFAULT 0, -- mostrador + price_2 NUMERIC(12,2) DEFAULT 0, -- taller + price_3 NUMERIC(12,2) DEFAULT 0, -- mayoreo + tax_rate NUMERIC(5,4) DEFAULT 0.16, + min_stock INTEGER DEFAULT 0, + max_stock INTEGER DEFAULT 0, + location VARCHAR(50), -- ubicacion en almacen + image_url VARCHAR(500), + is_active BOOLEAN DEFAULT TRUE, + catalog_part_id INTEGER, -- referencia a catalogo Nexus (via API, no FK) + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE inventory_operations ( + id SERIAL PRIMARY KEY, + inventory_id INTEGER REFERENCES inventory(id), + branch_id INTEGER REFERENCES branches(id), + operation_type VARCHAR(20) NOT NULL, -- SALE, PURCHASE, RETURN, ADJUST, TRANSFER, INITIAL + quantity INTEGER NOT NULL, -- positivo o negativo + reference_id INTEGER, + reference_type VARCHAR(50), -- 'sale', 'purchase', 'return', etc. + cost_at_time NUMERIC(12,2), + employee_id INTEGER REFERENCES employees(id), + device_id VARCHAR(200), + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); +-- Stock actual = SUM(inventory_operations.quantity) WHERE inventory_id=X AND branch_id=Y + +-- ===================== +-- VENTAS +-- ===================== +CREATE TABLE sales ( + id SERIAL PRIMARY KEY, + branch_id INTEGER REFERENCES branches(id), + customer_id INTEGER REFERENCES customers(id), -- NULL = publico general + employee_id INTEGER REFERENCES employees(id), + register_id INTEGER, -- FK cash_registers + sale_type VARCHAR(20) NOT NULL, -- cash, credit, mixed + payment_method VARCHAR(20), -- efectivo, transferencia, tarjeta, mixto + subtotal NUMERIC(12,2) NOT NULL, + discount_total NUMERIC(12,2) DEFAULT 0, + tax_total NUMERIC(12,2) NOT NULL, + total NUMERIC(12,2) NOT NULL, + amount_paid NUMERIC(12,2) DEFAULT 0, + change_given NUMERIC(12,2) DEFAULT 0, + metodo_pago_sat VARCHAR(3), -- PUE o PPD + forma_pago_sat VARCHAR(2), -- 01, 03, 04, 99 + status VARCHAR(20) DEFAULT 'completed', -- completed, cancelled, returned + device_id VARCHAR(200), + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE sale_items ( + id SERIAL PRIMARY KEY, + sale_id INTEGER REFERENCES sales(id), + inventory_id INTEGER REFERENCES inventory(id), + part_number VARCHAR(100), + name VARCHAR(300), + quantity INTEGER NOT NULL, + unit_price NUMERIC(12,2) NOT NULL, -- precio al momento de la venta + unit_cost NUMERIC(12,2), -- costo al momento de la venta + discount_pct NUMERIC(5,2) DEFAULT 0, + discount_amount NUMERIC(12,2) DEFAULT 0, + tax_rate NUMERIC(5,4) DEFAULT 0.16, + tax_amount NUMERIC(12,2) DEFAULT 0, + subtotal NUMERIC(12,2) NOT NULL, + clave_prod_serv VARCHAR(10), -- clave SAT + clave_unidad VARCHAR(10) -- clave unidad SAT +); + +-- ===================== +-- COTIZACIONES +-- ===================== +CREATE TABLE quotations ( + id SERIAL PRIMARY KEY, + branch_id INTEGER REFERENCES branches(id), + customer_id INTEGER REFERENCES customers(id), + employee_id INTEGER REFERENCES employees(id), + subtotal NUMERIC(12,2) NOT NULL, + tax_total NUMERIC(12,2) NOT NULL, + total NUMERIC(12,2) NOT NULL, + status VARCHAR(20) DEFAULT 'active', -- active, converted, expired, cancelled + valid_until DATE, + converted_sale_id INTEGER REFERENCES sales(id), + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE quotation_items ( + id SERIAL PRIMARY KEY, + quotation_id INTEGER REFERENCES quotations(id), + inventory_id INTEGER REFERENCES inventory(id), + part_number VARCHAR(100), + name VARCHAR(300), + quantity INTEGER NOT NULL, + unit_price NUMERIC(12,2) NOT NULL, + discount_pct NUMERIC(5,2) DEFAULT 0, + tax_rate NUMERIC(5,4) DEFAULT 0.16, + subtotal NUMERIC(12,2) NOT NULL +); + +-- ===================== +-- APARTADOS (LAYAWAYS) +-- ===================== +CREATE TABLE layaways ( + id SERIAL PRIMARY KEY, + branch_id INTEGER REFERENCES branches(id), + customer_id INTEGER REFERENCES customers(id) NOT NULL, + employee_id INTEGER REFERENCES employees(id), + total NUMERIC(12,2) NOT NULL, + amount_paid NUMERIC(12,2) DEFAULT 0, + status VARCHAR(20) DEFAULT 'active', -- active, completed, cancelled + expires_at DATE, + converted_sale_id INTEGER REFERENCES sales(id), + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE layaway_payments ( + id SERIAL PRIMARY KEY, + layaway_id INTEGER REFERENCES layaways(id), + amount NUMERIC(12,2) NOT NULL, + payment_method VARCHAR(20), + reference VARCHAR(100), + employee_id INTEGER REFERENCES employees(id), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ===================== +-- CAJA REGISTRADORA +-- ===================== +CREATE TABLE cash_registers ( + id SERIAL PRIMARY KEY, + branch_id INTEGER REFERENCES branches(id), + employee_id INTEGER REFERENCES employees(id), + register_number SMALLINT NOT NULL, -- numero de caja (1, 2, 3...) + opening_amount NUMERIC(12,2) NOT NULL, -- fondo inicial + closing_amount NUMERIC(12,2), -- monto contado al cerrar + expected_amount NUMERIC(12,2), -- monto esperado calculado + difference NUMERIC(12,2), -- closing - expected + status VARCHAR(10) DEFAULT 'open', -- open, closed + opened_at TIMESTAMPTZ DEFAULT NOW(), + closed_at TIMESTAMPTZ +); + +CREATE TABLE cash_movements ( + id SERIAL PRIMARY KEY, + register_id INTEGER REFERENCES cash_registers(id), + type VARCHAR(5) NOT NULL, -- 'in' o 'out' + amount NUMERIC(12,2) NOT NULL, + reason VARCHAR(300) NOT NULL, + employee_id INTEGER REFERENCES employees(id), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ===================== +-- FACTURACION (CFDI QUEUE) +-- ===================== +CREATE TABLE cfdi_queue ( + id SERIAL PRIMARY KEY, + sale_id INTEGER REFERENCES sales(id), + type VARCHAR(10) NOT NULL, -- ingreso, egreso, pago + xml_unsigned TEXT, -- XML generado por POS backend + xml_signed TEXT, -- XML firmado+timbrado por Horux + uuid_fiscal VARCHAR(36), -- UUID del SAT + status VARCHAR(20) DEFAULT 'pending', -- pending, sending, stamped, failed, cancelled + retry_count SMALLINT DEFAULT 0, + provisional_folio VARCHAR(20), -- PRE-XXXXX + error_message TEXT, + cancel_motive VARCHAR(2), -- 01, 02, 03, 04 + cancel_replacement_uuid VARCHAR(36), -- UUID del CFDI sustituto (motivo 01) + created_at TIMESTAMPTZ DEFAULT NOW(), + stamped_at TIMESTAMPTZ +); + +-- ===================== +-- CONTABILIDAD +-- ===================== +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(200) NOT NULL, + parent_id INTEGER REFERENCES accounts(id), + type VARCHAR(20) NOT NULL, -- activo, pasivo, capital, ingreso, costo, gasto + sat_code VARCHAR(20), + is_system BOOLEAN DEFAULT FALSE, -- cuentas predeterminadas no editables + is_active BOOLEAN DEFAULT TRUE +); + +CREATE TABLE journal_entries ( + id SERIAL PRIMARY KEY, + entry_number INTEGER NOT NULL, + date DATE NOT NULL, + type VARCHAR(20), -- ingreso, egreso, diario, poliza + description TEXT, + reference_type VARCHAR(50), -- sale, purchase, cash_register, etc. + reference_id INTEGER, + status VARCHAR(20) DEFAULT 'posted', -- draft, posted, cancelled + created_by INTEGER REFERENCES employees(id), + is_auto BOOLEAN DEFAULT TRUE, -- generada automaticamente + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE journal_entry_lines ( + id SERIAL PRIMARY KEY, + journal_entry_id INTEGER REFERENCES journal_entries(id), + account_id INTEGER REFERENCES accounts(id), + debit NUMERIC(14,2) DEFAULT 0, + credit NUMERIC(14,2) DEFAULT 0, + description TEXT +); + +CREATE TABLE fiscal_periods ( + id SERIAL PRIMARY KEY, + year SMALLINT NOT NULL, + month SMALLINT NOT NULL, + status VARCHAR(10) DEFAULT 'open', -- open, closed + closed_by INTEGER REFERENCES employees(id), + closed_at TIMESTAMPTZ, + UNIQUE (year, month) +); + +-- ===================== +-- AUDITORIA +-- ===================== +CREATE TABLE audit_log ( + id SERIAL PRIMARY KEY, + employee_id INTEGER REFERENCES employees(id), + action VARCHAR(50) NOT NULL, + entity_type VARCHAR(50), + entity_id INTEGER, + old_value JSONB, + new_value JSONB, + device_id VARCHAR(200), + ip_address VARCHAR(45), + branch_id INTEGER REFERENCES branches(id), + created_at TIMESTAMPTZ DEFAULT NOW() +); +-- INSERT-only: nunca UPDATE, nunca DELETE +``` + +--- + +## 11. Estructura del Proyecto + +### Codigo + +``` +/home/Autopartes/pos/ + +-- app.py (Flask app con blueprints) + +-- blueprints/ (auth, pos, catalog, inventory, customers, invoicing, accounting, reports, config, sync) + +-- services/ (tenant_manager, inventory_engine, accounting_engine, cfdi_queue, barcode_generator, sync_engine) + +-- static/pwa/ (manifest.json, sw.js, icons) + +-- static/js/ (pos, catalog, inventory, dashboard, accounting, reports, sync, keyboard, barcode) + +-- static/css/ (pos, catalog, common) + +-- templates/ (pos, catalog, inventory, customers, accounting, reports, dashboard, config, login) + +-- migrations/ (v1.0_initial.sql, runner.py) + +-- tenant_template.sql +``` + +### Tech Stack + +- **Backend**: Python 3 + Flask + Blueprints + psycopg2 +- **Frontend**: Vanilla JS + HTML + CSS (sin frameworks) +- **PWA**: Service Worker + IndexedDB + manifest.json +- **DB**: PostgreSQL (una DB por tenant + nexus_master) +- **Facturacion**: Horux360 API (timbrado CFDI) +- **XML CFDI**: Python lxml (generacion de XML) +- **Impresion**: Web Print API (tickets termicos 58/80mm) + +### Comunicacion entre sistemas + +- PWA <-> POS API (Flask): REST + sync engine +- POS API <-> Nexus Master (catalogo, bodegas): REST interno +- POS API <-> Horux360 (CFDI): REST con API key + +--- + +## 12. Theming y Branding + +### Sistema de temas + +El frontend es completamente temable via CSS custom properties. No hay colores, tipografia ni estilos hardcodeados. + +```css +/* Cada tema define estas variables */ +:root[data-theme="default"] { + --color-primary: ...; + --color-secondary: ...; + --color-accent: ...; + --color-bg: ...; + --color-surface: ...; + --color-text: ...; + --color-border: ...; + --font-display: ...; + --font-body: ...; + --font-mono: ...; + --radius: ...; + --shadow: ...; +} +``` + +### Selector de tema + +- Cada tenant puede seleccionar un tema desde Configuracion +- El tema se almacena en `config` del tenant +- Se aplica al cargar la PWA (antes del primer render) +- El equipo de diseno crea los temas, el sistema los aplica + +### Responsabilidades + +- **Desarrollo**: infraestructura de temas (CSS variables, selector, persistencia, aplicacion) +- **Equipo de diseno**: definicion de temas (paletas, tipografia, iconos, variantes light/dark) + +--- + +## 13. Infraestructura y Deployment + +### Deployment + +- Hosted en el mismo servidor que Nexus (actual) +- Nginx como reverse proxy +- SSL via Cloudflare +- PWA servida desde path `/pos/` +- Tenant routing via JWT: el token contiene `tenant_id`, el backend resuelve la DB correspondiente + +### Backups + +- `pg_dump` por tenant DB via cron nightly +- 30 dias de retencion +- Almacenado en volumen separado del servidor principal + +--- + +## 14. Fases Futuras (fuera de alcance v1) + +Las siguientes funcionalidades estan planeadas pero **no se incluyen en v1**: + +### Sugerencias de mantenimiento preventivo por vehiculo + +- Basado en historial de compras del cliente y kilometraje +- Requiere base de conocimiento de calendarios de mantenimiento por modelo/motor +- Ej: "Ultima vez le vendiste filtro de aceite hace 3 meses. Toca cambio de bujias?" +- **Prerequisito**: construir o integrar knowledge base de programas de mantenimiento + +### Marketplace / Pedidos a bodegas de la red Nexus + +- En v1 se incluye la funcion de **ver disponibilidad** en bodegas (solo lectura) +- Diferido: pedido directo a bodega, fulfillment, tracking de envio, confirmacion de recepcion +- Requiere: acuerdos comerciales entre bodegas, sistema de logistica, precios de distribuidor + +### Integracion WhatsApp + +- Venta por WhatsApp (cliente pregunta pieza, vendedor cotiza, apartado con QR) +- Notificaciones al cliente cuando llega su pedido/apartado +- Requiere: WhatsApp Business API, costos por mensaje, integracion con Meta +- Planeado para fase posterior + +### Push notifications + +- Notificaciones al dueno: alertas criticas (cancelaciones, diferencias de caja, stock cero) +- Notificaciones importantes (descuentos altos, credito excedido) +- Resumen diario de ventas +- Requiere: Web Push API, service worker notifications, backend push service +- Planeado para fase posterior