diff --git a/database.py b/database.py index 0458e2d..807ad57 100644 --- a/database.py +++ b/database.py @@ -163,6 +163,10 @@ def init_db(): venta_credito INTEGER DEFAULT 0, venta_mp INTEGER DEFAULT 0, venta_efectivo INTEGER DEFAULT 0, + boletas_debito INTEGER DEFAULT 0, + boletas_credito INTEGER DEFAULT 0, + boletas_mp INTEGER DEFAULT 0, + boletas_efectivo INTEGER DEFAULT 0, gastos INTEGER DEFAULT 0, observaciones TEXT, FOREIGN KEY (worker_id) REFERENCES workers(id), diff --git a/generar_unificado.py b/generar_unificado.py new file mode 100644 index 0000000..3b95dcb --- /dev/null +++ b/generar_unificado.py @@ -0,0 +1,185 @@ +import random +from datetime import date, timedelta +from werkzeug.security import generate_password_hash +from database import get_db_connection, init_db + +def generar_historico_definitivo(dias_atras=180): + init_db() + conn = get_db_connection() + c = conn.cursor() + + # 1. LIMPIEZA TOTAL (Evita el choque con los datos por defecto de database.py) + print("Limpiando datos de prueba anteriores...") + c.execute("DELETE FROM rendicion_items") + c.execute("DELETE FROM rendiciones") + c.execute("DELETE FROM workers WHERE is_admin = 0") + conn.commit() + + c.execute("SELECT id, name FROM modulos") + modulos = c.fetchall() + + if not modulos: + print("Error: No hay módulos creados.") + return + + # 2. RECLUTAMIENTO FORZADO PARA TODOS LOS MÓDULOS + print(f"Reclutando personal para {len(modulos)} módulos...") + default_pass = generate_password_hash("123456") + workers_data = [] + + for mod_id, mod_name in modulos: + tipos = ["Full Time", "Full Time", "Part Time", "Part Time"] + for i in range(4): + # Usamos un RUT fijo basado en la iteración para no inflar la DB si lo corres sin limpiar + rut_falso = f"{10 + i}.{mod_id:03d}.100-{i}" + nombre_falso = f"Trabajador {i+1} ({mod_name})" + phone_falso = f"+56 9 8888 {mod_id:02d}{i:02d}" + + workers_data.append(( + rut_falso, nombre_falso, phone_falso, + default_pass, 0, mod_id, tipos[i] + )) + + c.executemany('''INSERT OR IGNORE INTO workers + (rut, name, phone, password_hash, is_admin, modulo_id, tipo) + VALUES (?, ?, ?, ?, ?, ?, ?)''', workers_data) + conn.commit() + + # 3. PREPARACIÓN DE DATOS + c.execute("SELECT id, modulo_id FROM workers WHERE is_admin = 0") + all_workers_data = c.fetchall() + todos_los_trabajadores = [w[0] for w in all_workers_data] + + trabajadores_por_modulo = {} + for w_id, m_id in all_workers_data: + if m_id not in trabajadores_por_modulo: + trabajadores_por_modulo[m_id] = [] + trabajadores_por_modulo[m_id].append(w_id) + + c.execute("SELECT id, price, commission FROM productos") + productos = c.fetchall() + + # 4. VIAJE EN EL TIEMPO + hoy = date.today() + fecha_inicio = hoy - timedelta(days=dias_atras) + rendiciones_creadas = 0 + + # Textos de ejemplo para los gastos + motivos_gastos = [ + "Compra bidón de agua", + "Artículos de aseo", + "Lápices y cuaderno", + "Reparación menor del módulo", + "Bolsas para entregar productos", + "Cinta adhesiva", + "Pilas para escáner" + ] + + print(f"Generando turnos con gastos aleatorios desde {fecha_inicio} hasta {hoy}...") + + for i in range(dias_atras + 1): + fecha_actual = fecha_inicio + timedelta(days=i) + fecha_str = fecha_actual.strftime('%Y-%m-%d') + + for modulo_id, mod_name in modulos: + workers_modulo = trabajadores_por_modulo.get(modulo_id, []) + if not workers_modulo: + continue + + num_turnos = random.randint(1, 2) + turnos_a_hacer = [True, False] if num_turnos == 2 else [random.choice([True, False])] + + for es_manana in turnos_a_hacer: + # Reemplazos (15%) + es_reemplazo = random.random() < 0.15 + if es_reemplazo and len(todos_los_trabajadores) > len(workers_modulo): + posibles_reemplazos = [w for w in todos_los_trabajadores if w not in workers_modulo] + worker_id = random.choice(posibles_reemplazos) + else: + worker_id = random.choice(workers_modulo) + + if es_manana: + hora_entrada = f"{random.randint(8, 10):02d}:{random.choice(['00', '30'])}" + hora_salida = f"{random.randint(14, 16):02d}:{random.choice(['00', '30'])}" + else: + hora_entrada = f"{random.randint(13, 15):02d}:{random.choice(['00', '30'])}" + hora_salida = f"{random.randint(19, 21):02d}:{random.choice(['00', '30'])}" + + # Acompañante (70%) + companion_id = None + comp_in, comp_out = None, None + if random.random() < 0.70: + posibles_comp = [w for w in todos_los_trabajadores if w != worker_id] + if posibles_comp: + companion_id = random.choice(posibles_comp) + comp_in, comp_out = hora_entrada, hora_salida + + num_prods = random.randint(1, 5) + prods_elegidos = random.sample(productos, min(num_prods, len(productos))) + items_a_insertar = [] + total_calculado = 0 + + for prod in prods_elegidos: + p_id, p_price, p_comm = prod + cantidad = random.randint(1, 6) + items_a_insertar.append((p_id, cantidad, p_price, p_comm)) + total_calculado += (p_price * cantidad) + + debito, credito, mp, efectivo = 0, 0, 0, 0 + b_debito, b_credito, b_mp, b_efectivo = 0, 0, 0, 0 + + divisiones = random.randint(1, 3) + monto_restante = int(total_calculado) + metodos = ["debito", "credito", "mp", "efectivo"] + random.shuffle(metodos) + + for idx, metodo in enumerate(metodos[:divisiones]): + monto = monto_restante if idx == divisiones - 1 else random.randint(0, monto_restante // 2) + monto_restante -= monto + + if monto > 0: + boletas = random.randint(1, 4) + if metodo == "debito": debito, b_debito = monto, boletas + elif metodo == "credito": credito, b_credito = monto, boletas + elif metodo == "mp": mp, b_mp = monto, boletas + elif metodo == "efectivo": efectivo, b_efectivo = monto, boletas + + tipo_registro = "Reemplazo histórico" if es_reemplazo else "Turno histórico" + + # === LÓGICA DE GASTOS RANDOM === + # 15% de probabilidad de tener un gasto en el turno (entre $2.000 y $15.000) + gastos = 0 + if random.random() < 0.15: + gastos = random.randint(2, 15) * 1000 + tipo_registro += f" | {random.choice(motivos_gastos)}" + + c.execute(''' + INSERT INTO rendiciones + (worker_id, companion_id, modulo_id, fecha, hora_entrada, hora_salida, + companion_hora_entrada, companion_hora_salida, + venta_debito, venta_credito, venta_mp, venta_efectivo, + boletas_debito, boletas_credito, boletas_mp, boletas_efectivo, + gastos, observaciones, worker_comision, companion_comision) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?) + ''', (worker_id, companion_id, modulo_id, fecha_str, hora_entrada, hora_salida, + comp_in, comp_out, debito, credito, mp, efectivo, + b_debito, b_credito, b_mp, b_efectivo, + gastos, tipo_registro, 1 if companion_id else 0)) + + r_id = c.lastrowid + + for item in items_a_insertar: + p_id, cant, p_price, p_comm = item + c.execute(''' + INSERT INTO rendicion_items (rendicion_id, producto_id, cantidad, precio_historico, comision_historica) + VALUES (?, ?, ?, ?, ?) + ''', (r_id, p_id, cant, p_price, p_comm)) + + rendiciones_creadas += 1 + + conn.commit() + conn.close() + print(f"Éxito: Se inyectaron {rendiciones_creadas} rendiciones para TODOS los módulos.") + +if __name__ == '__main__': + generar_historico_definitivo(180) \ No newline at end of file diff --git a/routes_admin.py b/routes_admin.py index 80c1918..60c9488 100644 --- a/routes_admin.py +++ b/routes_admin.py @@ -1,9 +1,10 @@ import sqlite3 -from flask import render_template, request, redirect, url_for, flash, session +from flask import app, render_template, request, redirect, url_for, flash, session from werkzeug.security import generate_password_hash -from datetime import date +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 +import calendar def register_admin_routes(app): @app.route('/admin/workers', methods=['GET', 'POST']) @@ -299,16 +300,17 @@ def register_admin_routes(app): c = conn.cursor() c.execute(''' - SELECT r.id, r.fecha, w.name, m.name, - r.venta_debito, r.venta_credito, r.venta_mp, r.venta_efectivo, r.gastos, r.observaciones, - c_w.name, r.worker_id, r.companion_id, r.modulo_id, - r.worker_comision, r.companion_comision - FROM rendiciones r - JOIN workers w ON r.worker_id = w.id - JOIN modulos m ON r.modulo_id = m.id - LEFT JOIN workers c_w ON r.companion_id = c_w.id - ORDER BY r.fecha DESC, r.id DESC - ''') + SELECT r.id, r.fecha, w.name, m.name, + r.venta_debito, r.venta_credito, r.venta_mp, r.venta_efectivo, r.gastos, r.observaciones, + c_w.name, r.worker_id, r.companion_id, r.modulo_id, + r.worker_comision, r.companion_comision, + r.boletas_debito, r.boletas_credito, r.boletas_mp, r.boletas_efectivo + FROM rendiciones r + JOIN workers w ON r.worker_id = w.id + JOIN modulos m ON r.modulo_id = m.id + LEFT JOIN workers c_w ON r.companion_id = c_w.id + ORDER BY r.fecha DESC, r.id DESC + ''') rendiciones_basicas = c.fetchall() rendiciones_completas = [] @@ -388,6 +390,12 @@ def register_admin_routes(app): credito = clean_money(request.form.get('venta_credito')) mp = clean_money(request.form.get('venta_mp')) efectivo = clean_money(request.form.get('venta_efectivo')) + + bol_debito = int(request.form.get('boletas_debito') or 0) + bol_credito = int(request.form.get('boletas_credito') or 0) + bol_mp = int(request.form.get('boletas_mp') or 0) + bol_efectivo = int(request.form.get('boletas_efectivo') or 0) + gastos = clean_money(request.form.get('gastos')) observaciones = request.form.get('observaciones', '').strip() @@ -409,11 +417,13 @@ def register_admin_routes(app): UPDATE rendiciones SET fecha=?, worker_id=?, modulo_id=?, companion_id=?, venta_debito=?, venta_credito=?, venta_mp=?, venta_efectivo=?, + boletas_debito=?, boletas_credito=?, boletas_mp=?, boletas_efectivo=?, gastos=?, observaciones=?, worker_comision=?, companion_comision=? WHERE id=? ''', (fecha, worker_id, modulo_id, companion_id, debito, credito, mp, efectivo, - gastos, observaciones, worker_comision, companion_comision, id)) + bol_debito, bol_credito, bol_mp, bol_efectivo, + gastos, observaciones, worker_comision, companion_comision, id)) conn.commit() flash("Rendición y productos actualizados correctamente.", "success") @@ -594,6 +604,106 @@ def register_admin_routes(app): 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 + + 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))) + + 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" + 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" + + 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 + + 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, diff --git a/routes_worker.py b/routes_worker.py index d0707b2..06da270 100644 --- a/routes_worker.py +++ b/routes_worker.py @@ -16,7 +16,8 @@ def register_worker_routes(app): SELECT r.id, r.fecha, w.name, m.name, r.venta_debito, r.venta_credito, r.venta_mp, r.venta_efectivo, r.gastos, r.observaciones, c_w.name, r.worker_id, r.companion_id, r.modulo_id, - r.worker_comision, r.companion_comision + r.worker_comision, r.companion_comision, + r.boletas_debito, r.boletas_credito, r.boletas_mp, r.boletas_efectivo FROM rendiciones r JOIN workers w ON r.worker_id = w.id JOIN modulos m ON r.modulo_id = m.id @@ -87,6 +88,10 @@ def register_worker_routes(app): credito = clean_and_validate(request.form.get('venta_credito')) mp = clean_and_validate(request.form.get('venta_mp')) efectivo = clean_and_validate(request.form.get('venta_efectivo')) + bol_debito = int(request.form.get('boletas_debito') or 0) + bol_credito = int(request.form.get('boletas_credito') or 0) + bol_mp = int(request.form.get('boletas_mp') or 0) + bol_efectivo = int(request.form.get('boletas_efectivo') or 0) gastos = clean_and_validate(request.form.get('gastos')) or 0 obs = request.form.get('observaciones', '').strip() companion_id = request.form.get('companion_id') @@ -116,10 +121,11 @@ def register_worker_routes(app): c.execute('''INSERT INTO rendiciones (worker_id, companion_id, modulo_id, fecha, hora_entrada, hora_salida, companion_hora_entrada, companion_hora_salida, - venta_debito, venta_credito, venta_mp, venta_efectivo, gastos, observaciones, worker_comision, companion_comision) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + venta_debito, venta_credito, venta_mp, venta_efectivo, boletas_debito, boletas_credito, boletas_mp, boletas_efectivo, gastos, observaciones, worker_comision, companion_comision) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', (session['user_id'], companion_id, modulo_id, fecha, hora_entrada, hora_salida, companion_hora_entrada, companion_hora_salida, - debito, credito, mp, efectivo, gastos, obs, worker_comision, companion_comision)) + debito, credito, mp, efectivo, bol_debito, bol_credito, bol_mp, bol_efectivo, gastos, obs, worker_comision, companion_comision)) + rendicion_id = c.lastrowid for key, value in request.form.items(): diff --git a/templates/admin_rendiciones.html b/templates/admin_rendiciones.html index 0232c32..4f118aa 100644 --- a/templates/admin_rendiciones.html +++ b/templates/admin_rendiciones.html @@ -48,8 +48,8 @@ - {{ rendicion_detail_modal(r, r[16], r[17], r[18]) }} - {{ edit_rendicion_modal(r, r[16], workers, modulos) }} + {{ rendicion_detail_modal(r, r[20], r[21], r[22]) }} + {{ edit_rendicion_modal(r, r[20], workers, modulos) }} {{ confirm_modal( id='deleteRendicion' ~ r[0], diff --git a/templates/admin_report_horarios.html b/templates/admin_report_horarios.html new file mode 100644 index 0000000..e22672f --- /dev/null +++ b/templates/admin_report_horarios.html @@ -0,0 +1,105 @@ +{% extends "macros/base.html" %} + +{% block title %}Reporte: Horarios - {{ modulo_name }}{% endblock %} + +{% block styles %} + +{% endblock %} + +{% block content %} +
+
+ + Volver al Menú + +

