""" Art4Hotel Hub v2 - Servidor local Catalogo estandarizado + tabulador de precios + entregas + mobile Zero dependencias externas (stdlib only) Uso: python server.py http://localhost:4401 """ import http.server, json, sqlite3, os, urllib.parse, re, mimetypes, io import hashlib, hmac, base64, secrets, time, http.cookies from datetime import datetime, date from pathlib import Path PORT = 4401 DB_PATH = Path(__file__).parent / "art4hotel.db" STATIC_DIR = Path(__file__).parent UPLOADS_DIR = Path(__file__).parent / "uploads" UPLOADS_DIR.mkdir(exist_ok=True) MAX_UPLOAD = 20 * 1024 * 1024 # 20MB # ── Autenticación ── SESSION_HOURS = 24 * 14 # la sesión dura 14 días SECRET_FILE = Path(__file__).parent / "secret.key" def _load_secret(): if SECRET_FILE.exists(): return SECRET_FILE.read_bytes() s = secrets.token_bytes(32) SECRET_FILE.write_bytes(s) try: os.chmod(SECRET_FILE, 0o600) except Exception: pass return s SECRET = _load_secret() def hash_password(password, salt=None): if salt is None: salt = secrets.token_hex(16) h = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 200000).hex() return salt + '$' + h def verify_password(password, stored): try: salt, _ = stored.split('$', 1) return hmac.compare_digest(hash_password(password, salt), stored) except Exception: return False def make_session(username): exp = int(time.time()) + SESSION_HOURS * 3600 payload = f"{username}|{exp}" sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest() return base64.urlsafe_b64encode(payload.encode()).decode() + '.' + sig def check_session(token): """Devuelve el username si el token es válido y no expiró, si no None.""" try: b64, sig = token.split('.', 1) payload = base64.urlsafe_b64decode(b64.encode()).decode() expect = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expect): return None username, exp = payload.split('|') if int(exp) < time.time(): return None return username except Exception: return None def get_db(): conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") return conn def init_db(): conn = get_db() c = conn.cursor() # ── Catalogo: Modelos (lineas de producto) ── c.execute(""" CREATE TABLE IF NOT EXISTS modelos ( id INTEGER PRIMARY KEY AUTOINCREMENT, clave TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL, descripcion TEXT DEFAULT '', activo INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── Catalogo: Materiales ── c.execute(""" CREATE TABLE IF NOT EXISTS materiales ( id INTEGER PRIMARY KEY AUTOINCREMENT, clave TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL, tipo TEXT DEFAULT 'base', costo_unitario REAL DEFAULT 0, activo INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── Catalogo: Tipos de trabajo ── c.execute(""" CREATE TABLE IF NOT EXISTS trabajos ( id INTEGER PRIMARY KEY AUTOINCREMENT, clave TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL, costo_base REAL DEFAULT 0, variable_por TEXT DEFAULT 'fijo', proveedor_default TEXT DEFAULT '', activo INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── Catalogo: Productos (reemplaza inventario viejo) ── c.execute(""" CREATE TABLE IF NOT EXISTS productos ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL, categoria TEXT DEFAULT 'bolsa', talla TEXT DEFAULT '', material TEXT DEFAULT '', costo_base REAL DEFAULT 0, proveedor TEXT DEFAULT '', stock_actual INTEGER DEFAULT 0, punto_reorden INTEGER DEFAULT 10, activo INTEGER DEFAULT 1, notas TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # (Campos legacy color/modelo/logo_diseno/tipo_personalizacion retirados 2026-05-31) # ── Usuarios (login del Hub) ── c.execute(""" CREATE TABLE IF NOT EXISTS usuarios ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, pass_hash TEXT NOT NULL, nombre TEXT DEFAULT '', activo INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── Clientes (ampliado) ── c.execute(""" CREATE TABLE IF NOT EXISTS clientes ( id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT UNIQUE NOT NULL, tipo TEXT DEFAULT 'hotel', contacto TEXT DEFAULT '', zona_entrega TEXT DEFAULT '', costo_entrega REAL DEFAULT 0, condiciones_pago TEXT DEFAULT 'Por definir', notas TEXT DEFAULT '', activo INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── OC (Orden de Compra del cliente — agrupa lineas) ── c.execute(""" CREATE TABLE IF NOT EXISTS oc ( id INTEGER PRIMARY KEY AUTOINCREMENT, oc_id TEXT UNIQUE NOT NULL, cliente TEXT DEFAULT '', fecha_oc TEXT DEFAULT '', fecha_entrega TEXT DEFAULT '', recibio TEXT DEFAULT '', costo_logistica REAL DEFAULT 0, precio_factura REAL DEFAULT 0, iva_pct REAL DEFAULT 16, factura_num TEXT DEFAULT '', condiciones_pago TEXT DEFAULT 'Por definir', status TEXT DEFAULT 'Activa', notas TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # Migrate OC: add iva_pct, otros_gastos, payment fields if missing existing_oc = {r[1] for r in c.execute("PRAGMA table_info(oc)").fetchall()} if 'iva_pct' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN iva_pct REAL DEFAULT 16") if 'otros_gastos' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN otros_gastos REAL DEFAULT 0") if 'otros_gastos_desc' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN otros_gastos_desc TEXT DEFAULT ''") if 'pagado' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN pagado INTEGER DEFAULT 0") if 'fecha_pago' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN fecha_pago TEXT DEFAULT ''") if 'metodo_pago' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN metodo_pago TEXT DEFAULT ''") if 'oc_origen_id' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN oc_origen_id INTEGER DEFAULT NULL") if 'propuesta_id' not in existing_oc: c.execute("ALTER TABLE oc ADD COLUMN propuesta_id INTEGER DEFAULT NULL") # ── Catálogos (presentaciones de productos para enviar a prospectos) ── c.execute(""" CREATE TABLE IF NOT EXISTS catalogos ( id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT NOT NULL, segmento TEXT DEFAULT '', cliente_nombre TEXT DEFAULT '', fecha TEXT DEFAULT '', items TEXT DEFAULT '[]', show_prices INTEGER DEFAULT 1, show_clientes INTEGER DEFAULT 1, show_lead_time INTEGER DEFAULT 1, notas TEXT DEFAULT '', status TEXT DEFAULT 'borrador', entrega TEXT DEFAULT '2-4 semanas', minimo_compra TEXT DEFAULT '', terminos TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # Migración catalogos: términos comerciales (tabla puede existir sin estas columnas) existing_cat = {r[1] for r in c.execute("PRAGMA table_info(catalogos)").fetchall()} if 'entrega' not in existing_cat: c.execute("ALTER TABLE catalogos ADD COLUMN entrega TEXT DEFAULT '2-4 semanas'") if 'minimo_compra' not in existing_cat: c.execute("ALTER TABLE catalogos ADD COLUMN minimo_compra TEXT DEFAULT ''") if 'terminos' not in existing_cat: c.execute("ALTER TABLE catalogos ADD COLUMN terminos TEXT DEFAULT ''") if 'show_contacto' not in existing_cat: c.execute("ALTER TABLE catalogos ADD COLUMN show_contacto INTEGER DEFAULT 1") # ── Ordenes (lineas de produccion, ahora con oc_id FK) ── c.execute(""" CREATE TABLE IF NOT EXISTS ordenes ( id INTEGER PRIMARY KEY AUTOINCREMENT, orden_id TEXT NOT NULL, oc_id INTEGER DEFAULT NULL REFERENCES oc(id), tipo_orden TEXT DEFAULT 'OC', cliente TEXT DEFAULT '', producto TEXT DEFAULT '', sku TEXT DEFAULT '', cantidad INTEGER DEFAULT 0, tipo_trabajo TEXT DEFAULT '', stage TEXT NOT NULL DEFAULT 'En 2 Mares', fecha_oc TEXT DEFAULT '', fecha_inicio TEXT DEFAULT '', fecha_estimada TEXT DEFAULT '', fecha_recepcion TEXT DEFAULT '', fecha_entrega TEXT DEFAULT '', recibio TEXT DEFAULT '', urgente INTEGER DEFAULT 0, logo_instrucciones TEXT DEFAULT '', notas TEXT DEFAULT '', costo_producto REAL DEFAULT 0, costo_trabajo REAL DEFAULT 0, costo_logistica REAL DEFAULT 0, precio_factura REAL DEFAULT 0, check_facturada INTEGER DEFAULT 0, check_empacada INTEGER DEFAULT 0, check_etiquetas INTEGER DEFAULT 0, check_vehiculo INTEGER DEFAULT 0, grupo_oc TEXT DEFAULT '', piezas_recibidas INTEGER DEFAULT 0, piezas_danadas INTEGER DEFAULT 0, nota_recepcion TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # Migrate: add new columns if they don't exist (for existing DBs) new_cols_ordenes = [ ("costo_producto","REAL DEFAULT 0"), ("costo_trabajo","REAL DEFAULT 0"), ("costo_logistica","REAL DEFAULT 0"), ("precio_factura","REAL DEFAULT 0"), ("check_facturada","INTEGER DEFAULT 0"), ("check_empacada","INTEGER DEFAULT 0"), ("check_etiquetas","INTEGER DEFAULT 0"), ("check_vehiculo","INTEGER DEFAULT 0"), ("grupo_oc","TEXT DEFAULT ''"), ("piezas_recibidas","INTEGER DEFAULT 0"), ("piezas_danadas","INTEGER DEFAULT 0"), ("nota_recepcion","TEXT DEFAULT ''"), ("oc_id","INTEGER DEFAULT NULL"), ] existing = {r[1] for r in c.execute("PRAGMA table_info(ordenes)").fetchall()} for col, typedef in new_cols_ordenes: if col not in existing: c.execute(f"ALTER TABLE ordenes ADD COLUMN {col} {typedef}") # ── Inventario legacy (keep for backward compat, hidden) ── c.execute(""" CREATE TABLE IF NOT EXISTS inventario ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku TEXT UNIQUE NOT NULL, nombre TEXT DEFAULT '', descripcion TEXT DEFAULT '', tipo TEXT DEFAULT '', talla TEXT DEFAULT '', color_base TEXT DEFAULT '', proveedor TEXT DEFAULT '', stock_inicial INTEGER DEFAULT 0, punto_reorden INTEGER DEFAULT 10, costo_unitario REAL DEFAULT 0, ultimo_conteo TEXT DEFAULT '', notas TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── Compras ── c.execute(""" CREATE TABLE IF NOT EXISTS compras ( id INTEGER PRIMARY KEY AUTOINCREMENT, compra_id TEXT NOT NULL, proveedor TEXT DEFAULT '', fecha_compra TEXT DEFAULT '', fecha_llegada TEXT DEFAULT '', sku TEXT DEFAULT '', nombre_producto TEXT DEFAULT '', cantidad INTEGER DEFAULT 0, costo_unitario REAL DEFAULT 0, status TEXT DEFAULT 'Pedido', notas TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── Tareas operativas ── c.execute(""" CREATE TABLE IF NOT EXISTS tareas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, descripcion TEXT DEFAULT '', prioridad TEXT DEFAULT 'normal', stage TEXT NOT NULL DEFAULT 'pendiente', categoria TEXT DEFAULT 'general', fecha_limite TEXT DEFAULT '', asignado TEXT DEFAULT '', notas TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # ── Proyectos Recurrentes (receta cliente+modelo+logo+trabajo) ── c.execute(""" CREATE TABLE IF NOT EXISTS proyectos ( id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT NOT NULL, producto_id INTEGER REFERENCES productos(id), producto_nombre TEXT DEFAULT '', cliente TEXT DEFAULT '', tipo_trabajo TEXT DEFAULT '', costo_unitario REAL DEFAULT 0, costo_trabajo REAL DEFAULT 0, logo_descripcion TEXT DEFAULT '', logo_archivo TEXT DEFAULT '', foto_terminado TEXT DEFAULT '', notas TEXT DEFAULT '', activo INTEGER DEFAULT 1, veces_usado INTEGER DEFAULT 0, ultimo_uso TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # Migrate ordenes: add proyecto_id reference existing_ord = {r[1] for r in c.execute("PRAGMA table_info(ordenes)").fetchall()} if 'proyecto_id' not in existing_ord: c.execute("ALTER TABLE ordenes ADD COLUMN proyecto_id INTEGER DEFAULT NULL") # Curaduría web: ¿este pedido/foto se muestra como ejemplo en el sitio público? if 'web_ejemplo' not in existing_ord: c.execute("ALTER TABLE ordenes ADD COLUMN web_ejemplo INTEGER DEFAULT 0") # Etiqueta pública para el ejemplo (zona genérica, "muestra"...) — NO el cliente real if 'web_etiqueta' not in existing_ord: c.execute("ALTER TABLE ordenes ADD COLUMN web_etiqueta TEXT DEFAULT ''") # Migrate productos: add tipos_trabajo_disponibles (CSV) existing_pr = {r[1] for r in c.execute("PRAGMA table_info(productos)").fetchall()} if 'tipos_trabajo_disponibles' not in existing_pr: c.execute("ALTER TABLE productos ADD COLUMN tipos_trabajo_disponibles TEXT DEFAULT ''") # Flag para publicar el producto en el sitio web público (art4hotel.com) if 'mostrar_en_web' not in existing_pr: c.execute("ALTER TABLE productos ADD COLUMN mostrar_en_web INTEGER DEFAULT 0") # Descripción corta para el sitio web (opcional, editable desde el catálogo) if 'descripcion_web' not in existing_pr: c.execute("ALTER TABLE productos ADD COLUMN descripcion_web TEXT DEFAULT ''") # Medidas físicas del producto (texto libre, ej. "40 x 35 x 12 cm") if 'medidas' not in existing_pr: c.execute("ALTER TABLE productos ADD COLUMN medidas TEXT DEFAULT ''") # ── Propuestas / Cotizaciones ── c.execute(""" CREATE TABLE IF NOT EXISTS propuestas ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero TEXT UNIQUE NOT NULL, cliente_nombre TEXT DEFAULT '', contacto TEXT DEFAULT '', empresa TEXT DEFAULT '', direccion TEXT DEFAULT '', locacion TEXT DEFAULT '', tipo_negocio TEXT DEFAULT '', email TEXT DEFAULT '', telefono TEXT DEFAULT '', fecha TEXT DEFAULT '', vigencia_dias INTEGER DEFAULT 15, items TEXT DEFAULT '[]', iva_pct REAL DEFAULT 16, descuento_pct REAL DEFAULT 0, notas TEXT DEFAULT '', status TEXT DEFAULT 'borrador', created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ) """) # Migrate: add locacion if missing existing_pp = {r[1] for r in c.execute("PRAGMA table_info(propuestas)").fetchall()} if 'locacion' not in existing_pp: c.execute("ALTER TABLE propuestas ADD COLUMN locacion TEXT DEFAULT ''") # ── Bitacora ── c.execute(""" CREATE TABLE IF NOT EXISTS bitacora ( id INTEGER PRIMARY KEY AUTOINCREMENT, tipo TEXT NOT NULL DEFAULT 'nota', titulo TEXT NOT NULL, descripcion TEXT DEFAULT '', referencia TEXT DEFAULT '', fecha TEXT DEFAULT (datetime('now','localtime')) ) """) conn.commit() # Seed catalogo if empty if c.execute("SELECT COUNT(*) FROM modelos").fetchone()[0] == 0: seed_catalogo(conn) # Seed ordenes if empty if c.execute("SELECT COUNT(*) FROM ordenes").fetchone()[0] == 0: seed_data(conn) conn.close() def seed_catalogo(conn): """Seed catalog tables with initial data from Art4Hotel's real products""" c = conn.cursor() modelos = [ ("marina","Marina","Linea de bolsas con acabado nautico/yute"), ("cabo_bello","Cabo Bello","Diseno exclusivo Cabo Bello"), ("2026","2026","Coleccion 2026 - DTF"), ("east_cape","East Cape","Linea para zona East Cape/Los Barriles"), ("basica","Basica","Productos sin personalizacion de linea"), ("flora_mango","Flora Mango","Linea exclusiva Flora Farms"), ] for m in modelos: c.execute("INSERT INTO modelos (clave,nombre,descripcion) VALUES (?,?,?)", m) materiales = [ ("yute","Yute","base",0), ("manta","Manta cruda","base",0), ("loneta","Loneta","base",0), ("lona","Lona","base",0), ("piel","Piel","secundario",0), ("ceramica","Ceramica","base",0), ("metal","Metal","base",0), ("madera","Madera","base",0), ] for m in materiales: c.execute("INSERT INTO materiales (clave,nombre,tipo,costo_unitario) VALUES (?,?,?,?)", m) trabajos = [ ("bordado","Bordado",35,"complejidad","2 Mares"), ("serigrafia","Serigrafia",25,"fijo","2 Mares"), ("grabado_laser","Grabado laser",30,"tamano","2 Mares"), ("impresion_dtf","Impresion DTF",20,"fijo","2 Mares"), ("costura","Costura",15,"fijo","Taller Sofia"), ("otro","Otro",0,"fijo",""), ] for t in trabajos: c.execute("INSERT INTO trabajos (clave,nombre,costo_base,variable_por,proveedor_default) VALUES (?,?,?,?,?)", t) clientes = [ ("Nobu Hotel","hotel","","Corredor turistico",200,"Contra entrega",""), ("Nobu Residences","hotel","","Corredor turistico",200,"Contra entrega",""), ("Flora Farms","hotel","Ximena","San Jose del Cabo",150,"30 dias","Logo Flora Mango"), ("La Ventana","hotel","","La Ventana",350,"Contra entrega",""), ("Puerta Cortes","hotel","","Corredor turistico",200,"Contra entrega",""), ("The Cape","hotel","","Corredor turistico",200,"Contra entrega",""), ("R Residence","hotel","","Corredor turistico",200,"Contra entrega","OC por 100 pzas"), ("Corazon","hotel","","San Jose del Cabo",150,"Contra entrega","Piden factura"), ("Los Barriles","tienda","","East Cape",400,"Contra entrega",""), ("Tienda Chapitos Los Barriles","tienda","","East Cape",400,"Contra entrega",""), ("Vladimir (Pescadero)","independiente","","Pescadero",300,"Contra entrega","$2600 utilidad anterior"), ("Tres Guerras","distribuidor","","La Paz",500,"Contra entrega",""), ("Ventanas","hotel","","Los Cabos",200,"Contra entrega",""), ("Rubas","tienda","","Los Cabos",150,"Contra entrega",""), ("Cafe Saturno","restaurante","","Todos Santos",300,"Contra entrega","Prospecto"), ("Todos Santos Hotel Boutique","hotel","","Todos Santos",300,"Contra entrega","Prospecto"), ("Wild Cabo","hotel","","Los Cabos",200,"Contra entrega","Prospecto"), ("Puerto Los Cabos","hotel","","San Jose del Cabo",150,"Contra entrega",""), ] for cl in clientes: c.execute("INSERT INTO clientes (nombre,tipo,contacto,zona_entrega,costo_entrega,condiciones_pago,notas) VALUES (?,?,?,?,?,?,?)", cl) conn.commit() def seed_data(conn): c = conn.cursor() # ═══ 38 ORDENES REALES del CSV NocoDB ═══ ordenes = [ ("ORD-2026-019","OC","Nobu Hotel","Bolsa yute grande asas cafe (entrega 1)","",102,"Serigrafia","En Almacen","","2026-04-28","","","","",0,"","ENTREGA PARCIAL 1 de 2 - 102 bolsas entregadas. Faltan 98 pendientes en Taller Sofia."), ("ORD-2026-024","OC","Puerto Los Cabos","Bolsa manta base yute serigrafia","",99,"Serigrafia","Entregado","","2026-05-04","","","2026-05-23","",0,"","Entregado a Nobu (99 piezas). Falta 1 pendiente reponer en 2 Mares."), ("ORD-2026-029","OC","Tienda Chapitos Los Barriles","Maletin East Cape bordado","",5,"Bordado","En Almacen","","2026-05-04","","","","",0,"","Lista en bodega. Se entregara junto con todo el pedido de Los Barriles."), ("ORD-2026-034","OC","La Ventana","Tazas","",46,"Serigrafia","En Almacen","","2026-05-05","","2026-05-22","","",0,"","Recibidas 46 tazas. Posiblemente falta 1 en 2 Mares."), ("ORD-2026-035","OC","La Ventana","Sombreros con rectangulo de piel grabado y pegado","",40,"Grabado laser","En 2 Mares","","2026-05-05","","","","",0,"","PENDIENTE: esperando autorizacion de diseno en piel."), ("ORD-2026-036","OC","Puerta Cortes","Pieles sombrero","",5,"Grabado laser","En 2 Mares","","2026-05-05","","","","",0,"","PENDIENTE: todavia no sale bien el diseno."), ("ORD-2026-037","OC","Los Barriles","Pieles sombrero","",14,"Grabado laser","En 2 Mares","","2026-05-05","","","","",0,"","LISTO en taller. Manana entregan la piel para pegar (14 piezas)."), ("ORD-2026-038","OC","La Ventana","Bolsa base yute serigrafia","",10,"Serigrafia","En Almacen","","2026-05-05","","","2026-05-22","",0,"","Ya entregadas 40 piezas. Quedan 10 en bodega para proxima vuelta a la ventana."), ("ORD-2026-039","OC","Nobu Residences","Bolsa yute grande asas cafe - Logo Nobu Residences","",50,"Bordado","En Taller Sofia","","2026-05-16","","","","",0,"","En taller Sofia. Posiblemente listos manana, entrega ese mismo dia."), ("ORD-2026-040","OC","Flora Farms","Bolsa loneta asa cafe 2026 DTF","",50,"Impresion","En 2 Mares","","2026-05-16","","","","",0,"Logo: Flora Mango","Cliente Flora Farms. Logo: Flora Mango."), ("ORD-2026-041","OC","Flora Farms","Mandiles piel grabada","",59,"Grabado laser","En 2 Mares","","2026-05-16","","","","",0,"Logo: Flora Mango","Cliente Flora Farms. Logo: Flora Mango."), ("ORD-2026-042","OC","Corazon","Bolsa Cabo Bello serigrafia","",50,"Serigrafia","En 2 Mares","","2026-05-21","","","","",0,"","ORDEN NUEVA. Hotel realizara pago. Piden factura."), ("ORD-2026-043","OC","Flora Farms","Bolsa 2026 asas negras DTF","",50,"Impresion","Entregado","","2026-05-21","","","2026-05-23","",0,"Logo: Flora Mango","Entregadas hoy."), ("ORD-2026-031","OC","La Ventana","Bolsa Cabo Bello","",30,"","En Almacen","","","","2026-05-13","","",0,"","Ya entregadas 50, quedan 30 en bodega para proxima vuelta."), ("ORD-2026-032","OC","La Ventana","Bolsa 2026 DTF","",29,"","En Almacen","","","","2026-05-13","","",0,"","Ya entregadas 31, quedan 29 en bodega para proxima vuelta."), ("ORD-2026-033","OC","La Ventana","Bolsa base negra","",23,"","Entregado","","","","2026-05-13","2026-05-23","",0,"","Entregadas las 23 piezas."), ("ORD-2026-019-B","OC","Nobu Hotel","Bolsa yute grande asas cafe - Logo Nobu hotel bordado","",98,"","En Taller Sofia","","","","2026-05-13","","",0,"","Nobu - faltan 98 bolsas en taller Sofia. Esperando completar para entrega unica."), ("ORD-2026-026","OC","Puerta Cortes","Bolsa Cabo Bello bordado","",10,"","Entregado","","","","2026-05-05","2026-05-23","",0,"","Entrega total 10 piezas (5 produccion nueva + 5 bodega)."), ("ORD-2026-027","OC","Puerta Cortes","Bolsa mini bordado","",10,"","Entregado","","","","2026-05-05","2026-05-23","",0,"","Entregado."), ("ORD-2026-028","OC","Puerta Cortes","Maletin loneta bordado","",5,"","Entregado","","","","2026-05-05","2026-05-23","",0,"","Entregado."), ("ORD-2026-025","OC","Puerta Cortes","Bolsa 2026 DTF","",9,"","Entregado","","","","2026-05-05","2026-05-23","",0,"","Entregadas 9 piezas. Queda 1 para proxima vuelta."), ("ORD-2026-022","OC","R Residence","Bolsa yute forro interior","",50,"","En Almacen","","","","2026-05-21","","",0,"","Produccion adelantada. Esta semana OC por 100 piezas."), ("N/A-001","OC","Vladimir (Pescadero)","Bolsas varias","",55,"","Entregado","","","","","","",0,"","Pago recibido. $2600 utilidad. Resurtir 140+ bolsas prox visita"), ("N/A-002","OC","Flora Farms","Bolsa base yute","",150,"","Entregado","","","","","2026-05-03","Ximena",0,"","Factura firmada por Ximena"), ("N/A-003","OC","Flora Farms","Llaveros","",50,"","Entregado","","","","","2026-05-03","",0,"","OC 2"), ("N/A-004","OC","Flora Farms","Imanes","",50,"","Entregado","","","","","2026-05-03","",0,"",""), ("N/A-005","OC","Flora Farms","Vasos","",450,"","Entregado","","","","","2026-05-03","",0,"","+50 entregados mes anterior"), ("N/A-006","OC","Rubas","Bolsa 2026","",10,"","Entregado","","","","","2026-05-02","",0,"",""), ("ORD-2026-014","OC","The Cape","Bolsa playa piel grabada","",50,"","Entregado","","","","","2026-04-30","",0,"","Factura firmada, falta escanear"), ("N/A-007","OC","Tres Guerras","Bolsa base yute (20+60+60+60)","",200,"","Entregado","","","","","2026-04-30","",0,"",""), ("N/A-008","OC","Tres Guerras","Maleta","",25,"","Entregado","","","","","2026-04-30","",0,"",""), ("ORD-2026-023","OC","Ventanas","Bolsa yute con piel","",100,"","Entregado","","","","","2026-05-21","",0,"",""), ("MUES-2026-001","Muestra","Cafe Saturno","Taza Blanca con logo","",1,"Impresion","En Almacen","2026-05-21","","","2026-05-22","","",0,"DTF UV","MUESTRA para presentar a la duena."), ("MUES-2026-002","Muestra","Todos Santos Hotel Boutique","Taza Blanca con logo","",1,"Impresion","En Almacen","2026-05-21","","","2026-05-22","","",0,"DTF UV","MUESTRA para presentar a la duena."), ("MUES-2026-003","Muestra","Wild Cabo","Taza Blanca con logo","",1,"Impresion","En Almacen","2026-05-21","","","2026-05-22","","",0,"DTF UV","MUESTRA para presentar a la duena."), ("MUESTRA-2026-001","Muestra","Cafe Saturno","Taza blanca con logo DTF UV (muestra)","",1,"Impresion","En 2 Mares","2026-05-21","","","","","",0,"","Muestra para presentar a la duena."), ("MUESTRA-2026-002","Muestra","Todos Santos Hotel Boutique","Taza blanca con logo DTF UV (muestra)","",1,"Impresion","En 2 Mares","2026-05-21","","","","","",0,"","Muestra para presentar a la duena."), ("MUESTRA-2026-003","Muestra","Wild Cabo","Taza blanca con logo DTF UV (muestra)","",1,"Impresion","En 2 Mares","2026-05-21","","","","","",0,"","Muestra para presentar a la duena."), ] for o in ordenes: c.execute("""INSERT INTO ordenes (orden_id,tipo_orden,cliente,producto,sku,cantidad,tipo_trabajo,stage, fecha_oc,fecha_inicio,fecha_estimada,fecha_recepcion,fecha_entrega,recibio, urgente,logo_instrucciones,notas) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", o) # ═══ INVENTARIO legacy (mantener por compatibilidad) ═══ inventario = [ ("BOLSA-YUTE-GR","Bolsa yute grande asas cafe","Bolsa tote de yute con asas de algodon cafe","Bolsa tote","GR","Natural","Sandra",200,20,45,"2026-05-22","Stock principal Nobu + varios clientes"), ("BOLSA-MANTA-MD","Bolsa manta base yute","Bolsa de manta con base de yute","Bolsa ecologica","MD","Natural","Sandra",100,15,35,"2026-05-22","Para serigrafia"), ("BOLSA-LONETA-MD","Bolsa loneta asa cafe 2026","Bolsa loneta con asa cafe para DTF","Bolsa de tela","MD","Natural","Sandra",80,10,40,"2026-05-22","DTF Flora Mango"), ("BOLSA-CABO-BELLO","Bolsa Cabo Bello","Bolsa con diseno Cabo Bello para serigrafia","Bolsa tote","MD","Natural","Sandra",60,10,38,"2026-05-22","Varios clientes"), ("TAZA-BLANCA-STD","Taza blanca estandar","Taza ceramica blanca para impresion DTF UV","Accesorio","Unica","Blanco","Proveedor tazas",50,10,25,"2026-05-22","Para muestras y ordenes"), ] for i in inventario: c.execute("""INSERT INTO inventario (sku,nombre,descripcion,tipo,talla,color_base,proveedor,stock_inicial,punto_reorden,costo_unitario,ultimo_conteo,notas) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", i) # ═══ TAREAS del handover ═══ tareas = [ ("Llenar Stock_Inicial real en Inventario","Conteo fisico de bodega - actualizar cada SKU","alta","pendiente","operaciones","","Tess","Del handover NocoDB"), ("Ajustar SKUs de ejemplo a productos reales","Los 5 SKUs actuales son aproximados","normal","pendiente","operaciones","","Clod",""), ("Ligar ordenes existentes a SKU en Inventario","Campo Inventario_Link en cada orden","normal","pendiente","operaciones","","Clod",""), ("Subir soportes pendientes","Remisiones/fotos: ORD-031, 032, 026, 038","alta","pendiente","operaciones","","Clod",""), ("Crear cuentas Tess y Andre en NocoDB","Usuarios con permisos adecuados","normal","pendiente","config","","Clod",""), ("Compartir formulario Nueva Orden por WhatsApp","Link publico al grupo","normal","pendiente","config","","Clod",""), ("Crear vistas filtradas en NocoDB","Por taller, por cliente, por mes, urgentes","baja","backlog","config","","Clod",""), ("Configurar respaldos automaticos PostgreSQL","Backup del contenedor NocoDB","normal","backlog","config","","Clod",""), ("Asegurar Tailscale en celulares del equipo","Para acceso remoto a NocoDB","normal","pendiente","config","","Clod",""), ("Entregar ORD-029 Los Barriles","Maletin East Cape - listo en bodega","alta","pendiente","entregas","","Clod","Junto con todo el pedido"), ("Entregar ORD-022 R Residence","50 bolsas yute forro interior listas","alta","pendiente","entregas","","Clod","OC por 100 pzas esta semana"), ("Entregar muestras Cafe Saturno / Todos Santos / Wild Cabo","3 tazas DTF UV listas en almacen","normal","pendiente","ventas","","Clod","Presentar a las duenas"), ("Recoger ORD-039 Nobu Residences de Taller Sofia","50 bolsas bordado - posiblemente listas","alta","en_progreso","produccion","","Clod","Entrega mismo dia"), ("Cobrar factura Hotel Corazon ORD-042","Hotel realizara pago, piden factura","normal","pendiente","cobranza","","Clod",""), ("Resurtir 140+ bolsas Vladimir Pescadero","Proxima visita","normal","backlog","ventas","","Tess","$2600 utilidad anterior"), ] for t in tareas: c.execute("""INSERT INTO tareas (titulo,descripcion,prioridad,stage,categoria,fecha_limite,asignado,notas) VALUES (?,?,?,?,?,?,?,?)""", t) # ═══ BITACORA ═══ bitacora = [ ("entrega","Entrega Puerta Cortes completa","ORD-026,027,028,025 - bolsas, mini, maletin, DTF","2026-05-23"), ("entrega","Entrega Flora Farms ORD-043","50 bolsas 2026 asas negras DTF","2026-05-23"), ("entrega","Entrega Puerto Los Cabos ORD-024","99 bolsas manta serigrafia","2026-05-23"), ("entrega","Entrega La Ventana ORD-033","23 bolsas base negra","2026-05-23"), ("produccion","Recepcion tazas La Ventana ORD-034","46 tazas recibidas de 2 Mares","2026-05-22"), ("produccion","Muestras DTF UV listas","Cafe Saturno, Todos Santos, Wild Cabo","2026-05-22"), ("hito","Sistema NocoDB operativo","38 ordenes migradas, estructura validada","2026-05-22"), ("produccion","ORD-042 Corazon registrada","50 bolsas Cabo Bello serigrafia - nueva orden","2026-05-21"), ("produccion","R Residence produccion adelantada","50 bolsas forro interior listas","2026-05-21"), ("entrega","Flora Farm entrega grande","150 bolsas + 50 llaveros + 50 imanes + 450 vasos","2026-05-03"), ] for b in bitacora: c.execute("INSERT INTO bitacora (tipo,titulo,descripcion,fecha) VALUES (?,?,?,?)", b) conn.commit() # ═══════════════════════════════════════════ # API # ═══════════════════════════════════════════ TABLES = { 'oc': { 'fields': ['oc_id','cliente','fecha_oc','fecha_entrega','recibio', 'costo_logistica','otros_gastos','otros_gastos_desc', 'precio_factura','iva_pct','factura_num', 'condiciones_pago','status','notas', 'pagado','fecha_pago','metodo_pago','oc_origen_id','propuesta_id'], 'int_fields': ['pagado','oc_origen_id','propuesta_id'], 'float_fields': ['costo_logistica','otros_gastos','precio_factura','iva_pct'], 'nullable_fields': ['oc_origen_id','propuesta_id'], }, 'ordenes': { 'fields': ['orden_id','oc_id','proyecto_id','tipo_orden','cliente','producto','sku','cantidad','tipo_trabajo','stage', 'fecha_oc','fecha_inicio','fecha_estimada','fecha_recepcion','fecha_entrega','recibio', 'urgente','logo_instrucciones','notas', 'costo_producto','costo_trabajo','costo_logistica','precio_factura', 'check_facturada','check_empacada','check_etiquetas','check_vehiculo', 'grupo_oc','piezas_recibidas','piezas_danadas','nota_recepcion', 'web_ejemplo','web_etiqueta'], 'int_fields': ['cantidad','urgente','check_facturada','check_empacada','check_etiquetas','check_vehiculo','piezas_recibidas','piezas_danadas','oc_id','proyecto_id','web_ejemplo'], 'float_fields': ['costo_producto','costo_trabajo','costo_logistica','precio_factura'], 'nullable_fields': ['oc_id','proyecto_id'], }, 'inventario': { 'fields': ['sku','nombre','descripcion','tipo','talla','color_base','proveedor', 'stock_inicial','punto_reorden','costo_unitario','ultimo_conteo','notas'], 'int_fields': ['stock_inicial','punto_reorden'], 'float_fields': ['costo_unitario'], }, 'productos': { 'fields': ['sku','nombre','categoria','talla','material','tipos_trabajo_disponibles', 'costo_base','proveedor','stock_actual','punto_reorden','activo','notas', 'mostrar_en_web','descripcion_web','medidas'], 'int_fields': ['stock_actual','punto_reorden','activo','mostrar_en_web'], 'float_fields': ['costo_base'], }, 'proyectos': { 'fields': ['nombre','producto_id','producto_nombre','cliente','tipo_trabajo', 'costo_unitario','costo_trabajo','logo_descripcion','logo_archivo', 'foto_terminado','notas','activo','veces_usado','ultimo_uso'], 'int_fields': ['producto_id','activo','veces_usado'], 'float_fields': ['costo_unitario','costo_trabajo'], 'nullable_fields': ['producto_id'], }, 'modelos': { 'fields': ['clave','nombre','descripcion','activo'], 'int_fields': ['activo'], }, 'materiales': { 'fields': ['clave','nombre','tipo','costo_unitario','activo'], 'int_fields': ['activo'], 'float_fields': ['costo_unitario'], }, 'trabajos': { 'fields': ['clave','nombre','costo_base','variable_por','proveedor_default','activo'], 'int_fields': ['activo'], 'float_fields': ['costo_base'], }, 'clientes': { 'fields': ['nombre','tipo','contacto','zona_entrega','costo_entrega','condiciones_pago','notas','activo'], 'int_fields': ['activo'], 'float_fields': ['costo_entrega'], }, 'compras': { 'fields': ['compra_id','proveedor','fecha_compra','fecha_llegada','sku','nombre_producto', 'cantidad','costo_unitario','status','notas'], 'int_fields': ['cantidad'], 'float_fields': ['costo_unitario'], }, 'tareas': { 'fields': ['titulo','descripcion','prioridad','stage','categoria','fecha_limite','asignado','notas'], }, 'catalogos': { 'fields': ['nombre','segmento','cliente_nombre','fecha','items', 'show_prices','show_clientes','show_lead_time','notas','status', 'entrega','minimo_compra','terminos','show_contacto'], 'int_fields': ['show_prices','show_clientes','show_lead_time','show_contacto'], }, 'propuestas': { 'fields': ['numero','cliente_nombre','contacto','empresa','direccion','locacion','tipo_negocio', 'email','telefono','fecha','vigencia_dias','items','iva_pct', 'descuento_pct','notas','status'], 'int_fields': ['vigencia_dias'], 'float_fields': ['iva_pct','descuento_pct'], }, 'bitacora': { 'fields': ['tipo','titulo','descripcion','referencia','fecha'], }, } LOGIN_PAGE = """