Files
2026-09-14 20:05:45 +02:00

1583 lines
52 KiB
Python
Executable File

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/<int:id>/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/<int:id>/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/<int:id>/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/<int:vid>/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/<int:vid>/historial/<int:hid>/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/<int:id>/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/<int:doc_id>/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/<int:doc_id>/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/<int:doc_id>/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/<int:poblacion_id>')
@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/<int:id>/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/<int:id>/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/<int:unidad_id>", 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/<int:id>/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/<int:id>/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/<int:id>/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()