modified: database.py
new file: generar_unificado.py modified: routes_admin.py modified: routes_worker.py modified: templates/admin_rendiciones.html new file: templates/admin_report_horarios.html modified: templates/macros/modals.html modified: templates/worker_dashboard.html modified: templates/worker_history.html
This commit is contained in:
@@ -163,6 +163,10 @@ def init_db():
|
|||||||
venta_credito INTEGER DEFAULT 0,
|
venta_credito INTEGER DEFAULT 0,
|
||||||
venta_mp INTEGER DEFAULT 0,
|
venta_mp INTEGER DEFAULT 0,
|
||||||
venta_efectivo 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,
|
gastos INTEGER DEFAULT 0,
|
||||||
observaciones TEXT,
|
observaciones TEXT,
|
||||||
FOREIGN KEY (worker_id) REFERENCES workers(id),
|
FOREIGN KEY (worker_id) REFERENCES workers(id),
|
||||||
|
|||||||
185
generar_unificado.py
Normal file
185
generar_unificado.py
Normal file
@@ -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)
|
||||||
136
routes_admin.py
136
routes_admin.py
@@ -1,9 +1,10 @@
|
|||||||
import sqlite3
|
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 werkzeug.security import generate_password_hash
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from database import get_db_connection
|
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
|
||||||
|
import calendar
|
||||||
|
|
||||||
def register_admin_routes(app):
|
def register_admin_routes(app):
|
||||||
@app.route('/admin/workers', methods=['GET', 'POST'])
|
@app.route('/admin/workers', methods=['GET', 'POST'])
|
||||||
@@ -299,16 +300,17 @@ def register_admin_routes(app):
|
|||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
c.execute('''
|
c.execute('''
|
||||||
SELECT r.id, r.fecha, w.name, m.name,
|
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,
|
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,
|
c_w.name, r.worker_id, r.companion_id, r.modulo_id,
|
||||||
r.worker_comision, r.companion_comision
|
r.worker_comision, r.companion_comision,
|
||||||
FROM rendiciones r
|
r.boletas_debito, r.boletas_credito, r.boletas_mp, r.boletas_efectivo
|
||||||
JOIN workers w ON r.worker_id = w.id
|
FROM rendiciones r
|
||||||
JOIN modulos m ON r.modulo_id = m.id
|
JOIN workers w ON r.worker_id = w.id
|
||||||
LEFT JOIN workers c_w ON r.companion_id = c_w.id
|
JOIN modulos m ON r.modulo_id = m.id
|
||||||
ORDER BY r.fecha DESC, r.id DESC
|
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_basicas = c.fetchall()
|
||||||
|
|
||||||
rendiciones_completas = []
|
rendiciones_completas = []
|
||||||
@@ -388,6 +390,12 @@ def register_admin_routes(app):
|
|||||||
credito = clean_money(request.form.get('venta_credito'))
|
credito = clean_money(request.form.get('venta_credito'))
|
||||||
mp = clean_money(request.form.get('venta_mp'))
|
mp = clean_money(request.form.get('venta_mp'))
|
||||||
efectivo = clean_money(request.form.get('venta_efectivo'))
|
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'))
|
gastos = clean_money(request.form.get('gastos'))
|
||||||
observaciones = request.form.get('observaciones', '').strip()
|
observaciones = request.form.get('observaciones', '').strip()
|
||||||
|
|
||||||
@@ -409,11 +417,13 @@ def register_admin_routes(app):
|
|||||||
UPDATE rendiciones
|
UPDATE rendiciones
|
||||||
SET fecha=?, worker_id=?, modulo_id=?, companion_id=?,
|
SET fecha=?, worker_id=?, modulo_id=?, companion_id=?,
|
||||||
venta_debito=?, venta_credito=?, venta_mp=?, venta_efectivo=?,
|
venta_debito=?, venta_credito=?, venta_mp=?, venta_efectivo=?,
|
||||||
|
boletas_debito=?, boletas_credito=?, boletas_mp=?, boletas_efectivo=?,
|
||||||
gastos=?, observaciones=?, worker_comision=?, companion_comision=?
|
gastos=?, observaciones=?, worker_comision=?, companion_comision=?
|
||||||
WHERE id=?
|
WHERE id=?
|
||||||
''', (fecha, worker_id, modulo_id, companion_id,
|
''', (fecha, worker_id, modulo_id, companion_id,
|
||||||
debito, credito, mp, efectivo,
|
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()
|
conn.commit()
|
||||||
flash("Rendición y productos actualizados correctamente.", "success")
|
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)]
|
dias_en_periodo = [f'{d:02}' for d in range(1, 32)]
|
||||||
|
|
||||||
return render_template('admin_report_comisiones.html',
|
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/<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
|
||||||
|
|
||||||
|
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,
|
modulo_name=modulo_name,
|
||||||
mes_nombre=f'{mes_actual:02}/{anio_actual}',
|
mes_nombre=f'{mes_actual:02}/{anio_actual}',
|
||||||
workers_data=workers_data,
|
workers_data=workers_data,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ def register_worker_routes(app):
|
|||||||
SELECT r.id, r.fecha, w.name, m.name,
|
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,
|
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,
|
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
|
FROM rendiciones r
|
||||||
JOIN workers w ON r.worker_id = w.id
|
JOIN workers w ON r.worker_id = w.id
|
||||||
JOIN modulos m ON r.modulo_id = m.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'))
|
credito = clean_and_validate(request.form.get('venta_credito'))
|
||||||
mp = clean_and_validate(request.form.get('venta_mp'))
|
mp = clean_and_validate(request.form.get('venta_mp'))
|
||||||
efectivo = clean_and_validate(request.form.get('venta_efectivo'))
|
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
|
gastos = clean_and_validate(request.form.get('gastos')) or 0
|
||||||
obs = request.form.get('observaciones', '').strip()
|
obs = request.form.get('observaciones', '').strip()
|
||||||
companion_id = request.form.get('companion_id')
|
companion_id = request.form.get('companion_id')
|
||||||
@@ -116,10 +121,11 @@ def register_worker_routes(app):
|
|||||||
|
|
||||||
c.execute('''INSERT INTO rendiciones
|
c.execute('''INSERT INTO rendiciones
|
||||||
(worker_id, companion_id, modulo_id, fecha, hora_entrada, hora_salida, companion_hora_entrada, companion_hora_salida,
|
(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)
|
venta_debito, venta_credito, venta_mp, venta_efectivo, boletas_debito, boletas_credito, boletas_mp, boletas_efectivo, gastos, observaciones, worker_comision, companion_comision)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||||
(session['user_id'], companion_id, modulo_id, fecha, hora_entrada, hora_salida, companion_hora_entrada, companion_hora_salida,
|
(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
|
rendicion_id = c.lastrowid
|
||||||
|
|
||||||
for key, value in request.form.items():
|
for key, value in request.form.items():
|
||||||
|
|||||||
@@ -48,8 +48,8 @@
|
|||||||
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#deleteRendicion{{ r[0] }}" title="Eliminar Rendición"><i class="bi bi-trash"></i></button>
|
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#deleteRendicion{{ r[0] }}" title="Eliminar Rendición"><i class="bi bi-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ rendicion_detail_modal(r, r[16], r[17], r[18]) }}
|
{{ rendicion_detail_modal(r, r[20], r[21], r[22]) }}
|
||||||
{{ edit_rendicion_modal(r, r[16], workers, modulos) }}
|
{{ edit_rendicion_modal(r, r[20], workers, modulos) }}
|
||||||
|
|
||||||
{{ confirm_modal(
|
{{ confirm_modal(
|
||||||
id='deleteRendicion' ~ r[0],
|
id='deleteRendicion' ~ r[0],
|
||||||
|
|||||||
105
templates/admin_report_horarios.html
Normal file
105
templates/admin_report_horarios.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{% extends "macros/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Reporte: Horarios - {{ modulo_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<style>
|
||||||
|
.table-container {
|
||||||
|
max-height: 75vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-col {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border-right: 2px solid var(--bs-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
box-shadow: inset 0 -1px 0 var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th.sticky-col {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('admin_reportes_index') }}" class="btn btn-outline-secondary btn-sm mb-2">
|
||||||
|
<i class="bi bi-arrow-left"></i> Volver al Menú
|
||||||
|
</a>
|
||||||
|
<h2>Control de Horarios</h2>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div><strong class="text-primary fs-5">{{ modulo_name }}</strong></div>
|
||||||
|
<div class="text-muted"><small>Período: {{ mes_nombre }}</small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if workers_data %}
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive table-container custom-scrollbar">
|
||||||
|
<table class="table table-bordered table-hover table-sm mb-0 text-center text-nowrap align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2" class="sticky-col bg-dark text-white align-middle" style="min-width: 70px;">Día</th>
|
||||||
|
{% for w_id, data in workers_data.items() %}
|
||||||
|
<th colspan="3" class="bg-secondary text-white">{{ data.name }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</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-success-subtle text-success">Hrs</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for dia in dias_en_periodo %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold sticky-col p-0 align-middle">
|
||||||
|
<div class="d-flex justify-content-between align-items-center px-2 py-1 h-100">
|
||||||
|
<span class="text-muted" style="font-weight: 500;">{{ dia.name }}</span>
|
||||||
|
<span>{{ dia.num }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% for w_id, data in workers_data.items() %}
|
||||||
|
{% set turno = data.dias.get(dia.num) %}
|
||||||
|
<td>{{ turno.in if turno else '-' }}</td>
|
||||||
|
<td>{{ turno.out if turno else '-' }}</td>
|
||||||
|
<td class="bg-success-subtle fw-bold text-success">{{ turno.hrs if turno else '0:00' }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th class="sticky-col bg-dark text-white">TOTAL</th>
|
||||||
|
{% for w_id, data in workers_data.items() %}
|
||||||
|
<td colspan="2" class="text-end pe-2">Horas Totales:</td>
|
||||||
|
<td class="fs-6 text-success fw-bold">{{ data.total_hrs_str }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info text-center shadow-sm mt-4">
|
||||||
|
No hay trabajadores asignados ni registros de horarios para este módulo.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -239,19 +239,19 @@
|
|||||||
</dl>
|
</dl>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">Débito:</span>
|
<span class="text-muted">Débito <small>(x{{ rendicion[16] }})</small>:</span>
|
||||||
<span>${{ "{:,.0f}".format(rendicion[4] or 0).replace(',', '.') }}</span>
|
<span>${{ "{:,.0f}".format(rendicion[4] or 0).replace(',', '.') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">Crédito:</span>
|
<span class="text-muted">Crédito <small>(x{{ rendicion[17] }})</small>:</span>
|
||||||
<span>${{ "{:,.0f}".format(rendicion[5] or 0).replace(',', '.') }}</span>
|
<span>${{ "{:,.0f}".format(rendicion[5] or 0).replace(',', '.') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">Mercado Pago:</span>
|
<span class="text-muted">Mercado Pago <small>(x{{ rendicion[18] }})</small>:</span>
|
||||||
<span>${{ "{:,.0f}".format(rendicion[6] or 0).replace(',', '.') }}</span>
|
<span>${{ "{:,.0f}".format(rendicion[6] or 0).replace(',', '.') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<div class="d-flex justify-content-between mb-2">
|
||||||
<span class="text-muted">Efectivo:</span>
|
<span class="text-muted">Efectivo <small>(x{{ rendicion[19] }})</small>:</span>
|
||||||
<span>${{ "{:,.0f}".format(rendicion[7] or 0).replace(',', '.') }}</span>
|
<span>${{ "{:,.0f}".format(rendicion[7] or 0).replace(',', '.') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -398,22 +398,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-1 mt-1 border-top pt-2">
|
<div class="row g-2 mt-1 border-top pt-2">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="small text-muted mb-0">Débito</label>
|
<label class="small text-muted mb-0">Débito</label>
|
||||||
<input type="text" class="form-control form-control-sm text-end money-input" id="edit_debito_{{ rendicion[0] }}" name="venta_debito" value="{{ '{:,.0f}'.format(rendicion[4] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_debito_{{ rendicion[0] }}" name="venta_debito" value="{{ '{:,.0f}'.format(rendicion[4] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
|
||||||
|
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_debito" value="{{ rendicion[16] or 0 }}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="small text-muted mb-0">Crédito</label>
|
<label class="small text-muted mb-0">Crédito</label>
|
||||||
<input type="text" class="form-control form-control-sm text-end money-input" id="edit_credito_{{ rendicion[0] }}" name="venta_credito" value="{{ '{:,.0f}'.format(rendicion[5] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_credito_{{ rendicion[0] }}" name="venta_credito" value="{{ '{:,.0f}'.format(rendicion[5] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
|
||||||
|
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_credito" value="{{ rendicion[17] or 0 }}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="small text-muted mb-0">Mercado Pago</label>
|
<label class="small text-muted mb-0">Mercado Pago</label>
|
||||||
<input type="text" class="form-control form-control-sm text-end money-input" id="edit_mp_{{ rendicion[0] }}" name="venta_mp" value="{{ '{:,.0f}'.format(rendicion[6] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_mp_{{ rendicion[0] }}" name="venta_mp" value="{{ '{:,.0f}'.format(rendicion[6] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
|
||||||
|
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_mp" value="{{ rendicion[18] or 0 }}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="small text-muted mb-0">Efectivo</label>
|
<label class="small text-muted mb-0">Efectivo</label>
|
||||||
<input type="text" class="form-control form-control-sm text-end money-input" id="edit_efectivo_{{ rendicion[0] }}" name="venta_efectivo" value="{{ '{:,.0f}'.format(rendicion[7] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
<input type="text" class="form-control form-control-sm text-end money-input mb-1" id="edit_efectivo_{{ rendicion[0] }}" name="venta_efectivo" value="{{ '{:,.0f}'.format(rendicion[7] or 0).replace(',', '.') }}" oninput="calcTotalEdit({{ rendicion[0] }})">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-dark border-secondary text-muted" style="font-size: 0.7em;">Boletas</span>
|
||||||
|
<input type="number" class="form-control text-center bg-dark text-white border-secondary" name="boletas_efectivo" value="{{ rendicion[19] or 0 }}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -502,7 +518,7 @@
|
|||||||
<h5 class="card-title mb-0">Control de Horarios</h5>
|
<h5 class="card-title mb-0">Control de Horarios</h5>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small flex-grow-1">Registro de horas de entrada y salida de los trabajadores y acompañantes.</p>
|
<p class="text-muted small flex-grow-1">Registro de horas de entrada y salida de los trabajadores y acompañantes.</p>
|
||||||
<button class="btn btn-outline-warning w-100 mt-3" onclick="alert('Reporte en construcción')">Generar Reporte</button>
|
<a href="{{ url_for('report_modulo_horarios', modulo_id=modulo_id) }}" class="btn btn-warning w-100 mt-3">Generar Reporte</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -131,32 +131,48 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Venta Débito</label>
|
<label class="form-label">Débito</label>
|
||||||
<div class="input-group">
|
<div class="input-group mb-2">
|
||||||
<span class="input-group-text">$</span>
|
<span class="input-group-text">$</span>
|
||||||
<input type="text" class="form-control money-input sale-input" name="venta_debito" id="venta_debito" required>
|
<input type="text" class="form-control money-input sale-input" name="venta_debito" id="venta_debito" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-secondary text-white border-0">Nº Boletas</span>
|
||||||
|
<input type="number" class="form-control" name="boletas_debito" min="0" placeholder="0">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Venta Crédito</label>
|
<label class="form-label">Crédito</label>
|
||||||
<div class="input-group">
|
<div class="input-group mb-2">
|
||||||
<span class="input-group-text">$</span>
|
<span class="input-group-text">$</span>
|
||||||
<input type="text" class="form-control money-input sale-input" name="venta_credito" id="venta_credito" required>
|
<input type="text" class="form-control money-input sale-input" name="venta_credito" id="venta_credito" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="input-group input-group-sm">
|
||||||
<div class="col-md-3">
|
<span class="input-group-text bg-secondary text-white border-0">Nº Boletas</span>
|
||||||
<label class="form-label">Venta Mercado Pago</label>
|
<input type="number" class="form-control" name="boletas_credito" min="0" placeholder="0">
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text">$</span>
|
|
||||||
<input type="text" class="form-control money-input sale-input" placeholder="0" name="venta_mp" id="venta_mp" required>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Venta Efectivo</label>
|
<label class="form-label">Mercado Pago</label>
|
||||||
<div class="input-group">
|
<div class="input-group mb-2">
|
||||||
|
<span class="input-group-text">$</span>
|
||||||
|
<input type="text" class="form-control money-input sale-input" placeholder="0" name="venta_mp" id="venta_mp" required>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-secondary text-white border-0">Nº Boletas</span>
|
||||||
|
<input type="number" class="form-control" name="boletas_mp" min="0" placeholder="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Efectivo</label>
|
||||||
|
<div class="input-group mb-2">
|
||||||
<span class="input-group-text">$</span>
|
<span class="input-group-text">$</span>
|
||||||
<input type="text" class="form-control money-input sale-input" placeholder="0" name="venta_efectivo" id="venta_efectivo" required>
|
<input type="text" class="form-control money-input sale-input" placeholder="0" name="venta_efectivo" id="venta_efectivo" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-secondary text-white border-0">Nº Boletas</span>
|
||||||
|
<input type="number" class="form-control" name="boletas_efectivo" min="0" placeholder="0">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<button type="button" class="btn btn-sm btn-info text-white" data-bs-toggle="modal" data-bs-target="#viewRendicion{{ r[0] }}" title="Ver Detalle">
|
<button type="button" class="btn btn-sm btn-info text-white" data-bs-toggle="modal" data-bs-target="#viewRendicion{{ r[0] }}" title="Ver Detalle">
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
{{ rendicion_detail_modal(r, r[16], r[17], r[18]) }}
|
{{ rendicion_detail_modal(r, r[20], r[21], r[22]) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
Reference in New Issue
Block a user