From 313ae9a15a8fa5216e406c1dcf72019533ac4bb9 Mon Sep 17 00:00:00 2001 From: Shiro-Nek0 Date: Sat, 28 Mar 2026 01:11:23 -0300 Subject: [PATCH] universal filters v1 + more sample data --- database.py | 158 +++++--- routes_admin.py | 481 ++++++------------------- templates/admin_report_cc.html | 12 +- templates/admin_report_comisiones.html | 12 +- templates/admin_report_horarios.html | 16 +- templates/admin_report_iva.html | 12 +- templates/admin_report_modulo.html | 12 + templates/macros/modals.html | 42 +++ utils.py | 34 +- 9 files changed, 351 insertions(+), 428 deletions(-) diff --git a/database.py b/database.py index 807ad57..fdd47e6 100644 --- a/database.py +++ b/database.py @@ -1,4 +1,6 @@ import sqlite3 +import datetime +import random from werkzeug.security import generate_password_hash DB_NAME = "db/rendiciones.db" @@ -7,13 +9,19 @@ def get_db_connection(): return sqlite3.connect(DB_NAME) def populateDefaults(): + random.seed(42) conn = get_db_connection() c = conn.cursor() + c.execute("SELECT COUNT(*) FROM zonas") if c.fetchone()[0] == 0: zonas = ['Norte', 'Quinta', 'RM', 'Sur'] for zona in zonas: c.execute("INSERT INTO zonas (name) VALUES (?)", (zona,)) + conn.commit() + + c.execute("SELECT COUNT(*) FROM modulos") + if c.fetchone()[0] == 0: c.execute("SELECT id, name FROM zonas") zona_map = {name: id for id, name in c.fetchall()} modulos_data = [ @@ -30,7 +38,7 @@ def populateDefaults(): c.execute("SELECT COUNT(*) FROM productos") if c.fetchone()[0] == 0: - productos_data = [ + productos_base = [ ('PACK LENTES DE SOL 1 x', 12990, 200), ('PACK LENTES DE PANTALLA', 12990, 200), ('PACK LENTES DE SOL 2 x', 19990, 400), @@ -64,59 +72,115 @@ def populateDefaults(): c.execute("SELECT id FROM zonas") zonas_ids = [row[0] for row in c.fetchall()] for zona_id in zonas_ids: - for name, price, commission in productos_data: + for name, price, commission in productos_base: c.execute("INSERT INTO productos (zona_id, name, price, commission) VALUES (?, ?, ?, ?)", (zona_id, name, price, commission)) + conn.commit() + + c.execute("SELECT COUNT(*) FROM workers WHERE is_admin = 0") + if c.fetchone()[0] == 0: + c.execute("SELECT id FROM modulos") + mod_ids = [row[0] for row in c.fetchall()] + default_pass = generate_password_hash("123456") + + workers_data = [ + ("11.111.111-1", "Juan Perez", "+56 9 1111 1111", default_pass, 0, mod_ids[0], "Full Time"), + ("22.222.222-2", "Maria Gonzalez", "+56 9 2222 2222", default_pass, 0, mod_ids[0], "Part Time"), + ("33.333.333-3", "Pedro Soto", "+56 9 3333 3333", default_pass, 0, mod_ids[1], "Full Time"), + ("44.444.444-4", "Ana Silva", "+56 9 4444 4444", default_pass, 0, mod_ids[1], "Part Time"), + ("55.555.555-5", "Diego Lopez", "+56 9 5555 5555", default_pass, 0, mod_ids[2], "Full Time"), + ("66.666.666-6", "Claudia Jara", "+56 9 6666 6666", default_pass, 0, mod_ids[2], "Full Time"), + ("77.777.777-7", "Roberto Munoz", "+56 9 7777 7777", default_pass, 0, mod_ids[3], "Part Time"), + ("88.888.888-8", "Elena Espinoza", "+56 9 8888 8888", default_pass, 0, mod_ids[3], "Part Time"), + ("99.999.999-9", "Mauricio Rivas", "+56 9 9999 9999", default_pass, 0, mod_ids[4], "Full Time"), + ("10.101.101-0", "Sofia Castro", "+56 9 1010 1010", default_pass, 0, mod_ids[4], "Part Time") + ] + for w in workers_data: + c.execute("INSERT OR IGNORE INTO workers (rut, name, phone, password_hash, is_admin, modulo_id, tipo) VALUES (?, ?, ?, ?, ?, ?, ?)", w) + conn.commit() - c.execute("SELECT COUNT(*) FROM workers WHERE is_admin = 0") - if c.fetchone()[0] == 0: - c.execute("SELECT id FROM modulos LIMIT 2") - modulos_ids = c.fetchall() - if len(modulos_ids) >= 2: - mod_1 = modulos_ids[0][0] - mod_2 = modulos_ids[1][0] - default_pass = generate_password_hash("123456") - workers_data = [ - ("11.111.111-1", "Juan Perez", "+56 9 1111 1111", default_pass, 0, mod_1, "Full Time"), - ("22.222.222-2", "Maria Gonzalez", "+56 9 2222 2222", default_pass, 0, mod_1, "Part Time"), - ("33.333.333-3", "Pedro Soto", "+56 9 3333 3333", default_pass, 0, mod_2, "Full Time"), - ("44.444.444-4", "Ana Silva", "+56 9 4444 4444", default_pass, 0, mod_2, "Part Time") - ] - for w in workers_data: - c.execute("INSERT OR IGNORE INTO workers (rut, name, phone, password_hash, is_admin, modulo_id, tipo) VALUES (?, ?, ?, ?, ?, ?, ?)", w) - conn.commit() - # Generar Rendiciones Placeholder c.execute("SELECT COUNT(*) FROM rendiciones") if c.fetchone()[0] == 0: - import datetime - # 1. Obtener datos necesarios para las llaves foráneas - c.execute("SELECT id, modulo_id FROM workers WHERE is_admin = 0") + c.execute("SELECT id, modulo_id, tipo FROM workers WHERE is_admin = 0") workers_list = c.fetchall() - - # 2. Insertar una rendición por trabajador - for w_id, m_id in workers_list: - fecha_ejemplo = (datetime.date.today() - datetime.timedelta(days=1)).isoformat() - - c.execute('''INSERT INTO rendiciones - (worker_id, worker_comision, modulo_id, fecha, hora_entrada, hora_salida, - venta_debito, venta_credito, venta_mp, venta_efectivo, gastos, observaciones) - VALUES (?, 1, ?, ?, '09:00', '21:00', ?, ?, ?, ?, 0, 'Rendición de prueba')''', - (w_id, m_id, fecha_ejemplo, 10000, 20000, 5000, 15000)) - - rendicion_id = c.lastrowid - # 3. Añadir productos a esa rendición (Rendicion_items) - # Obtenemos productos de cualquier zona para el ejemplo - c.execute("SELECT id, price, commission FROM productos LIMIT 3") - prods = c.fetchall() - - for p_id, p_price, p_comm in prods: - c.execute('''INSERT INTO rendicion_items - (rendicion_id, producto_id, cantidad, precio_historico, comision_historica) - VALUES (?, ?, ?, ?, ?)''', - (rendicion_id, p_id, 2, p_price, p_comm)) + hoy = datetime.date.today() + dias_totales = 365 * 2 + hoy.timetuple().tm_yday - conn.commit() + for i in range(dias_totales): + fecha_actual = (hoy - datetime.timedelta(days=i)) + fecha_str = fecha_actual.isoformat() + + num_modulos_hoy = random.randint(3, 6) + mods_activos = random.sample(range(len(modulos_data)), k=num_modulos_hoy) + + for mod_idx in mods_activos: + target_mod_id = mod_idx + 1 + workers_in_mod = [w for w in workers_list if w[1] == target_mod_id] + + if not workers_in_mod: + continue + + main_worker = random.choice(workers_in_mod) + w_id, m_id, w_tipo = main_worker + + w_comision = 1 + if w_tipo == "Part Time": + w_comision = random.choice([0, 1]) + + companion_id = None + comp_hora_ent = None + comp_hora_sal = None + comp_comision = 0 + + other_workers_in_mod = [w for w in workers_in_mod if w[0] != w_id] + if other_workers_in_mod and random.random() < 0.3: + companion = random.choice(other_workers_in_mod) + companion_id = companion[0] + comp_hora_ent = "11:00" + comp_hora_sal = "19:00" + comp_comision = random.choice([0, 1]) + + v_debito = random.randint(15000, 80000) + v_credito = random.randint(10000, 120000) + v_efectivo = random.randint(5000, 50000) + + b_debito = max(1, v_debito // 15000) + b_credito = max(1, v_credito // 15000) + b_efectivo = max(1, v_efectivo // 15000) + + gastos = random.choice([0, 0, 0, 2000, 5000]) + + c.execute('''INSERT INTO rendiciones + (worker_id, worker_comision, companion_id, companion_hora_entrada, companion_hora_salida, + companion_comision, modulo_id, fecha, hora_entrada, hora_salida, + venta_debito, venta_credito, venta_efectivo, + boletas_debito, boletas_credito, boletas_efectivo, + gastos, observaciones) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, '09:00', '21:00', ?, ?, ?, ?, ?, ?, ?, ?)''', + (w_id, w_comision, companion_id, comp_hora_ent, comp_hora_sal, + comp_comision, m_id, fecha_str, v_debito, v_credito, v_efectivo, + b_debito, b_credito, b_efectivo, + gastos, f"Carga histórica {fecha_str}")) + + rend_id = c.lastrowid + + c.execute("SELECT id, price, commission FROM productos WHERE zona_id = (SELECT zona_id FROM modulos WHERE id = ?)", (m_id,)) + prods_zona = c.fetchall() + + if prods_zona: + items_hoy = random.sample(prods_zona, k=min(len(prods_zona), random.randint(2, 6))) + for p_id, p_price, p_comm in items_hoy: + c.execute('''INSERT INTO rendicion_items + (rendicion_id, producto_id, cantidad, precio_historico, comision_historica) + VALUES (?, ?, ?, ?, ?)''', + (rend_id, p_id, random.randint(1, 4), p_price, p_comm)) + + if i % 100 == 0: + conn.commit() + + conn.commit() + conn.close() def init_db(): @@ -181,11 +245,13 @@ def init_db(): comision_historica INTEGER NOT NULL, FOREIGN KEY (rendicion_id) REFERENCES rendiciones(id), FOREIGN KEY (producto_id) REFERENCES productos(id))''') + c.execute("SELECT id FROM workers WHERE is_admin = 1") if not c.fetchone(): admin_pass = generate_password_hash("admin123") c.execute("INSERT INTO workers (rut, name, phone, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)", ("1-9", "System Admin", "+56 9 0000 0000", admin_pass, 1)) + conn.commit() conn.close() populateDefaults() \ No newline at end of file diff --git a/routes_admin.py b/routes_admin.py index 9678413..cfc1442 100644 --- a/routes_admin.py +++ b/routes_admin.py @@ -3,7 +3,7 @@ from flask import app, render_template, request, redirect, url_for, flash, sessi from werkzeug.security import generate_password_hash from datetime import date, datetime from database import get_db_connection -from utils import admin_required, validate_rut, format_rut, validate_phone, format_phone, generate_random_password +from utils import admin_required, validate_rut, format_rut, validate_phone, format_phone, generate_random_password, get_report_params, get_common_report_data import calendar def register_admin_routes(app): @@ -508,413 +508,146 @@ def register_admin_routes(app): @app.route('/admin/reportes/modulo/') @admin_required def report_modulo_periodo(modulo_id): - mes_actual = date.today().month - anio_actual = date.today().year - dias_en_periodo = [f'{d:02}' for d in range(1, 32)] - + anio, mes, dia_f, worker_id = get_report_params() conn = get_db_connection() c = conn.cursor() - - c.execute("SELECT name FROM modulos WHERE id = ?", (modulo_id,)) - modulo_info = c.fetchone() - if not modulo_info: - conn.close() - flash("Módulo no encontrado.", "danger") - return redirect(url_for('admin_reportes_index')) - modulo_name = modulo_info[0] - - c.execute(''' - SELECT strftime('%d', r.fecha) as dia, - SUM(r.venta_debito) as debito, - SUM(r.venta_credito) as credito, - SUM(r.venta_mp) as mp, - SUM(r.venta_efectivo) as efectivo, - SUM(r.gastos) as gastos - FROM rendiciones r - WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ? - GROUP BY dia - ''', (modulo_id, f'{mes_actual:02}', str(anio_actual))) - finanzas_db = c.fetchall() - - c.execute(''' - SELECT strftime('%d', r.fecha) as dia, - SUM(ri.cantidad * ri.comision_historica * CASE WHEN r.worker_comision = 1 OR r.companion_comision = 1 THEN 1 ELSE 0 END) as comision_total - FROM rendicion_items ri - JOIN rendiciones r ON ri.rendicion_id = r.id - WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ? - GROUP BY dia - ''', (modulo_id, f'{mes_actual:02}', str(anio_actual))) - comisiones_db = c.fetchall() + mod_name, workers_list, anios_list = get_common_report_data(c, modulo_id) + where_clause = "WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ?" + params = [modulo_id, mes, anio] + if dia_f: where_clause += " AND strftime('%d', r.fecha) = ?"; params.append(dia_f.zfill(2)) + if worker_id: where_clause += " AND r.worker_id = ?"; params.append(worker_id) + + c.execute(f"SELECT strftime('%d', r.fecha) as dia, SUM(r.venta_debito), SUM(r.venta_credito), SUM(r.venta_mp), SUM(r.venta_efectivo), SUM(r.gastos) FROM rendiciones r {where_clause} GROUP BY dia", tuple(params)) + finanzas = c.fetchall() + c.execute(f"SELECT strftime('%d', r.fecha) as dia, SUM(ri.cantidad * ri.comision_historica) FROM rendicion_items ri JOIN rendiciones r ON ri.rendicion_id = r.id {where_clause} AND (r.worker_comision = 1 OR r.companion_comision = 1) GROUP BY dia", tuple(params)) + comisiones = {row[0]: row[1] for row in c.fetchall()} + + _, num_dias = calendar.monthrange(int(anio), int(mes)) + dias_en_periodo = [f'{d:02d}' for d in range(1, num_dias + 1)] + data_por_dia = {d: {'debito':0,'credito':0,'mp':0,'efectivo':0,'gastos':0,'comision':0,'venta_total':0} for d in dias_en_periodo} + for r in finanzas: + d = r[0] + vt = sum(filter(None, r[1:5])) + data_por_dia[d].update({'debito':r[1] or 0,'credito':r[2] or 0,'mp':r[3] or 0,'efectivo':r[4] or 0,'gastos':r[5] or 0,'venta_total':vt,'comision':comisiones.get(d, 0)}) + + totales_mes = {k: sum(d[k] for d in data_por_dia.values()) for k in data_por_dia['01'].keys()} + dias_activos = sum(1 for d in data_por_dia.values() if d['venta_total'] > 0) conn.close() + return render_template('admin_report_modulo.html', modulo_name=mod_name, modulo_id=modulo_id, mes_nombre=f"{mes}/{anio}", dias_en_periodo=dias_en_periodo, data_por_dia=data_por_dia, totales_mes=totales_mes, dias_activos=dias_activos, workers_list=workers_list, worker_actual=worker_id, dia_actual=dia_f, mes_actual=mes, anio_actual=anio, anios_disponibles=anios_list) - data_por_dia = {dia: {'debito': 0, 'credito': 0, 'mp': 0, 'efectivo': 0, 'gastos': 0, 'comision': 0, 'venta_total': 0} for dia in dias_en_periodo} - - for row in finanzas_db: - dia, debito, credito, mp, efectivo, gastos = row - venta_total = (debito or 0) + (credito or 0) + (mp or 0) + (efectivo or 0) - data_por_dia[dia].update({ - 'debito': debito or 0, - 'credito': credito or 0, - 'mp': mp or 0, - 'efectivo': efectivo or 0, - 'gastos': gastos or 0, - 'venta_total': venta_total - }) - - for row in comisiones_db: - dia, comision = row - data_por_dia[dia]['comision'] = comision or 0 - - totales_mes = {'debito': 0, 'credito': 0, 'mp': 0, 'efectivo': 0, 'gastos': 0, 'comision': 0, 'venta_total': 0} - dias_activos = 0 - - for dia, datos in data_por_dia.items(): - if datos['venta_total'] > 0 or datos['gastos'] > 0: - dias_activos += 1 - for k in totales_mes.keys(): - totales_mes[k] += datos[k] - - return render_template('admin_report_modulo.html', - modulo_name=modulo_name, - mes_nombre=f'{mes_actual:02}/{anio_actual}', - dias_en_periodo=dias_en_periodo, - data_por_dia=data_por_dia, - totales_mes=totales_mes, - dias_activos=dias_activos) - @app.route('/admin/reportes/modulo//comisiones') @admin_required def report_modulo_comisiones(modulo_id): - mes_actual = date.today().month - anio_actual = date.today().year - + anio, mes, dia_f, worker_id = get_report_params() conn = get_db_connection() c = conn.cursor() - - c.execute("SELECT name FROM modulos WHERE id = ?", (modulo_id,)) - modulo_info = c.fetchone() - if not modulo_info: - conn.close() - flash("Módulo no encontrado.", "danger") - return redirect(url_for('admin_reportes_index')) - modulo_name = modulo_info[0] - - # Fetch rendiciones with commission calculations for this module and month - c.execute(''' - SELECT r.id, strftime('%d', r.fecha) as dia, - w.id, w.name, w.tipo, r.worker_comision, - cw.id, cw.name, cw.tipo, r.companion_comision, - COALESCE((SELECT SUM(cantidad * comision_historica) FROM rendicion_items WHERE rendicion_id = r.id), 0) as total_comision - FROM rendiciones r - JOIN workers w ON r.worker_id = w.id - LEFT JOIN workers cw ON r.companion_id = cw.id - WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ? - ORDER BY r.fecha ASC - ''', (modulo_id, f'{mes_actual:02}', str(anio_actual))) - + mod_name, workers_list, anios_list = get_common_report_data(c, modulo_id) + where_clause = "WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ?" + params = [modulo_id, mes, anio] + if dia_f: where_clause += " AND strftime('%d', r.fecha) = ?"; params.append(dia_f.zfill(2)) + if worker_id: where_clause += " AND (r.worker_id = ? OR r.companion_id = ?)"; params.extend([worker_id, worker_id]) + c.execute(f"SELECT r.id, strftime('%d', r.fecha) as dia, w.id, w.name, w.tipo, r.worker_comision, cw.id, cw.name, cw.tipo, r.companion_comision, (SELECT SUM(cantidad * comision_historica) FROM rendicion_items WHERE rendicion_id = r.id) FROM rendiciones r JOIN workers w ON r.worker_id = w.id LEFT JOIN workers cw ON r.companion_id = cw.id {where_clause}", tuple(params)) rendiciones = c.fetchall() - conn.close() - workers_data = {} - - for row in rendiciones: - r_id, dia, w_id, w_name, w_tipo, w_com, c_id, c_name, c_tipo, c_com, total_com = row - - w_share = 0 - c_share = 0 - - # Split logic - if w_com and c_com: - w_share = total_com / 2 - c_share = total_com / 2 - elif w_com: - w_share = total_com - elif c_com: - c_share = total_com - - # Process Titular Worker - if w_id not in workers_data: - workers_data[w_id] = {'name': w_name, 'tipo': w_tipo, 'dias': {}, 'total': 0, 'enabled': bool(w_com)} - else: - if w_com: workers_data[w_id]['enabled'] = True - - workers_data[w_id]['dias'][dia] = workers_data[w_id]['dias'].get(dia, 0) + w_share - workers_data[w_id]['total'] += w_share - - # Process Companion (if any) - if c_id: - if c_id not in workers_data: - workers_data[c_id] = {'name': c_name, 'tipo': c_tipo, 'dias': {}, 'total': 0, 'enabled': bool(c_com)} - else: - if c_com: workers_data[c_id]['enabled'] = True - - workers_data[c_id]['dias'][dia] = workers_data[c_id]['dias'].get(dia, 0) + c_share - workers_data[c_id]['total'] += c_share + for r in rendiciones: + total_com = r[10] or 0 + # Lógica simplificada: reparte si ambos comisionan + for idx, wid, wname, wtipo, wcom in [(0, r[2], r[3], r[4], r[5]), (1, r[6], r[7], r[8], r[9])]: + if wid and wcom: + if wid not in workers_data: workers_data[wid] = {'name':wname,'tipo':wtipo,'dias':{},'total':0,'enabled':True} + val = total_com / 2 if (r[5] and r[9]) else total_com + workers_data[wid]['dias'][r[1]] = workers_data[wid]['dias'].get(r[1], 0) + val + workers_data[wid]['total'] += val + _, num_dias = calendar.monthrange(int(anio), int(mes)) + dias_en_periodo = [f'{d:02d}' for d in range(1, num_dias + 1)] + conn.close() + return render_template('admin_report_comisiones.html', modulo_name=mod_name, modulo_id=modulo_id, mes_nombre=f"{mes}/{anio}", workers_data=dict(sorted(workers_data.items(), key=lambda x:x[1]['name'])), dias_en_periodo=dias_en_periodo, workers_list=workers_list, worker_actual=worker_id, dia_actual=dia_f, mes_actual=mes, anio_actual=anio, anios_disponibles=anios_list) - # Sort alphabetically so the table doesn't shuffle randomly - workers_data = dict(sorted(workers_data.items(), key=lambda item: item[1]['name'])) - dias_en_periodo = [f'{d:02}' for d in range(1, 32)] - - return render_template('admin_report_comisiones.html', - modulo_name=modulo_name, - mes_nombre=f'{mes_actual:02}/{anio_actual}', - workers_data=workers_data, - dias_en_periodo=dias_en_periodo) - @app.route('/admin/reportes/modulo//horarios') @admin_required def report_modulo_horarios(modulo_id): - import calendar - from datetime import date, datetime - - mes_actual = date.today().month - anio_actual = date.today().year - + anio, mes, dia_f, worker_id = get_report_params() conn = get_db_connection() c = conn.cursor() - - c.execute("SELECT name FROM modulos WHERE id = ?", (modulo_id,)) - modulo_info = c.fetchone() - if not modulo_info: - conn.close() - flash("Módulo no encontrado.", "danger") - return redirect(url_for('admin_reportes_index')) - modulo_name = modulo_info[0] - - # 1. Pre-cargar a los trabajadores oficiales del módulo (aunque no hayan trabajado aún) - c.execute("SELECT id, name FROM workers WHERE modulo_id = ? AND is_admin = 0", (modulo_id,)) - assigned_workers = c.fetchall() - - workers_data = {} - for w_id, w_name in assigned_workers: - workers_data[w_id] = {'name': w_name, 'dias': {}, 'total_horas': 0.0} - - # 2. Extraer rendiciones del mes/módulo - c.execute(''' - SELECT - r.fecha, - w.id, w.name, r.hora_entrada, r.hora_salida, - cw.id, cw.name, r.companion_hora_entrada, r.companion_hora_salida - FROM rendiciones r - JOIN workers w ON r.worker_id = w.id - LEFT JOIN workers cw ON r.companion_id = cw.id - WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ? - ORDER BY r.fecha ASC - ''', (modulo_id, f'{mes_actual:02}', str(anio_actual))) - + mod_name, workers_list, anios_list = get_common_report_data(c, modulo_id) + where_clause = "WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ?" + params = [modulo_id, mes, anio] + if dia_f: where_clause += " AND strftime('%d', r.fecha) = ?"; params.append(dia_f.zfill(2)) + if worker_id: where_clause += " AND (r.worker_id = ? OR r.companion_id = ?)"; params.extend([worker_id, worker_id]) + c.execute(f"SELECT r.fecha, w.id, w.name, r.hora_entrada, r.hora_salida, cw.id, cw.name, r.companion_hora_entrada, r.companion_hora_salida FROM rendiciones r JOIN workers w ON r.worker_id = w.id LEFT JOIN workers cw ON r.companion_id = cw.id {where_clause}", tuple(params)) rendiciones = c.fetchall() - conn.close() - - def calc_horas(in_str, out_str): - if not in_str or not out_str: - return 0.0, "0:00" + workers_data = {} + def calc_h(i, o): + if not i or not o: return 0, "0:00" try: - t1 = datetime.strptime(in_str, '%H:%M') - t2 = datetime.strptime(out_str, '%H:%M') - delta = t2 - t1 - return delta.seconds / 3600, f"{delta.seconds // 3600}:{(delta.seconds % 3600) // 60:02d}" - except ValueError: - return 0.0, "0:00" - + t1, t2 = datetime.strptime(i, '%H:%M'), datetime.strptime(o, '%H:%M') + d = t2 - t1 + return d.seconds/3600, f"{d.seconds//3600}:{(d.seconds%3600)//60:02d}" + except: return 0, "0:00" for r in rendiciones: - fecha, w_id, w_name, w_in, w_out, c_id, c_name, c_in, c_out = r - dia = fecha[-2:] - - # Titular (Si no es del módulo, lo metemos con etiqueta de Apoyo) - if w_id not in workers_data: - workers_data[w_id] = {'name': f"{w_name} (Apoyo)", 'dias': {}, 'total_horas': 0.0} - - h_dec, h_str = calc_horas(w_in, w_out) - workers_data[w_id]['dias'][dia] = {'in': w_in, 'out': w_out, 'hrs': h_str} - workers_data[w_id]['total_horas'] += h_dec - - # Acompañante - if c_id and c_in and c_out: - if c_id not in workers_data: - workers_data[c_id] = {'name': f"{c_name} (Apoyo)", 'dias': {}, 'total_horas': 0.0} - - h_dec, h_str = calc_horas(c_in, c_out) - workers_data[c_id]['dias'][dia] = {'in': c_in, 'out': c_out, 'hrs': h_str} - workers_data[c_id]['total_horas'] += h_dec + d = r[0][-2:] + for wid, wname, win, wout in [(r[1], r[2], r[3], r[4]), (r[5], r[6], r[7], r[8])]: + if wid: + if wid not in workers_data: workers_data[wid] = {'name':wname,'dias':{},'total_horas':0} + h_dec, h_str = calc_h(win, wout) + workers_data[wid]['dias'][d] = {'in':win,'out':wout,'hrs':h_str} + workers_data[wid]['total_horas'] += h_dec + for w in workers_data.values(): + th = w['total_horas'] + w['total_hrs_str'] = f"{int(th)}:{int((th-int(th))*60):02d}" + _, num_dias = calendar.monthrange(int(anio), int(mes)) + dias_en_periodo = [{'num':f'{d:02d}','name':['L','M','M','J','V','S','D'][date(int(anio),int(mes),d).weekday()]} for d in range(1, num_dias+1)] + conn.close() + return render_template('admin_report_horarios.html', modulo_name=mod_name, modulo_id=modulo_id, mes_nombre=f"{mes}/{anio}", workers_data=workers_data, dias_en_periodo=dias_en_periodo, workers_list=workers_list, worker_actual=worker_id, dia_actual=dia_f, mes_actual=mes, anio_actual=anio, anios_disponibles=anios_list) - for w_id in workers_data: - th = workers_data[w_id]['total_horas'] - workers_data[w_id]['total_hrs_str'] = f"{int(th)}:{int(round((th - int(th)) * 60)):02d}" - - # Ordenar alfabéticamente (Los de apoyo quedarán entremezclados por orden alfabético) - workers_data = dict(sorted(workers_data.items(), key=lambda x: x[1]['name'])) - - _, num_dias = calendar.monthrange(anio_actual, mes_actual) - nombres_dias = ['D', 'L', 'M', 'M', 'J', 'V', 'S'] # Ajustado para que el 0 de Python(Lunes) sea coherente si usas isoweekday - - dias_en_periodo = [] - for d in range(1, num_dias + 1): - dia_semana = date(anio_actual, mes_actual, d).weekday() - dias_en_periodo.append({ - 'num': f'{d:02}', - 'name': ['L', 'M', 'M', 'J', 'V', 'S', 'D'][dia_semana] # weekday(): Lunes es 0, Domingo es 6 - }) - - return render_template('admin_report_horarios.html', - modulo_name=modulo_name, - mes_nombre=f'{mes_actual:02}/{anio_actual}', - workers_data=workers_data, - dias_en_periodo=dias_en_periodo) - - @app.route('/admin/reportes/modulo//centros_comerciales') @admin_required def report_modulo_centros_comerciales(modulo_id): - import calendar - from datetime import date - - mes_actual = date.today().month - anio_actual = date.today().year - - # Obtenemos la cantidad real de días del mes - _, num_dias = calendar.monthrange(anio_actual, mes_actual) - - dias_en_periodo = [] - for d in range(1, num_dias + 1): - dia_semana = date(anio_actual, mes_actual, d).weekday() - dias_en_periodo.append({ - 'num': f'{d:02}', - 'name': ['L', 'M', 'M', 'J', 'V', 'S', 'D'][dia_semana] - }) - + anio, mes, dia_f, worker_id = get_report_params() conn = get_db_connection() c = conn.cursor() - - c.execute("SELECT name FROM modulos WHERE id = ?", (modulo_id,)) - modulo_info = c.fetchone() - if not modulo_info: - conn.close() - flash("Módulo no encontrado.", "danger") - return redirect(url_for('admin_reportes_index')) - modulo_name = modulo_info[0] - - c.execute(''' - SELECT strftime('%d', r.fecha) as dia, - SUM(r.boletas_debito + r.boletas_credito + r.boletas_mp) as trans_red_compra, - SUM(r.boletas_efectivo) as boletas_efectivo, - SUM(r.venta_debito + r.venta_credito + r.venta_mp + r.venta_efectivo) as venta_total - FROM rendiciones r - WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ? - GROUP BY dia - ''', (modulo_id, f'{mes_actual:02}', str(anio_actual))) - + mod_name, workers_list, anios_list = get_common_report_data(c, modulo_id) + where_clause = "WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ?" + params = [modulo_id, mes, anio] + if dia_f: where_clause += " AND strftime('%d', r.fecha) = ?"; params.append(dia_f.zfill(2)) + if worker_id: where_clause += " AND r.worker_id = ?"; params.append(worker_id) + c.execute(f"SELECT strftime('%d', r.fecha) as dia, SUM(r.boletas_debito + r.boletas_credito + r.boletas_mp), SUM(r.boletas_efectivo), SUM(r.venta_debito + r.venta_credito + r.venta_mp + r.venta_efectivo) FROM rendiciones r {where_clause} GROUP BY dia", tuple(params)) resultados = c.fetchall() + _, num_dias = calendar.monthrange(int(anio), int(mes)) + data_por_dia = {f'{d:02d}': {'red_compra':0,'efectivo':0,'total_trans':0,'venta_neta':0} for d in range(1, num_dias+1)} + totales = {'red_compra':0,'efectivo':0,'total_trans':0,'venta_neta':0} + for r in resultados: + dia, rc, ef, vt = r[0], r[1] or 0, r[2] or 0, r[3] or 0 + vn = round(vt/1.19) + data_por_dia[dia] = {'red_compra':rc,'efectivo':ef,'total_trans':rc+ef,'venta_neta':vn} + for k in totales: totales[k] += data_por_dia[dia][k] + dias_en_periodo = [{'num':f'{d:02d}','name':['L','M','M','J','V','S','D'][date(int(anio),int(mes),d).weekday()]} for d in range(1, num_dias+1)] conn.close() + return render_template('admin_report_cc.html', modulo_name=mod_name, modulo_id=modulo_id, mes_nombre=f"{mes}/{anio}", dias_en_periodo=dias_en_periodo, data_por_dia=data_por_dia, totales=totales, workers_list=workers_list, worker_actual=worker_id, dia_actual=dia_f, mes_actual=mes, anio_actual=anio, anios_disponibles=anios_list) - data_por_dia = {f'{d:02}': {'red_compra': 0, 'efectivo': 0, 'total_trans': 0, 'venta_neta': 0} for d in range(1, num_dias + 1)} - totales = {'red_compra': 0, 'efectivo': 0, 'total_trans': 0, 'venta_neta': 0} - - for row in resultados: - dia, red_compra, efectivo, venta_total = row - if dia not in data_por_dia: - continue - - red_compra = red_compra or 0 - efectivo = efectivo or 0 - venta_total = venta_total or 0 - - total_trans = red_compra + efectivo - venta_neta = round(venta_total / 1.19) - - data_por_dia[dia] = { - 'red_compra': red_compra, - 'efectivo': efectivo, - 'total_trans': total_trans, - 'venta_neta': venta_neta - } - - totales['red_compra'] += red_compra - totales['efectivo'] += efectivo - totales['total_trans'] += total_trans - totales['venta_neta'] += venta_neta - - return render_template('admin_report_cc.html', - modulo_name=modulo_name, - mes_nombre=f'{mes_actual:02}/{anio_actual}', - dias_en_periodo=dias_en_periodo, - data_por_dia=data_por_dia, - totales=totales) - @app.route('/admin/reportes/modulo//calculo_iva') @admin_required def report_modulo_calculo_iva(modulo_id): - import calendar - from datetime import date - - mes_actual = date.today().month - anio_actual = date.today().year - - _, num_dias = calendar.monthrange(anio_actual, mes_actual) - - dias_en_periodo = [] - for d in range(1, num_dias + 1): - dia_semana = date(anio_actual, mes_actual, d).weekday() - dias_en_periodo.append({ - 'num': f'{d:02}', - 'name': ['L', 'M', 'M', 'J', 'V', 'S', 'D'][dia_semana] - }) - + anio, mes, dia_f, worker_id = get_report_params() conn = get_db_connection() c = conn.cursor() - - c.execute("SELECT name FROM modulos WHERE id = ?", (modulo_id,)) - modulo_info = c.fetchone() - if not modulo_info: - conn.close() - flash("Módulo no encontrado.", "danger") - return redirect(url_for('admin_reportes_index')) - modulo_name = modulo_info[0] - - c.execute(''' - SELECT strftime('%d', r.fecha) as dia, - SUM(r.venta_efectivo) as venta_efectivo, - SUM(r.venta_debito + r.venta_credito + r.venta_mp) as venta_tbk - FROM rendiciones r - WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ? - GROUP BY dia - ''', (modulo_id, f'{mes_actual:02}', str(anio_actual))) - + mod_name, workers_list, anios_list = get_common_report_data(c, modulo_id) + where_clause = "WHERE r.modulo_id = ? AND strftime('%m', r.fecha) = ? AND strftime('%Y', r.fecha) = ?" + params = [modulo_id, mes, anio] + if dia_f: where_clause += " AND strftime('%d', r.fecha) = ?"; params.append(dia_f.zfill(2)) + if worker_id: where_clause += " AND r.worker_id = ?"; params.append(worker_id) + c.execute(f"SELECT strftime('%d', r.fecha) as dia, SUM(r.venta_efectivo), SUM(r.venta_debito + r.venta_credito + r.venta_mp) FROM rendiciones r {where_clause} GROUP BY dia", tuple(params)) resultados = c.fetchall() + _, num_dias = calendar.monthrange(int(anio), int(mes)) + data_por_dia = {f'{d:02d}': {'efectivo':0,'tbk':0,'total':0,'porcentaje':0} for d in range(1, num_dias+1)} + totales = {'efectivo':0,'tbk':0,'total':0,'porcentaje':0} + for r in resultados: + dia, ef, tbk = r[0], r[1] or 0, r[2] or 0 + tt = ef + tbk + data_por_dia[dia] = {'efectivo':ef,'tbk':tbk,'total':tt,'porcentaje':round((ef/tt)*100) if tt>0 else 0} + totales['efectivo'] += ef; totales['tbk'] += tbk; totales['total'] += tt + if totales['total'] > 0: totales['porcentaje'] = round((totales['efectivo']/totales['total'])*100) + dias_en_periodo = [{'num':f'{d:02d}','name':['L','M','M','J','V','S','D'][date(int(anio),int(mes),d).weekday()]} for d in range(1, num_dias+1)] conn.close() - - data_por_dia = {f'{d:02}': {'efectivo': 0, 'tbk': 0, 'total': 0, 'porcentaje': 0} for d in range(1, num_dias + 1)} - totales = {'efectivo': 0, 'tbk': 0, 'total': 0, 'porcentaje': 0} - - for row in resultados: - dia, efectivo, tbk = row - if dia not in data_por_dia: - continue - - efectivo = efectivo or 0 - tbk = tbk or 0 - total = efectivo + tbk - - # Evitar dividir por cero cuando no se vende nada en el día - porcentaje = round((efectivo / total) * 100) if total > 0 else 0 - - data_por_dia[dia] = { - 'efectivo': efectivo, - 'tbk': tbk, - 'total': total, - 'porcentaje': porcentaje - } - - totales['efectivo'] += efectivo - totales['tbk'] += tbk - totales['total'] += total - - if totales['total'] > 0: - totales['porcentaje'] = round((totales['efectivo'] / totales['total']) * 100) - - return render_template('admin_report_iva.html', - modulo_name=modulo_name, - mes_nombre=f'{mes_actual:02}/{anio_actual}', - dias_en_periodo=dias_en_periodo, - data_por_dia=data_por_dia, - totales=totales) \ No newline at end of file + return render_template('admin_report_iva.html', modulo_name=mod_name, modulo_id=modulo_id, mes_nombre=f"{mes}/{anio}", dias_en_periodo=dias_en_periodo, data_por_dia=data_por_dia, totales=totales, workers_list=workers_list, worker_actual=worker_id, dia_actual=dia_f, mes_actual=mes, anio_actual=anio, anios_disponibles=anios_list) \ No newline at end of file diff --git a/templates/admin_report_cc.html b/templates/admin_report_cc.html index 9647e31..9e3a996 100644 --- a/templates/admin_report_cc.html +++ b/templates/admin_report_cc.html @@ -1,4 +1,5 @@ {% extends "macros/base.html" %} +{% from "macros/modals.html" import report_filters %} {% block title %}Reporte: Centros Comerciales - {{ modulo_name }}{% endblock %} @@ -49,7 +50,16 @@
Período: {{ mes_nombre }}
- +{{ report_filters( + url_for('report_modulo_periodo', modulo_id=modulo_id), + workers_list, + worker_actual, + dia_actual, + [('01','Ene'),('02','Feb'),('03','Mar'),('04','Abr'),('05','May'),('06','Jun'),('07','Jul'),('08','Ago'),('09','Sep'),('10','Oct'),('11','Nov'),('12','Dic')], + mes_actual, + anios_disponibles, + anio_actual +) }}
diff --git a/templates/admin_report_comisiones.html b/templates/admin_report_comisiones.html index 4090268..e7db5f1 100644 --- a/templates/admin_report_comisiones.html +++ b/templates/admin_report_comisiones.html @@ -1,4 +1,5 @@ {% extends "macros/base.html" %} +{% from "macros/modals.html" import report_filters %} {% block title %}Reporte: Comisiones - {{ modulo_name }}{% endblock %} @@ -31,7 +32,16 @@
Período: {{ mes_nombre }}
- +{{ report_filters( + url_for('report_modulo_periodo', modulo_id=modulo_id), + workers_list, + worker_actual, + dia_actual, + [('01','Ene'),('02','Feb'),('03','Mar'),('04','Abr'),('05','May'),('06','Jun'),('07','Jul'),('08','Ago'),('09','Sep'),('10','Oct'),('11','Nov'),('12','Dic')], + mes_actual, + anios_disponibles, + anio_actual +) }}
{% if workers_data %} diff --git a/templates/admin_report_horarios.html b/templates/admin_report_horarios.html index e22672f..89ae014 100644 --- a/templates/admin_report_horarios.html +++ b/templates/admin_report_horarios.html @@ -1,4 +1,5 @@ {% extends "macros/base.html" %} +{% from "macros/modals.html" import report_filters %} {% block title %}Reporte: Horarios - {{ modulo_name }}{% endblock %} @@ -45,7 +46,16 @@
Período: {{ mes_nombre }}
- +{{ report_filters( + url_for('report_modulo_periodo', modulo_id=modulo_id), + workers_list, + worker_actual, + dia_actual, + [('01','Ene'),('02','Feb'),('03','Mar'),('04','Abr'),('05','May'),('06','Jun'),('07','Jul'),('08','Ago'),('09','Sep'),('10','Oct'),('11','Nov'),('12','Dic')], + mes_actual, + anios_disponibles, + anio_actual +) }} {% if workers_data %}
@@ -60,8 +70,8 @@ {% for w_id, data in workers_data.items() %} - Ent - Sal + Entrada + Salida Hrs {% endfor %} diff --git a/templates/admin_report_iva.html b/templates/admin_report_iva.html index 29532ab..befa518 100644 --- a/templates/admin_report_iva.html +++ b/templates/admin_report_iva.html @@ -1,4 +1,5 @@ {% extends "macros/base.html" %} +{% from "macros/modals.html" import report_filters %} {% block title %}Reporte: Cálculo de IVA - {{ modulo_name }}{% endblock %} @@ -49,7 +50,16 @@
Período: {{ mes_nombre }}
- +{{ report_filters( + url_for('report_modulo_periodo', modulo_id=modulo_id), + workers_list, + worker_actual, + dia_actual, + [('01','Ene'),('02','Feb'),('03','Mar'),('04','Abr'),('05','May'),('06','Jun'),('07','Jul'),('08','Ago'),('09','Sep'),('10','Oct'),('11','Nov'),('12','Dic')], + mes_actual, + anios_disponibles, + anio_actual +) }}
diff --git a/templates/admin_report_modulo.html b/templates/admin_report_modulo.html index cd3ecfb..aea257d 100644 --- a/templates/admin_report_modulo.html +++ b/templates/admin_report_modulo.html @@ -1,4 +1,5 @@ {% extends "macros/base.html" %} +{% from "macros/modals.html" import report_filters %} {% block title %}Reporte: Finanzas - {{ modulo_name }}{% endblock %} @@ -72,6 +73,17 @@
+{{ report_filters( + url_for('report_modulo_periodo', modulo_id=modulo_id), + workers_list, + worker_actual, + dia_actual, + [('01','Ene'),('02','Feb'),('03','Mar'),('04','Abr'),('05','May'),('06','Jun'),('07','Jul'),('08','Ago'),('09','Sep'),('10','Oct'),('11','Nov'),('12','Dic')], + mes_actual, + anios_disponibles, + anio_actual +) }} +
Desglose Diario diff --git a/templates/macros/modals.html b/templates/macros/modals.html index 05555bb..c324396 100644 --- a/templates/macros/modals.html +++ b/templates/macros/modals.html @@ -573,4 +573,46 @@
+{% endmacro %} + +{% macro report_filters(action_url, workers, selected_worker, selected_dia, meses, mes_act, anios, anio_act) %} +
+
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
{% endmacro %} \ No newline at end of file diff --git a/utils.py b/utils.py index e39f5be..26eb659 100644 --- a/utils.py +++ b/utils.py @@ -2,7 +2,8 @@ import re import random import string from functools import wraps -from flask import session, redirect, url_for, flash +from flask import session, redirect, url_for, flash, request +from datetime import date def generate_random_password(length=6): return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) @@ -53,4 +54,33 @@ def admin_required(f): flash("Acceso denegado. Se requieren permisos de administrador.", "danger") return redirect(url_for('index')) return f(*args, **kwargs) - return decorated_function \ No newline at end of file + return decorated_function + +def get_report_params(): + """Captura filtros de la URL o establece valores por defecto.""" + hoy = date.today() + anio = request.args.get('anio', str(hoy.year)) + mes = request.args.get('mes', f"{hoy.month:02d}") + dia = request.args.get('dia') + worker_id = request.args.get('worker_id') + return anio, mes, dia, worker_id + +def get_common_report_data(c, modulo_id): + """Obtiene nombre del módulo y lista de trabajadores para los filtros.""" + c.execute("SELECT name FROM modulos WHERE id = ?", (modulo_id,)) + modulo_info = c.fetchone() + + c.execute(''' + SELECT DISTINCT w.id, w.name + FROM workers w + WHERE w.modulo_id = ? AND w.is_admin = 0 + ORDER BY w.name + ''', (modulo_id,)) + workers = c.fetchall() + + c.execute("SELECT DISTINCT strftime('%Y', fecha) as anio FROM rendiciones ORDER BY anio DESC") + anios = [row[0] for row in c.fetchall()] + if str(date.today().year) not in anios: + anios.insert(0, str(date.today().year)) + + return modulo_info[0] if modulo_info else "Módulo", workers, anios \ No newline at end of file