Control de Horarios

+
+
+
{{ modulo_name }}
+
Período: {{ mes_nombre }}
+
+
+ +{% if workers_data %} +
+
+
+ + + + + {% for w_id, data in workers_data.items() %} + + {% endfor %} + + + {% for w_id, data in workers_data.items() %} + + + + {% endfor %} + + + + {% for dia in dias_en_periodo %} + + + {% for w_id, data in workers_data.items() %} + {% set turno = data.dias.get(dia.num) %} + + + + {% endfor %} + + {% endfor %} + + + + + {% for w_id, data in workers_data.items() %} + + + {% endfor %} + + +
Día{{ data.name }}
EntSalHrs
+
+ {{ dia.name }} + {{ dia.num }} +
+
{{ turno.in if turno else '-' }}{{ turno.out if turno else '-' }}{{ turno.hrs if turno else '0:00' }}
TOTALHoras Totales:{{ data.total_hrs_str }}
+
+
+
+{% else %} +
+ No hay trabajadores asignados ni registros de horarios para este módulo. +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/macros/modals.html b/templates/macros/modals.html index 0f31344..8893160 100644 --- a/templates/macros/modals.html +++ b/templates/macros/modals.html @@ -239,19 +239,19 @@
- Débito: + Débito (x{{ rendicion[16] }}): ${{ "{:,.0f}".format(rendicion[4] or 0).replace(',', '.') }}
- Crédito: + Crédito (x{{ rendicion[17] }}): ${{ "{:,.0f}".format(rendicion[5] or 0).replace(',', '.') }}
- Mercado Pago: + Mercado Pago (x{{ rendicion[18] }}): ${{ "{:,.0f}".format(rendicion[6] or 0).replace(',', '.') }}
- Efectivo: + Efectivo (x{{ rendicion[19] }}): ${{ "{:,.0f}".format(rendicion[7] or 0).replace(',', '.') }}
@@ -398,22 +398,38 @@ -
+
- + +
+ Boletas + +
- + +
+ Boletas + +
- + +
+ Boletas + +
- + +
+ Boletas + +
@@ -502,7 +518,7 @@
Control de Horarios

Registro de horas de entrada y salida de los trabajadores y acompañantes.

- + Generar Reporte diff --git a/templates/worker_dashboard.html b/templates/worker_dashboard.html index 09c5252..bd41480 100644 --- a/templates/worker_dashboard.html +++ b/templates/worker_dashboard.html @@ -131,32 +131,48 @@
- -
+ +
$
+
+ Nº Boletas + +
- -
+ +
$
-
-
- -
- $ - +
+ Nº Boletas +
- -
+ +
+ $ + +
+
+ Nº Boletas + +
+
+
+ +
$
+
+ Nº Boletas + +
diff --git a/templates/worker_history.html b/templates/worker_history.html index c7b2e4c..227a40d 100644 --- a/templates/worker_history.html +++ b/templates/worker_history.html @@ -64,7 +64,7 @@ - {{ rendicion_detail_modal(r, r[16], r[17], r[18]) }} + {{ rendicion_detail_modal(r, r[20], r[21], r[22]) }} {% else %}