universal filters v1 + more sample data

This commit is contained in:
2026-03-28 01:11:23 -03:00
parent 41ceba5237
commit 313ae9a15a
9 changed files with 351 additions and 428 deletions

View File

@@ -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()
hoy = datetime.date.today()
dias_totales = 365 * 2 + hoy.timetuple().tm_yday
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))
for i in range(dias_totales):
fecha_actual = (hoy - datetime.timedelta(days=i))
fecha_str = fecha_actual.isoformat()
rendicion_id = c.lastrowid
num_modulos_hoy = random.randint(3, 6)
mods_activos = random.sample(range(len(modulos_data)), k=num_modulos_hoy)
# 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 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]
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))
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()

View File

@@ -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/<int:modulo_id>')
@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()
mod_name, workers_list, anios_list = get_common_report_data(c, modulo_id)
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]
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('''
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(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()}
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()
_, 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()
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)
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)
@app.route('/admin/reportes/modulo/<int:modulo_id>/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
# 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)
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)
@app.route('/admin/reportes/modulo/<int:modulo_id>/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
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)
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)
@app.route('/admin/reportes/modulo/<int:modulo_id>/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()
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)
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)
@app.route('/admin/reportes/modulo/<int:modulo_id>/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)
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)

View File

@@ -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 @@
<div class="text-muted"><small>Período: {{ mes_nombre }}</small></div>
</div>
</div>
{{ 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
) }}
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive table-container custom-scrollbar">

View File

@@ -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 @@
<div class="text-muted"><small>Período: {{ mes_nombre }}</small></div>
</div>
</div>
{{ 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
) }}
<div class="card shadow-sm border-0">
<div class="card-body">
{% if workers_data %}

View File

@@ -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 @@
<div class="text-muted"><small>Período: {{ mes_nombre }}</small></div>
</div>
</div>
{{ 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 %}
<div class="card shadow-sm border-0">
<div class="card-body p-0">
@@ -60,8 +70,8 @@
</tr>
<tr>
{% for w_id, data in workers_data.items() %}
<th class="bg-body-tertiary">Ent</th>
<th class="bg-body-tertiary">Sal</th>
<th class="bg-body-tertiary">Entrada</th>
<th class="bg-body-tertiary">Salida</th>
<th class="bg-success-subtle text-success">Hrs</th>
{% endfor %}
</tr>

View File

@@ -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 @@
<div class="text-muted"><small>Período: {{ mes_nombre }}</small></div>
</div>
</div>
{{ 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
) }}
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive table-container custom-scrollbar">

View File

@@ -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 @@
</div>
</div>
{{ 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
) }}
<div class="card shadow-sm border-0">
<div class="card-header border-0 bg-transparent d-flex justify-content-between align-items-center pb-0">
<span class="fw-bold text-muted text-uppercase"><i class="bi bi-calendar3 me-1"></i> Desglose Diario</span>

View File

@@ -574,3 +574,45 @@
</div>
</div>
{% endmacro %}
{% macro report_filters(action_url, workers, selected_worker, selected_dia, meses, mes_act, anios, anio_act) %}
<div class="card shadow-sm mb-4 border-0 bg-dark-subtle">
<div class="card-body p-3">
<form method="GET" action="{{ action_url }}" class="row g-2 align-items-end">
<div class="col-md-2">
<label class="form-label small text-muted mb-1">Año/Mes</label>
<div class="input-group input-group-sm">
<select name="anio" class="form-select">
{% for a in anios %}<option value="{{ a }}" {{ 'selected' if a|string == anio_act|string }}>{{ a }}</option>{% endfor %}
</select>
<select name="mes" class="form-select">
{% for m_num, m_name in meses %}<option value="{{ m_num }}" {{ 'selected' if m_num == mes_act }}>{{ m_name }}</option>{% endfor %}
</select>
</div>
</div>
<div class="col-md-2">
<label class="form-label small text-muted mb-1">Día (Opcional)</label>
<select name="dia" class="form-select form-select-sm">
<option value="">Todos los días</option>
{% for d in range(1, 32) %}
{% set d_str = "%02d"|format(d) %}
<option value="{{ d_str }}" {{ 'selected' if d_str == selected_dia }}>{{ d_str }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label small text-muted mb-1">Trabajador</label>
<select name="worker_id" class="form-select form-select-sm">
<option value="">Todos los trabajadores</option>
{% for w in workers %}
<option value="{{ w[0] }}" {{ 'selected' if w[0]|string == selected_worker|string }}>{{ w[1] }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary btn-sm w-100"><i class="bi bi-filter"></i> Aplicar Filtros</button>
</div>
</form>
</div>
</div>
{% endmacro %}

View File

@@ -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))
@@ -54,3 +55,32 @@ def admin_required(f):
return redirect(url_for('index'))
return f(*args, **kwargs)
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