Files
Autoparts-DB/docs/plans/2026-03-27-pos-inventario-design.md
consultoria-as 11d8d7ae0b 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) <noreply@anthropic.com>
2026-03-31 01:03:24 +00:00

33 KiB

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)

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)

-- =====================
-- 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.

/* 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