import sqlite3 import os import io from flask import Flask, render_template, session, request, redirect, url_for, g, flash, send_file, jsonify from werkzeug.security import generate_password_hash, check_password_hash from functools import wraps from datetime import datetime, timedelta import unicodedata from werkzeug.utils import secure_filename from werkzeug.exceptions import RequestEntityTooLarge app = Flask(__name__) app.secret_key = 'clave-secreta-navbar-app' # Límite máximo de subida: 5 MiB # app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # DATABASE = 'database/pyfleet.db' DATABASE = 'database/auto.db' PASSWORD_HASH = generate_password_hash('123') # Duración de inactividad máxima SESSION_TIMEOUT = timedelta(hours=4) UPLOAD_FOLDER = 'uploads/documentos' ALLOWED_EXTENSIONS = {'pdf'} # ── Auth ───────────────────────────────────────────────────── def login_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get('autenticado'): return redirect(url_for('login')) # Comprobar timeout de inactividad # ultima_actividad = session.get('ultima_actividad') # if ultima_actividad: # ultima = datetime.fromisoformat(ultima_actividad) # if datetime.now() - ultima > SESSION_TIMEOUT: # session.clear() # flash('Tu sesión ha expirado por inactividad. Vuelve a iniciar sesión.', 'error') # return redirect(url_for('login')) # # Actualizar timestamp de última actividad # session['ultima_actividad'] = datetime.now().isoformat() return f(*args, **kwargs) return decorated @app.route('/api/session') def check_session(): from flask import jsonify if session.get('autenticado'): # Actualizar timestamp session['ultima_actividad'] = datetime.now().isoformat() return jsonify({'activa': True}) return jsonify({'activa': False}), 401 @app.route('/login', methods=['GET', 'POST']) def login(): session["animate_navbar"] = True error = None if request.method == 'POST': pwd = request.form.get('password', '') if check_password_hash(PASSWORD_HASH, pwd): session['autenticado'] = True return redirect(url_for('vehiculos_list')) error = 'Contraseña incorrecta. Inténtalo de nuevo.' return render_template('login.html', error=error) @app.route('/logout') def logout(): session.clear() return redirect(url_for('login')) # ── Funcion normalizar ─────────────────────────────────────────── def normalizar(texto): # if texto is None: # return '' texto = str(texto).strip().lower() # Quitar tildes texto = unicodedata.normalize('NFD', texto) texto = ''.join( c for c in texto if unicodedata.category(c) != 'Mn' ) texto = ' '.join(texto.split()) # Convertir varios espacios seguidos en uno return texto # ── DB helpers ──────────────────────────────────────────────── def get_db(): if 'db' not in g: g.db = sqlite3.connect(DATABASE) g.db.create_function("NORM", 1, normalizar) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(e=None): db = g.pop('db', None) if db: db.close() def init_db(): db = sqlite3.connect(DATABASE) db.create_function("NORM", 1, normalizar) # db.commit() # db.close() # ... tablas existentes ... db.execute(''' CREATE TABLE IF NOT EXISTS documentos ( id INTEGER PRIMARY KEY AUTOINCREMENT, vehiculo_id INTEGER NOT NULL REFERENCES vehiculos(id) ON DELETE CASCADE, nombre TEXT, filename TEXT NOT NULL, fecha DATETIME DEFAULT (datetime('now','localtime')) ) ''') db.commit() db.close() # ── Rutas generales ─────────────────────────────────────────── @app.route('/', methods=['GET', 'POST']) @login_required def index(): if request.method == 'POST': texto = request.form.get('mensaje', '').strip() if texto: get_db().execute('INSERT INTO mensajes (texto) VALUES (?)', (texto,)) get_db().commit() return redirect(url_for('index')) return render_template('index.html', page='inicio') @app.route('/explorar', methods=['GET', 'POST']) @login_required def explorar(): if request.method == 'POST': texto = request.form.get('mensaje', '').strip() if texto: get_db().execute('INSERT INTO mensajes (texto) VALUES (?)', (texto,)) get_db().commit() return redirect(url_for('explorar')) return render_template('explorar.html', page='explorar') @app.route('/contacto', methods=['GET', 'POST']) @login_required def contacto(): if request.method == 'POST': texto = request.form.get('mensaje', '').strip() if texto: get_db().execute('INSERT INTO mensajes (texto) VALUES (?)', (texto,)) get_db().commit() return redirect(url_for('contacto')) return render_template('contacto.html', page='contacto') @app.route('/limpiar') @login_required def limpiar(): ref = request.referrer or url_for('index') return redirect(ref) @app.context_processor def inject_globals(): from datetime import date mensajes = get_db().execute( 'SELECT * FROM mensajes ORDER BY fecha DESC' ).fetchall() return { 'now': date.today().isoformat(), 'mensajes': mensajes } @app.route('/mensajes//eliminar', methods=['POST']) @login_required def mensaje_eliminar(id): get_db().execute('DELETE FROM mensajes WHERE id=?', (id,)) get_db().commit() return redirect(request.referrer or url_for('index')) @app.route('/mensajes/eliminar_todos', methods=['POST']) @login_required def mensajes_eliminar_todos(): get_db().execute('DELETE FROM mensajes') get_db().commit() return redirect(request.referrer or url_for('index')) # ── CRUD Vehículos ──────────────────────────────────────────── @app.route('/vehiculos') @login_required def vehiculos_list(): db = get_db() q = request.args.get('q', '').strip() solo_filas = request.args.get('solo_filas') == '1' incl_bajas = request.args.get('incl_bajas', '0') == '1' # Si viene incl_bajas en la URL, actualizamos la sesión if 'incl_bajas' in request.args: incl_bajas = request.args.get('incl_bajas') == '1' session['incl_bajas'] = incl_bajas else: # Si no viene en la URL, recuperamos el último estado incl_bajas = session.get('incl_bajas', False) terminos = [t.strip() for t in q.split(',') if t.strip()] if q else [] # Filtro base de bajas filtro_bajas = '' if incl_bajas else "AND (situacion IS NULL OR UPPER(situacion) != 'BAJA')" if terminos: condiciones = [] params = [] for t in terminos: t_norm = normalizar(t) if t_norm == 'lanzadestellos': condiciones.append('lanzadestellos = 1') continue like = f'%{t_norm}%' condiciones.append('''( NORM(m_oficial) LIKE ? OR NORM(m_reservada) LIKE ? OR NORM(VIN) LIKE ? OR NORM(marca) LIKE ? OR NORM(modelo) LIKE ? OR NORM(poblacion) LIKE ? OR NORM(unidad) LIKE ? OR NORM(tipo) LIKE ? OR NORM(situacion) LIKE ? )''') params.extend([like] * 9) where = ' AND '.join(condiciones) rows = db.execute( f'''SELECT id, m_oficial, m_reservada, marca, modelo, poblacion, unidad, tipo, situacion, lanzadestellos FROM vehiculos WHERE {where} {filtro_bajas} ORDER BY id''', params ).fetchall() else: # PARA QUE LA LISTA SALGA EN BLANCO INICIALMENTE rows = [] # else: # rows = db.execute( # '''SELECT id, m_oficial, m_reservada, marca, modelo, poblacion, unidad, tipo, tipo, situacion # FROM vehiculos # WHERE situacion IS NULL OR UPPER(situacion) != 'BAJA' # ORDER BY id''' # ).fetchall() if solo_filas: return render_template('vehiculos/rows.html', rows=rows, q=q, incl_bajas=incl_bajas) return render_template('vehiculos/list.html', page='vehiculos', rows=rows, q=q, incl_bajas=incl_bajas) @app.route('/vehiculos/nuevo', methods=['GET', 'POST']) @login_required def vehiculos_nuevo(): db = get_db() tipo = db.execute('SELECT * FROM tipo ORDER BY tipo').fetchall() unidades = db.execute('SELECT * FROM unidades ORDER BY unidad').fetchall() poblaciones = db.execute('SELECT * FROM poblaciones ORDER BY poblacion').fetchall() vehiculo = None if request.method == 'POST': f = request.form lanzadestellos = 1 if f.get('lanzadestellos') else 0 cursor = db.execute(''' INSERT INTO vehiculos ( m_oficial, m_reservada, m_propia, VIN, poliza, viat, legajo, marca, modelo, tipo, color, combustible, plazas, lanzadestellos ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) ''', ( f.get('m_oficial'), f.get('m_reservada'), f.get('m_propia'), f.get('VIN'), f.get('poliza'), f.get('viat'), f.get('legajo') or None, f.get('marca'), f.get('modelo'), f.get('tipo'), f.get('color'), f.get('combustible'), f.get('plazas'), lanzadestellos )) # nuevo_id = cursor.lastrowid db.commit() flash('Vehículo registrado correctamente.', 'ok') return redirect(url_for('vehiculos_list')) return render_template('vehiculos/form.html', page='vehiculos', vehiculo=vehiculo, tipo=tipo, unidades=unidades, poblaciones=poblaciones) @app.route('/vehiculos//editar', methods=['GET', 'POST']) @login_required def vehiculos_editar(id): db = get_db() vehiculo = db.execute('SELECT * FROM vehiculos WHERE id=?', (id,)).fetchone() tipo = db.execute('SELECT * FROM tipo ORDER BY tipo').fetchall() poblaciones = db.execute('SELECT * FROM poblaciones ORDER BY poblacion').fetchall() unidades = db.execute('SELECT * FROM unidades ORDER BY unidad').fetchall() historial = db.execute( ''' SELECT h.*, v.m_oficial FROM historial h JOIN vehiculos v ON h.vehiculo_id = v.id WHERE h.vehiculo_id = ? ORDER BY h.id DESC ''', (vehiculo['id'],) ).fetchall() if vehiculo else [] # Recoger parámetros GET q = request.args.get('q', '') desde = request.args.get('desde', '') hasta = request.args.get('hasta', '') q_itv = request.args.get('q_itv', '') origen = request.args.get('origen', '') q_viat = request.args.get('q_viat', '') if not vehiculo: flash('Vehículo no encontrado.', 'error') return redirect(url_for('vehiculos_list')) if request.method == 'POST': # Recoger parámetros del POST (campos ocultos) q = request.form.get('_q', '') desde = request.form.get('_desde', '') hasta = request.form.get('_hasta', '') q_itv = request.args.get('q_itv', '') f = request.form origen = request.form.get('_origen', '') q_viat = request.form.get('_q_viat', '') lanzadestellos = 1 if f.get('lanzadestellos') else 0 db.execute(''' UPDATE vehiculos SET m_oficial=?, m_reservada=?, m_propia=?, VIN=?, poliza=?, viat=?, legajo=?, marca=?, modelo=?, tipo=?, color=?, combustible=?, plazas=?, lanzadestellos=?, situacion=?, poblacion=?, unidad=?, int_operacion=?, int_grupo=?, int_juzgado=?, int_procedimiento=?, km=?, km_1=?, itv_ultima=?, itv=?, notas=? WHERE id=? ''', ( f.get('m_oficial'), f.get('m_reservada'), f.get('m_propia'), f.get('VIN'), f.get('poliza'), f.get('viat') or '', f.get('legajo') or None, f.get('marca'), f.get('modelo'), f.get('tipo'), f.get('color'), f.get('combustible'), f.get('plazas'), lanzadestellos, f.get('situacion'), f.get('poblacion'), f.get('unidad'), f.get('int_operacion'), f.get('int_grupo'), f.get('int_juzgado'), f.get('int_procedimiento'), f.get('km') or None, f.get('km_1') or None, f.get('itv_ultima') or None, f.get('itv') or None, f.get('notas'), id )) db.commit() flash('Vehículo actualizado.', 'ok') # Volver a ITV si venía de ahí if desde or hasta or q_itv: return redirect(f'/itv?desde={desde}&hasta={hasta}&q_itv={q_itv}') elif origen == 'viat': return redirect(f'/viat?q={q_viat}') return redirect(url_for('vehiculos_list', q=q) if q else url_for('vehiculos_list')) documentos = db.execute( 'SELECT * FROM documentos WHERE vehiculo_id=? ORDER BY fecha DESC', (id,) ).fetchall() matricula = vehiculo['m_oficial'] or vehiculo['m_reservada'] or '' return render_template('vehiculos/form.html', page='vehiculos', vehiculo=vehiculo, tipo=tipo, poblaciones=poblaciones, unidades=unidades, historial=historial, matricula=matricula, q=q, desde=desde, hasta=hasta, q_itv=q_itv, origen=origen, q_viat=q_viat, documentos=documentos) @app.route('/vehiculos//eliminar', methods=['POST']) @login_required def vehiculos_eliminar(id): db = get_db() db.execute('DELETE FROM vehiculos WHERE id=?', (id,)) db.commit() flash('Vehículo eliminado.', 'ok') q = request.form.get('_q', '') return redirect(url_for('vehiculos_list', q=q) if q else url_for('vehiculos_list')) @app.route('/vehiculos//historial/nuevo', methods=['POST']) @login_required def historial_nuevo(vid): db = get_db() # Recoger TODOS los parámetros de navegación del POST q = request.form.get('_q', '') desde = request.form.get('_desde', '') hasta = request.form.get('_hasta', '') q_itv = request.form.get('_q_itv', '') origen = request.form.get('_origen', '') q_viat = request.form.get('_q_viat', '') # ← POST no GET # Datos del historial h_fecha = request.form.get('h_fecha') or None h_vehiculo_id = request.form.get('h_vehiculo_id', '').strip() h_matricula = request.form.get('h_matricula', '').strip() h_situacion = request.form.get('h_situacion', '').strip() h_poblacion_id = request.form.get('h_poblacion', '').strip() h_unidad = request.form.get('h_unidad', '').strip() h_grupo = request.form.get('h_grupo', '').strip() h_observaciones = request.form.get('h_observaciones', '').strip() # Obtener nombre de población poblacion_nombre = '' if h_poblacion_id: row = db.execute('SELECT poblacion FROM poblaciones WHERE id=?', (h_poblacion_id,)).fetchone() if row: poblacion_nombre = row['poblacion'] db.execute(''' INSERT INTO historial (fecha, vehiculo_id, matricula, situacion, poblacion, unidad, grupo, observaciones) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', (h_fecha, h_vehiculo_id, h_matricula, h_situacion, poblacion_nombre, h_unidad, h_grupo, h_observaciones)) # Actualizar situacion del vehiculo if h_situacion.upper() == 'BAJA': db.execute("UPDATE vehiculos SET situacion='BAJA', poblacion='', unidad='' WHERE id=?", (vid,)) else: db.execute( "UPDATE vehiculos SET situacion=?, poblacion=?, unidad=?, grupo=? WHERE id=?", (h_situacion, poblacion_nombre, h_unidad, h_grupo, vid) ) db.commit() flash('Entrada añadida al historial.', 'ok') # Redirect según origen if origen == 'viat': return redirect(f'/vehiculos/{vid}/editar?origen=viat&q_viat={q_viat}') if desde or hasta or q_itv: return redirect(f'/vehiculos/{vid}/editar?desde={desde}&hasta={hasta}&q_itv={q_itv}') return redirect(url_for('vehiculos_editar', id=vid, q=q)) @app.route('/vehiculos//historial//eliminar', methods=['POST']) @login_required def historial_eliminar(vid, hid): q = request.form.get('_q', '') desde = request.form.get('_desde', '') hasta = request.form.get('_hasta', '') db = get_db() # Recoger parámetros de navegación q = request.form.get('_q', '') desde = request.form.get('_desde', '') hasta = request.form.get('_hasta', '') q_itv = request.form.get('_q_itv', '') origen = request.form.get('_origen', '') q_viat = request.form.get('_q_viat', '') # ← POST no GET # 1. Comprobar que el vehículo existe vehiculo = db.execute(''' SELECT id, m_oficial FROM vehiculos WHERE id = ? ''', (vid,)).fetchone() if not vehiculo: flash('El vehículo no existe.', 'error') return redirect(url_for('vehiculos')) # 2. Comprobar que el registro de historial pertenece realmente a este vehículo asiento = db.execute(''' SELECT id, vehiculo_id, situacion FROM historial WHERE id = ? AND vehiculo_id = ? ''', (hid, vid)).fetchone() if not asiento: flash('El registro de historial no existe o no pertenece a este vehículo.', 'error') return redirect(url_for('vehiculo_historial', vid=vid)) # 3. Eliminar el registro db.execute(''' DELETE FROM historial WHERE id = ? AND vehiculo_id = ? ''', (hid, vid)) # 4. Buscar el ÚLTIMO registro que queda para este vehículo ultimo = db.execute(''' SELECT id, situacion, poblacion, unidad FROM historial WHERE vehiculo_id = ? ORDER BY id DESC LIMIT 1 ''', (vid,)).fetchone() # 5. Si queda historial, actualizar vehiculos con los datos del último registro if ultimo: situacion = (ultimo['situacion'] or '').strip().upper() poblacion = (ultimo['poblacion'] or '').strip() unidad = (ultimo['unidad'] or '').strip() db.execute(''' UPDATE vehiculos SET situacion = ?, poblacion = ?, unidad = ? WHERE id = ? ''', ( situacion, poblacion, unidad, vid )) # 6. Si no queda ningún historial, limpiar el vehículo else: db.execute(''' UPDATE vehiculos SET situacion = NULL, poblacion = '', unidad = '' WHERE id = ? ''', (vid,)) db.commit() flash('Entrada eliminada del historial.', 'ok') # Redirect según origen if origen == 'viat': return redirect(f'/vehiculos/{vid}/editar?origen=viat&q_viat={q_viat}') if desde or hasta or q_itv: return redirect(f'/vehiculos/{vid}/editar?desde={desde}&hasta={hasta}&q_itv={q_itv}') url = f'/vehiculos/{vid}/editar' if q: url += f'?q={q}' return redirect(url) @app.route('/vehiculos/exportar') @login_required def vehiculos_exportar(): import io, openpyxl from openpyxl.styles import Font, PatternFill, Alignment from flask import send_file db = get_db() q = request.args.get('q', '').strip() incl_bajas = request.args.get('incl_bajas', '0') == '1' filtro_bajas = '' if incl_bajas else "AND (situacion IS NULL OR UPPER(situacion) != 'BAJA')" terminos = [t.strip() for t in q.split(',') if t.strip()] if q else [] # terminos = [t.strip() for t in q.split(',') if t.strip()] if q else [] # Filtro base de bajas filtro_bajas = '' if incl_bajas else "AND (situacion IS NULL OR UPPER(situacion) != 'BAJA')" if terminos: condiciones = [] params = [] for t in terminos: t_norm = normalizar(t) if t_norm == 'lanzadestellos': condiciones.append('lanzadestellos = 1') continue like = f'%{t_norm}%' condiciones.append('''( NORM(m_oficial) LIKE ? OR NORM(m_reservada) LIKE ? OR NORM(VIN) LIKE ? OR NORM(marca) LIKE ? OR NORM(modelo) LIKE ? OR NORM(poblacion) LIKE ? OR NORM(unidad) LIKE ? OR NORM(denominacion) LIKE ? OR NORM(situacion) LIKE ? )''') params.extend([like] * 9) where = ' AND '.join(condiciones) rows = db.execute( f'''SELECT id, m_oficial, m_reservada, marca, modelo, poblacion, unidad, denominacion, tipo, situacion, lanzadestellos FROM vehiculos WHERE {where} {filtro_bajas} ORDER BY id''', params ).fetchall() else: rows = db.execute( '''SELECT m_oficial, m_reservada, marca, modelo, poblacion, unidad, grupo, tipo, situacion FROM vehiculos WHERE situacion IS NULL OR UPPER(situacion) != 'BAJA' ORDER BY id''' ).fetchall() wb = openpyxl.Workbook() ws = wb.active ws.title = 'Vehículos' headers = ['M. Oficial', 'M. Reservada', 'Marca', 'Modelo', 'Poblacion', 'Unidad', 'Grupo', 'Denominación', 'Situación' ] header_fill = PatternFill('solid', fgColor='5C6EF8') header_font = Font(bold=True, color='FFFFFF', size=11) for col, h in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=h) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center') ws.row_dimensions[1].height = 22 for row in rows: ws.append(list(row)) col_widths = [8, 14, 14, 16, 16, 24, 16, 16, 18, 10, 14] for i, w in enumerate(col_widths, 1): ws.column_dimensions[ws.cell(1, i).column_letter].width = w zebra = PatternFill('solid', fgColor='EEF0F8') for row_idx in range(2, ws.max_row + 1): if row_idx % 2 == 0: for col_idx in range(1, len(headers) + 1): ws.cell(row_idx, col_idx).fill = zebra buf = io.BytesIO() wb.save(buf) buf.seek(0) nombre_q = '_'.join(terminos).replace(' ', '-') if terminos else '' filename = f'vehiculos{"_" + nombre_q if nombre_q else ""}.xlsx' return send_file(buf, as_attachment=True, download_name=filename, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') # UPLOADS FICHAS TECNICAS PDF def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS # @app.errorhandler(RequestEntityTooLarge) # def handle_file_too_large(e): # flash('Error: El archivo pdf no puede superar los 5 MB.', 'error') # return redirect(request.referrer or '/') @app.route('/vehiculos//documentos/upload', methods=['POST']) @login_required def documentos_upload(id): q = request.form.get('_q', '') incl_bajas = request.form.get('_incl_bajas', '0') desde = request.form.get('_desde', '') hasta = request.form.get('_hasta', '') q_itv = request.form.get('_q_itv', '') origen = request.form.get('_origen', '') q_viat = request.form.get('_q_viat', '') nombre = request.form.get('nombre_doc', '').strip() + '.pdf' if 'pdf' not in request.files: flash('No se seleccionó ningún archivo.', 'error') return redirect(f'/vehiculos/{id}/editar') file = request.files['pdf'] if file.filename == '' or not allowed_file(file.filename): flash('Solo se permiten archivos PDF.', 'error') return redirect(f'/vehiculos/{id}/editar') # Comprobar tamaño máximo: 5 MiB MAX_FILE_SIZE = 5 * 1024 * 1024 file.seek(0, os.SEEK_END) file_size = file.tell() file.seek(0) if file_size > MAX_FILE_SIZE: flash('El archivo PDF no puede superar los 5 MB.', 'error') return redirect(f'/vehiculos/{id}/editar?q={q}') os.makedirs(UPLOAD_FOLDER, exist_ok=True) # Nombre único: vehiculo_id_timestamp.pdf import time filename = f'v{id}_{int(time.time())}.pdf' file.save(os.path.join(UPLOAD_FOLDER, filename)) # Si no puso nombre, usar el nombre original del archivo if not nombre: nombre = secure_filename(file.filename) db = get_db() db.execute( 'INSERT INTO documentos (vehiculo_id, nombre, filename) VALUES (?,?,?)', (id, nombre, filename) ) db.commit() flash(f'Documento «{nombre}» subido correctamente.', 'ok') url = f'/vehiculos/{id}/editar?q={q}' if origen == 'viat': url = f'/vehiculos/{id}/editar?origen=viat&q_viat={q_viat}' elif desde or hasta or q_itv: url = f'/vehiculos/{id}/editar?desde={desde}&hasta={hasta}&q_itv={q_itv}' return redirect(url) @app.route('/vehiculos/documentos//ver') @login_required def documentos_ver(doc_id): from flask import send_from_directory db = get_db() doc = db.execute('SELECT * FROM documentos WHERE id=?', (doc_id,)).fetchone() if not doc: flash('Documento no encontrado.', 'error') return redirect(url_for('vehiculos_list')) return send_from_directory(UPLOAD_FOLDER, doc['filename'], as_attachment=False) @app.route('/vehiculos/documentos//descargar') @login_required def documentos_descargar(doc_id): from flask import send_from_directory db = get_db() doc = db.execute('SELECT * FROM documentos WHERE id=?', (doc_id,)).fetchone() if not doc: flash('Documento no encontrado.', 'error') return redirect(url_for('vehiculos_list')) return send_from_directory(UPLOAD_FOLDER, doc['filename'], as_attachment=True, download_name=doc['nombre']) # download_name=doc['nombre'] + '.pdf' @app.route('/vehiculos/documentos//eliminar', methods=['POST']) @login_required def documentos_eliminar(doc_id): db = get_db() vid = request.form.get('vid', '') q = request.form.get('_q', '') incl_bajas = request.form.get('_incl_bajas', '0') doc = db.execute('SELECT * FROM documentos WHERE id=?', (doc_id,)).fetchone() if doc: filepath = os.path.join(UPLOAD_FOLDER, doc['filename']) if os.path.exists(filepath): os.remove(filepath) db.execute('DELETE FROM documentos WHERE id=?', (doc_id,)) db.commit() flash('Documento eliminado.', 'ok') return redirect(f'/vehiculos/{vid}/editar?q={q}&incl_bajas={incl_bajas}') # Ruta API que devuelve unidades según población @app.route('/api/unidades/') @login_required def api_unidades(poblacion_id): from flask import jsonify db = get_db() rows = db.execute( 'SELECT id, unidad FROM unidades WHERE poblacion_id=? ORDER BY unidad', (poblacion_id,) ).fetchall() return jsonify([{'id': r['id'], 'unidad': r['unidad']} for r in rows]) @app.route('/itv') @login_required def itv_list(): db = get_db() desde = request.args.get('desde', '').strip() hasta = request.args.get('hasta', '').strip() q_itv = request.args.get('q_itv', '').strip() rows = [] buscado = False if desde or hasta or q_itv: buscado = True desde_q = desde or '1900-01-01' hasta_q = hasta or '2999-12-31' terminos = [t.strip() for t in q_itv.split(',') if t.strip()] if q_itv else [] if terminos: condiciones = [] params = [desde_q, hasta_q] for t in terminos: like = f'%{t}%' condiciones.append('''( poblacion LIKE ? OR unidad LIKE ? OR tipo LIKE ? )''') params.extend([like] * 3) where_q = ' AND '.join(condiciones) rows = db.execute(f''' SELECT id, m_oficial, m_reservada, marca, modelo, tipo, tipo, unidad, poblacion, itv FROM vehiculos WHERE situacion !='BAJA' AND itv IS NOT NULL AND itv != '' AND itv BETWEEN ? AND ? AND {where_q} ORDER BY itv ASC ''', params).fetchall() else: rows = db.execute(''' SELECT id, m_oficial, m_reservada, marca, modelo, tipo, tipo, unidad, poblacion, itv FROM vehiculos WHERE situacion !='BAJA' AND itv IS NOT NULL AND itv != '' AND itv BETWEEN ? AND ? ORDER BY itv ASC ''', (desde_q, hasta_q)).fetchall() return render_template('itv/list.html', page='itv', rows=rows, desde=desde, hasta=hasta, q_itv=q_itv, buscado=buscado) @app.route('/itv/exportar') @login_required def itv_exportar(): import io, openpyxl from openpyxl.styles import Font, PatternFill, Alignment from flask import send_file db = get_db() desde = request.args.get('desde', '').strip() hasta = request.args.get('hasta', '').strip() q_itv = request.args.get('q_itv', '').strip() desde_q = desde or '1900-01-01' hasta_q = hasta or '2999-12-31' terminos = [t.strip() for t in q_itv.split(',') if t.strip()] if q_itv else [] if terminos: condiciones = [] params = [desde_q, hasta_q] for t in terminos: like = f'%{t}%' condiciones.append('''( poblacion LIKE ? OR unidad LIKE ? OR tipo LIKE ? )''') params.extend([like] * 3) where_q = ' AND '.join(condiciones) rows = db.execute(f''' SELECT m_oficial, m_reservada, marca, modelo, tipo, tipo, unidad, poblacion, itv FROM vehiculos WHERE situacion !='BAJA' AND itv IS NOT NULL AND itv != '' AND itv BETWEEN ? AND ? AND {where_q} ORDER BY itv ASC ''', params).fetchall() else: rows = db.execute(''' SELECT m_oficial, m_reservada, marca, modelo, tipo, tipo, unidad, poblacion, itv FROM vehiculos WHERE situacion !='BAJA' AND itv IS NOT NULL AND itv != '' AND itv BETWEEN ? AND ? ORDER BY itv ASC ''', (desde_q, hasta_q)).fetchall() wb = openpyxl.Workbook() ws = wb.active ws.title = 'ITV' headers = ['M. Oficial', 'M. Reservada', 'Marca', 'Modelo', 'Denominación', 'Tipo', 'Unidad', 'Población', 'Fecha ITV'] header_fill = PatternFill('solid', fgColor='5C6EF8') header_font = Font(bold=True, color='FFFFFF', size=11) for col, h in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=h) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center') ws.row_dimensions[1].height = 22 for row in rows: ws.append(list(row)) col_widths = [8, 14, 14, 16, 16, 24, 14, 18, 18, 14] for i, w in enumerate(col_widths, 1): ws.column_dimensions[ws.cell(1, i).column_letter].width = w zebra = PatternFill('solid', fgColor='EEF0F8') for row_idx in range(2, ws.max_row + 1): if row_idx % 2 == 0: for col_idx in range(1, len(headers) + 1): ws.cell(row_idx, col_idx).fill = zebra buf = io.BytesIO() wb.save(buf) buf.seek(0) nombre_q = '_'.join(terminos).replace(' ', '-') if terminos else '' filename = f'itv_{desde}__{hasta}{"_" + nombre_q if nombre_q else ""}.xlsx' return send_file(buf, as_attachment=True, download_name=filename, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') # ── CRUD Poblaciones ────────────────────────────────────────── @app.route('/poblaciones') @login_required def poblaciones_list(): db = get_db() rows = db.execute('SELECT * FROM poblaciones ORDER BY poblacion').fetchall() return render_template('tablas/poblaciones.html', page='poblaciones', rows=rows) @app.route('/poblaciones/nueva', methods=['POST']) @login_required def poblaciones_nueva(): nombre = request.form.get('poblacion', '').strip() if nombre: try: get_db().execute('INSERT INTO poblaciones (poblacion) VALUES (?)', (nombre,)) get_db().commit() flash(f'Población «{nombre}» añadida.', 'ok') except: flash('Esa población ya existe.', 'error') return redirect(url_for('poblaciones_list')) @app.route("/editar_poblaciones", methods=["POST"]) def editar_poblaciones(): print(request.form) accion = request.form.get("action") db = get_db() cur = db.cursor() if accion == "edit": id = request.form["id"] poblacion = request.form.get("poblacion") cur.execute("""UPDATE poblaciones SET poblacion=? WHERE id=?""",(poblacion, id)) db.commit() flash(f'Poblacion «{poblacion}» modificada.', 'ok') elif accion == "delete": id = request.form["id"] cur.execute("DELETE FROM poblaciones WHERE id=?",(id,)) db.commit() flash(f'Poblacion eliminada.', 'ok') db.close() return jsonify({ "action": accion, "id": request.form["id"] }) # @app.route('/poblaciones//eliminar', methods=['POST']) # @login_required # def poblaciones_eliminar(id): # db = get_db() # db.execute('DELETE FROM poblaciones WHERE id=?', (id,)) # db.commit() # flash('Población eliminada.', 'ok') # ── CRUD Unidades ───────────────────────────────────────────── @app.route('/unidades') @login_required def unidades_list(): db = get_db() rows = db.execute(''' SELECT u.*, p.poblacion FROM unidades u LEFT JOIN poblaciones p ON u.poblacion_id = p.id ORDER BY p.poblacion, u.unidad ''').fetchall() poblaciones = db.execute('SELECT * FROM poblaciones ORDER BY poblacion').fetchall() return render_template('tablas/unidades.html', page='unidades', rows=rows, poblaciones=poblaciones) @app.route('/unidades/nueva', methods=['POST']) @login_required def unidades_nueva(): unidad = request.form.get('unidad', '').strip() poblacion_id = request.form.get('poblacion_id', '').strip() if unidad and poblacion_id: get_db().execute( 'INSERT INTO unidades (unidad, poblacion_id) VALUES (?,?)', (unidad, poblacion_id) ) get_db().commit() flash(f'Unidad «{unidad}» añadida.', 'ok') return redirect(url_for('unidades_list')) @app.route("/editar_unidades", methods=["POST"]) def editar_unidades(): print(request.form) accion = request.form.get("action") db = get_db() cur = db.cursor() if accion == "edit": id = request.form["id"] poblacion = request.form.get("poblacion") unidad = request.form.get("unidad") fila = db.execute( 'SELECT id FROM poblaciones WHERE poblacion=?', (poblacion,) ).fetchone() if fila is None: return jsonify({"error": "La población no existe"}), 400 poblacion_id = fila["id"] cur.execute( """UPDATE unidades SET poblacion_id=?, unidad=? WHERE id=?""", (poblacion_id, unidad, id) ) db.commit() flash(f'Unidad «{unidad}» modificada.', 'ok') elif accion == "delete": id = request.form["id"] cur.execute("DELETE FROM unidades WHERE id=?",(id,)) db.commit() flash(f'Unidad eliminada.', 'ok') db.close() return jsonify({ "action": accion, "id": request.form["id"] }) # @app.route('/unidades//eliminar', methods=['POST']) # @login_required # def unidades_eliminar(id): # db = get_db() # db.execute('DELETE FROM unidades WHERE id=?', (id,)) # db.commit() # flash('Unidad eliminada.', 'ok') # return redirect(url_for('unidades_list')) ############### API PARA USAR TABULATOR ###################################### # @app.route("/api/unidades/", methods=["PATCH"]) # @login_required # def actualizar_unidad(unidad_id): # datos = request.get_json(silent=True) or {} # campo = datos.get("campo") # valor = datos.get("valor") # # Solo permitimos estos campos # if campo not in ("poblacion_id", "unidad"): # return jsonify({ # "error": "Campo no permitido" # }), 400 # db = get_db() # # Comprobar que existe la unidad # unidad_actual = db.execute( # "SELECT id FROM unidades WHERE id=?", # (unidad_id,) # ).fetchone() # if unidad_actual is None: # return jsonify({ # "error": "La unidad no existe" # }), 404 # # CAMBIO DE POBLACIÓN # if campo == "poblacion_id": # try: # poblacion_id = int(valor) # except (ValueError, TypeError): # return jsonify({ # "error": "Población no válida" # }), 400 # poblacion = db.execute( # """ SELECT id, poblacion FROM poblaciones WHERE id=? """, # (poblacion_id,) # ).fetchone() # if poblacion is None: # return jsonify({ # "error": "La población no existe" # }), 400 # valor = poblacion_id # # CAMBIO DE UNIDAD # elif campo == "unidad": # valor = str(valor or "").strip() # if not valor: # return jsonify({ # "error": "El nombre de la unidad no puede estar vacío" # }), 400 # # UPDATE # db.execute(f""" UPDATE unidades SET {campo}=? WHERE id=?""", # (valor, unidad_id) # ) # db.commit() # # DEVOLVER EL REGISTRO ACTUALIZADO # fila = db.execute( # """ # SELECT # u.id, # u.poblacion_id, # u.unidad, # p.poblacion # FROM unidades u # LEFT JOIN poblaciones p # ON u.poblacion_id = p.id # WHERE u.id=? """, # (unidad_id,) # ).fetchone() # return jsonify({ # "id": fila["id"], # "poblacion_id": fila["poblacion_id"], # "poblacion": fila["poblacion"], # "unidad": fila["unidad"] # }) # ── CRUD tipo ───────────────────────────────────────── @app.route('/tipo') @login_required def tipo_list(): db = get_db() rows = db.execute('SELECT * FROM tipo ORDER BY tipo').fetchall() return render_template('tablas/tipo.html', page='tipo', rows=rows) @app.route('/tipo/nueva', methods=['POST']) @login_required def tipo_nueva(): tipo = request.form.get('tipo', '').strip() literal = request.form.get('literal', '').strip() if tipo: try: get_db().execute( 'INSERT INTO tipo (tipo) VALUES (?)', (tipo) ) get_db().commit() flash(f'Tipo vehículo «{tipo}» añadido.', 'ok') except: flash('Ese tipo ya existe.', 'error') return redirect(url_for('tipo_list')) @app.route("/editar_tipo", methods=["POST"]) def editar_tipo(): print(request.form) accion = request.form.get("action") db = get_db() cur = db.cursor() if accion == "edit": id = request.form["id"] tipo = request.form.get("tipo") cur.execute("UPDATE tipo SET tipo=? WHERE id=?", (tipo, id) ) db.commit() flash(f'Tipo vehículo «{tipo}» modificado.', 'ok') elif accion == "delete": id = request.form["id"] cur.execute("DELETE FROM tipo WHERE id=?",(id,)) db.commit() flash(f'Tipo vehículo eliminado.', 'ok') db.close() return jsonify({ "action": accion, "id": request.form["id"] }) # @app.route('/tipo//eliminar', methods=['POST']) # @login_required # def tipo_eliminar(id): # db = get_db() # db.execute('DELETE FROM tipo WHERE id=?', (id,)) # db.commit() # flash('Denominación eliminada.', 'ok') # return redirect(url_for('tipo_list')) # ── TEMPLATE VIA-T ────────────────────────────────────────────── @app.route('/viat') @login_required def viat_list(): db = get_db() q = request.args.get('q', '').strip() solo_filas = request.args.get('solo_filas') == '1' terminos = [t.strip() for t in q.split(',') if t.strip()] if q else [] sql = ''' SELECT id, viat, m_oficial, m_reservada, marca, modelo, poblacion, unidad, tipo FROM vehiculos WHERE viat IS NOT NULL AND viat != '' ''' params = [] if terminos: condiciones = [] for termino in terminos: condiciones.append(''' ( viat LIKE ? OR m_oficial LIKE ? OR m_reservada LIKE ? OR marca LIKE ? OR modelo LIKE ? OR poblacion LIKE ? OR unidad LIKE ? OR tipo LIKE ? ) ''') like = f'%{termino}%' params.extend([like] * 8) sql += ' AND ' + ' AND '.join(condiciones) sql += ' ORDER BY id' rows = db.execute(sql, params).fetchall() if solo_filas: return render_template('viat/rows.html', rows=rows, q=q) return render_template('viat/list.html', page='viat', rows=rows, q=q) @app.route('/viat/exportar') @login_required def viat_exportar(): import io, openpyxl from openpyxl.styles import Font, PatternFill, Alignment from flask import send_file db = get_db() q = request.args.get('q', '').strip() terminos = [t.strip() for t in q.split(',') if t.strip()] if q else [] sql = ''' SELECT id, viat, m_oficial, m_reservada, marca, modelo, poblacion, unidad, tipo FROM vehiculos WHERE viat IS NOT NULL AND viat != '' ''' params = [] if terminos: condiciones = [] for termino in terminos: condiciones.append(''' ( viat LIKE ? OR m_oficial LIKE ? OR m_reservada LIKE ? OR marca LIKE ? OR modelo LIKE ? OR poblacion LIKE ? OR unidad LIKE ? OR tipo LIKE ? ) ''') like = f'%{termino}%' params.extend([like] * 8) sql += ' AND ' + ' AND '.join(condiciones) sql += ' ORDER BY id' rows = db.execute(sql, params).fetchall() wb = openpyxl.Workbook() ws = wb.active ws.title = 'Via-T' headers = ['Via-T', 'M. Oficial', 'M. Reservada', 'Marca', 'Modelo', 'Población', 'Unidad', 'Tipo'] header_fill = PatternFill('solid', fgColor='5C6EF8') header_font = Font(bold=True, color='FFFFFF', size=11) for col, h in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=h) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center') ws.row_dimensions[1].height = 22 for row in rows: ws.append([row['viat'], row['m_oficial'], row['m_reservada'], row['marca'], row['modelo'], row['poblacion'], row['unidad'], row['tipo']]) col_widths = [16, 14, 14, 16, 16, 18, 18, 14] for i, w in enumerate(col_widths, 1): ws.column_dimensions[ws.cell(1, i).column_letter].width = w zebra = PatternFill('solid', fgColor='EEF0F8') for row_idx in range(2, ws.max_row + 1): if row_idx % 2 == 0: for col_idx in range(1, len(headers) + 1): ws.cell(row_idx, col_idx).fill = zebra buf = io.BytesIO() wb.save(buf) buf.seek(0) nombre_q = '_'.join(terminos).replace(' ', '-') if terminos else '' filename = f'viat{"_" + nombre_q if nombre_q else ""}.xlsx' return send_file(buf, as_attachment=True, download_name=filename, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') # ── CRUD Campus ────────────────────────────────────────────── @app.route('/campus') @login_required def campus_list(): db = get_db() q = request.args.get('q', '').strip() solo_filas = request.args.get('solo_filas') == '1' base_sql = ''' SELECT c.*, cat.tipo as categoria FROM campus c LEFT JOIN categorias cat ON c.categoria_id = cat.id ''' if q: like = f'%{q}%' rows = db.execute( base_sql + ' WHERE c.nombre LIKE ? OR c.telefono LIKE ? OR c.email LIKE ? OR CAST(c.id AS TEXT) LIKE ? OR cat.tipo LIKE ? ORDER BY c.id', (like, like, like, like, like) ).fetchall() else: rows = db.execute(base_sql + ' ORDER BY c.id').fetchall() if solo_filas: return render_template('campus/rows.html', rows=rows, q=q) return render_template('campus/list.html', page='campus', rows=rows, q=q) @app.route('/campus/nuevo', methods=['GET', 'POST']) @login_required def campus_nuevo(): db = get_db() categorias = db.execute('SELECT * FROM categorias ORDER BY tipo').fetchall() if request.method == 'POST': nombre = request.form['nombre'].strip() telefono = request.form['telefono'].strip() email = request.form['email'].strip() categoria_id = request.form.get('categoria_id') or None error = None if not nombre: error = 'El nombre es obligatorio.' elif not telefono: error = 'El teléfono es obligatorio.' elif not email: error = 'El email es obligatorio.' if error: flash(error, 'error') else: try: db.execute( 'INSERT INTO campus (nombre, telefono, email, categoria_id) VALUES (?, ?, ?, ?)', (nombre, telefono, email, categoria_id) ) db.commit() flash('Campus creado correctamente.', 'ok') return redirect(url_for('campus_list')) except sqlite3.IntegrityError: flash('Ese email ya existe en la base de datos.', 'error') return render_template('campus/form.html', page='campus', campus=None, categorias=categorias) @app.route('/campus//editar', methods=['GET', 'POST']) @login_required def campus_editar(id): db = get_db() campus = db.execute('SELECT * FROM campus WHERE id=?', (id,)).fetchone() categorias = db.execute('SELECT * FROM categorias ORDER BY tipo').fetchall() if not campus: flash('Campus no encontrado.', 'error') return redirect(url_for('campus_list')) if request.method == 'POST': nombre = request.form['nombre'].strip() telefono = request.form['telefono'].strip() email = request.form['email'].strip() categoria_id = request.form.get('categoria_id') or None error = None if not nombre: error = 'El nombre es obligatorio.' elif not telefono: error = 'El teléfono es obligatorio.' elif not email: error = 'El email es obligatorio.' if error: flash(error, 'error') else: try: db.execute( 'UPDATE campus SET nombre=?, telefono=?, email=?, categoria_id=? WHERE id=?', (nombre, telefono, email, categoria_id, id) ) db.commit() flash('Campus actualizado.', 'ok') return redirect(url_for('campus_list')) except sqlite3.IntegrityError: flash('Ese email ya está en uso por otro registro.', 'error') return render_template('campus/form.html', page='campus', campus=campus, categorias=categorias) @app.route('/campus//eliminar', methods=['POST']) @login_required def campus_eliminar(id): db = get_db() db.execute('DELETE FROM campus WHERE id=?', (id,)) db.commit() flash('Campus eliminado.', 'ok') return redirect(url_for('campus_list')) @app.context_processor def inject_globals(): from datetime import date mensajes = get_db().execute( 'SELECT * FROM mensajes ORDER BY fecha DESC' ).fetchall() return { 'now': date.today().isoformat(), 'mensajes': mensajes } @app.template_filter('fecha_es') def fecha_es(valor): if not valor: return '—' try: from datetime import datetime return datetime.strptime(valor[:10], '%Y-%m-%d').strftime('%d-%m-%Y') except: return valor @app.template_filter('fecha_es_hora') def fecha_es(valor): if not valor: return '—' try: from datetime import datetime dt = datetime.strptime(str(valor), '%Y-%m-%d %H:%M:%S') return dt.strftime('%d-%m-%Y %H:%M') except: return valor # ------------------------------------------------------------ if __name__ == '__main__': init_db() app.run(host="0.0.0.0", port=5000, debug=True) # app.run(debug=True) # CODIGO PREVIO A INCLUIR NORMALIZACION # if terminos: # condiciones = [] # params = [] # for t in terminos: # like = f'%{t}%' # condiciones.append('''( # m_oficial LIKE ? OR m_reservada LIKE ? OR VIN LIKE ? # OR marca LIKE ? OR modelo LIKE ? OR poblacion LIKE ? # OR unidad LIKE ? OR grupo LIKE ? OR tipo LIKE ? # OR situacion LIKE ? OR int_operacion LIKE ? # )''') # params.extend([like] * 11) # where = ' AND '.join(condiciones) # rows = db.execute( # f'SELECT id, m_oficial, m_reservada, marca, modelo, poblacion, unidad, grupo, tipo, situacion FROM vehiculos WHERE {where} {filtro_bajas} ORDER BY id', # params # ).fetchall()