# 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