diff --git a/app.py b/app.py new file mode 100644 index 0000000..deac336 --- /dev/null +++ b/app.py @@ -0,0 +1,619 @@ +from flask import Flask, render_template, request, redirect, url_for, flash, session +from werkzeug.security import generate_password_hash, check_password_hash +import sqlite3 +import re +import random +import string +from functools import wraps +from datetime import date + +app = Flask(__name__) +app.secret_key = "super_secret_dev_key" +DB_NAME = "rendiciones.db" + +# --- Database & Helpers --- + +def init_db(): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + # 1. Zonas + c.execute('''CREATE TABLE IF NOT EXISTS zonas + (id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL)''') + + # 2. Modulos (Belong to Zonas) + c.execute('''CREATE TABLE IF NOT EXISTS modulos + (id INTEGER PRIMARY KEY AUTOINCREMENT, + zona_id INTEGER NOT NULL, + name TEXT NOT NULL, + FOREIGN KEY (zona_id) REFERENCES zonas(id))''') + + # 3. Productos (Belong to Zonas to enforce unique pricing/commissions per zone) + c.execute('''CREATE TABLE IF NOT EXISTS productos + (id INTEGER PRIMARY KEY AUTOINCREMENT, + zona_id INTEGER NOT NULL, + name TEXT NOT NULL, + price REAL NOT NULL, + commission REAL NOT NULL, + FOREIGN KEY (zona_id) REFERENCES zonas(id))''') + + # 4. Workers (Now tied to a Modulo) + # Added modulo_id. It can be NULL for the system admin. + c.execute('''CREATE TABLE IF NOT EXISTS workers + (id INTEGER PRIMARY KEY AUTOINCREMENT, + rut TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + phone TEXT NOT NULL, + password_hash TEXT NOT NULL, + is_admin BOOLEAN DEFAULT 0, + modulo_id INTEGER, + FOREIGN KEY (modulo_id) REFERENCES modulos(id))''') + + # 5. Rendiciones (The main form headers) + c.execute('''CREATE TABLE IF NOT EXISTS rendiciones + (id INTEGER PRIMARY KEY AUTOINCREMENT, + worker_id INTEGER NOT NULL, + modulo_id INTEGER NOT NULL, + fecha DATE NOT NULL, + turno TEXT NOT NULL, + venta_tarjeta INTEGER DEFAULT 0, + venta_mp INTEGER DEFAULT 0, + venta_efectivo INTEGER DEFAULT 0, + gastos INTEGER DEFAULT 0, + observaciones TEXT, + FOREIGN KEY (worker_id) REFERENCES workers(id), + FOREIGN KEY (modulo_id) REFERENCES modulos(id))''') + + # 6. Rendicion Items (The individual product quantities sold) + c.execute('''CREATE TABLE IF NOT EXISTS rendicion_items + (id INTEGER PRIMARY KEY AUTOINCREMENT, + rendicion_id INTEGER NOT NULL, + producto_id INTEGER NOT NULL, + cantidad INTEGER NOT NULL, + precio_historico INTEGER NOT NULL, + comision_historica INTEGER NOT NULL, + FOREIGN KEY (rendicion_id) REFERENCES rendiciones(id), + FOREIGN KEY (producto_id) REFERENCES productos(id))''') + + # Ensure default admin exists + 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() + +def generate_random_password(length=6): + """Generates a simple alphanumeric password.""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + +def validate_rut(rut): + rut_clean = re.sub(r'[^0-9kK]', '', rut).upper() + if len(rut_clean) < 2: return False + body, dv = rut_clean[:-1], rut_clean[-1] + try: + body_reversed = reversed(body) + total = sum(int(digit) * factor for digit, factor in zip(body_reversed, [2, 3, 4, 5, 6, 7, 2, 3, 4, 5, 6, 7])) + calc_dv = 11 - (total % 11) + if calc_dv == 11: calc_dv = '0' + elif calc_dv == 10: calc_dv = 'K' + else: calc_dv = str(calc_dv) + return calc_dv == dv + except ValueError: + return False + +def format_rut(rut): + rut_clean = re.sub(r'[^0-9kK]', '', rut).upper() + body, dv = rut_clean[:-1], rut_clean[-1] + body_fmt = f"{int(body):,}".replace(',', '.') + return f"{body_fmt}-{dv}" + +def validate_phone(phone): + phone_clean = re.sub(r'\D', '', phone) + if phone_clean.startswith('56'): phone_clean = phone_clean[2:] + return len(phone_clean) == 9 + +def format_phone(phone): + phone_clean = re.sub(r'\D', '', phone) + if phone_clean.startswith('56'): phone_clean = phone_clean[2:] + return f"+56 {phone_clean[-9]} {phone_clean[-8:-4]} {phone_clean[-4:]}" + +# --- Decorators --- + +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session or not session.get('is_admin'): + flash("Acceso denegado. Se requieren permisos de administrador.", "danger") + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + +# --- Routes --- + +@app.route('/', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + raw_rut = request.form['rut'] + password = request.form['password'] + + rut = format_rut(raw_rut) if validate_rut(raw_rut) else raw_rut + + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + c.execute("SELECT id, password_hash, is_admin FROM workers WHERE rut = ?", (rut,)) + user = c.fetchone() + conn.close() + + if user and check_password_hash(user[1], password): + session['user_id'] = user[0] + session['is_admin'] = user[2] + session['rut'] = rut # Stores RUT for the navbar + + if user[2]: + # This line changed: Redirects to the rendiciones list instead of workers + return redirect(url_for('admin_rendiciones')) + else: + return redirect(url_for('worker_dashboard')) + else: + flash("RUT o contraseña incorrectos.", "danger") + + return render_template('login.html') + +@app.route('/logout') +def logout(): + session.clear() + return redirect(url_for('login')) + +@app.route('/dashboard', methods=['GET', 'POST']) +@login_required +def worker_dashboard(): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + # 1. Identify the worker and their assigned location + c.execute('''SELECT w.modulo_id, m.name, z.id, z.name + FROM workers w + JOIN modulos m ON w.modulo_id = m.id + JOIN zonas z ON m.zona_id = z.id + WHERE w.id = ?''', (session['user_id'],)) + worker_info = c.fetchone() + + if not worker_info: + conn.close() + return "Error: No tienes un módulo asignado. Contacta al administrador." + + modulo_id, modulo_name, zona_id, zona_name = worker_info + + # 2. Handle form submission + if request.method == 'POST': + fecha = request.form.get('fecha') + turno = request.form.get('turno') + + # Clean the money inputs (strip dots) + def clean_money(val): + return int(val.replace('.', '')) if val else 0 + + tarjeta = clean_money(request.form.get('venta_tarjeta')) + mp = clean_money(request.form.get('venta_mp')) + efectivo = clean_money(request.form.get('venta_efectivo')) + gastos = clean_money(request.form.get('gastos')) + obs = request.form.get('observaciones', '').strip() + + # Insert Header + c.execute('''INSERT INTO rendiciones + (worker_id, modulo_id, fecha, turno, venta_tarjeta, venta_mp, venta_efectivo, gastos, observaciones) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (session['user_id'], modulo_id, fecha, turno, tarjeta, mp, efectivo, gastos, obs)) + rendicion_id = c.lastrowid + + # Insert Products (Only those with a quantity > 0) + for key, value in request.form.items(): + if key.startswith('qty_') and value and int(value) > 0: + prod_id = int(key.split('_')[1]) + cantidad = int(value) + + # Fetch current price/commission to snapshot it + c.execute("SELECT price, commission FROM productos WHERE id = ?", (prod_id,)) + prod_data = c.fetchone() + + if prod_data: + c.execute('''INSERT INTO rendicion_items + (rendicion_id, producto_id, cantidad, precio_historico, comision_historica) + VALUES (?, ?, ?, ?, ?)''', + (rendicion_id, prod_id, cantidad, prod_data[0], prod_data[1])) + + conn.commit() + flash("Rendición enviada exitosamente.", "success") + return redirect(url_for('worker_dashboard')) + + # 3. Load Products for the GET request + c.execute("SELECT id, name, price, commission FROM productos WHERE zona_id = ? ORDER BY name", (zona_id,)) + productos = c.fetchall() + conn.close() + + # Determine if this module uses commissions (Peppermint) or not (Candy) + has_commission = any(prod[3] > 0 for prod in productos) + + return render_template('worker_dashboard.html', + modulo_name=modulo_name, + zona_name=zona_name, + productos=productos, + has_commission=has_commission, + today=date.today().strftime('%Y-%m-%d')) + +@app.route('/admin/workers', methods=['GET', 'POST']) +@admin_required +def manage_workers(): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + form_data = {} + + if request.method == 'POST': + raw_rut = request.form['rut'] + raw_phone = request.form['phone'] + name = request.form['name'].strip() + modulo_id = request.form.get('modulo_id') + form_data = request.form + + if not validate_rut(raw_rut): + flash("El RUT ingresado no es válido.", "danger") + elif not validate_phone(raw_phone): + flash("El teléfono debe tener 9 dígitos válidos.", "danger") + elif not modulo_id: + flash("Debes asignar un módulo al trabajador.", "danger") + else: + rut = format_rut(raw_rut) + phone = format_phone(raw_phone) + password = generate_random_password() + p_hash = generate_password_hash(password) + + try: + # Now inserting modulo_id + c.execute("INSERT INTO workers (rut, name, phone, password_hash, is_admin, modulo_id) VALUES (?, ?, ?, ?, 0, ?)", + (rut, name, phone, p_hash, modulo_id)) + conn.commit() + flash(f"Trabajador guardado. Contraseña temporal: {password}", "success") + return redirect(url_for('manage_workers')) + except sqlite3.IntegrityError: + flash("El RUT ya existe en el sistema.", "danger") + + # Fetch workers and JOIN their module name + c.execute('''SELECT w.id, w.rut, w.name, w.phone, m.name + FROM workers w + LEFT JOIN modulos m ON w.modulo_id = m.id + WHERE w.is_admin = 0''') + workers = c.fetchall() + + # Fetch modules and their zones for the dropdown + c.execute('''SELECT m.id, m.name, z.name + FROM modulos m + JOIN zonas z ON m.zona_id = z.id + ORDER BY z.name, m.name''') + modulos = c.fetchall() + + conn.close() + return render_template('admin_workers.html', workers=workers, form=form_data, modulos=modulos) + + +@app.route('/admin/workers/edit/', methods=['GET', 'POST']) +@admin_required +def edit_worker(id): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + if request.method == 'POST': + raw_phone = request.form['phone'] + name = request.form['name'].strip() + modulo_id = request.form.get('modulo_id') + + if not validate_phone(raw_phone): + flash("El teléfono debe tener 9 dígitos válidos.", "danger") + return redirect(url_for('edit_worker', id=id)) + elif not modulo_id: + flash("Debes seleccionar un módulo.", "danger") + return redirect(url_for('edit_worker', id=id)) + + c.execute("UPDATE workers SET name=?, phone=?, modulo_id=? WHERE id=?", + (name, format_phone(raw_phone), modulo_id, id)) + conn.commit() + flash("Trabajador actualizado exitosamente.", "success") + conn.close() + return redirect(url_for('manage_workers')) + + # Added modulo_id to SELECT + c.execute("SELECT id, rut, name, phone, modulo_id FROM workers WHERE id=?", (id,)) + worker = c.fetchone() + + c.execute('''SELECT m.id, m.name, z.name + FROM modulos m + JOIN zonas z ON m.zona_id = z.id + ORDER BY z.name, m.name''') + modulos = c.fetchall() + + conn.close() + + if not worker: return redirect(url_for('manage_workers')) + return render_template('edit_worker.html', worker=worker, modulos=modulos) +@app.route('/admin/workers/delete/', methods=['POST']) +@admin_required +def delete_worker(id): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + c.execute("DELETE FROM workers WHERE id=?", (id,)) + conn.commit() + conn.close() + flash("Trabajador eliminado.", "info") + return redirect(url_for('manage_workers')) + +@app.route('/admin/workers/reset_password/', methods=['POST']) +@admin_required +def admin_reset_password(id): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + c.execute("SELECT name FROM workers WHERE id=?", (id,)) + worker = c.fetchone() + + if not worker: + conn.close() + return redirect(url_for('manage_workers')) + + new_password = generate_random_password() + p_hash = generate_password_hash(new_password) + + c.execute("UPDATE workers SET password_hash=? WHERE id=?", (p_hash, id)) + conn.commit() + conn.close() + + flash(f"Contraseña de {worker[0]} restablecida. La nueva contraseña es: {new_password}", "warning") + return redirect(url_for('edit_worker', id=id)) + + +@app.route('/admin/estructura', methods=['GET', 'POST']) +@admin_required +def manage_structure(): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + if request.method == 'POST': + action = request.form.get('action') + + if action == 'add_zona': + name = request.form.get('zona_name').strip() + try: + c.execute("INSERT INTO zonas (name) VALUES (?)", (name,)) + conn.commit() + flash("Zona guardada exitosamente.", "success") + except sqlite3.IntegrityError: + flash("Ese nombre de Zona ya existe.", "danger") + + elif action == 'add_modulo': + name = request.form.get('modulo_name').strip() + zona_id = request.form.get('zona_id') + if not zona_id: + flash("Debes seleccionar una Zona válida.", "danger") + else: + c.execute("INSERT INTO modulos (zona_id, name) VALUES (?, ?)", (zona_id, name)) + conn.commit() + flash("Módulo guardado exitosamente.", "success") + + return redirect(url_for('manage_structure')) + + # Fetch Zonas + c.execute("SELECT id, name FROM zonas ORDER BY name") + zonas = c.fetchall() + + # Fetch Modulos with their parent Zona name + c.execute('''SELECT m.id, m.name, z.name + FROM modulos m + JOIN zonas z ON m.zona_id = z.id + ORDER BY z.name, m.name''') + modulos = c.fetchall() + + conn.close() + return render_template('admin_structure.html', zonas=zonas, modulos=modulos) + +@app.route('/admin/estructura/delete//', methods=['POST']) +@admin_required +def delete_structure(type, id): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + try: + if type == 'zona': + # SQLite doesn't enforce foreign keys by default unless PRAGMA foreign_keys = ON + # But we should manually prevent orphan modules just in case. + c.execute("SELECT id FROM modulos WHERE zona_id=?", (id,)) + if c.fetchone(): + flash("No puedes eliminar una Zona que tiene Módulos asignados.", "danger") + else: + c.execute("DELETE FROM zonas WHERE id=?", (id,)) + flash("Zona eliminada.", "info") + elif type == 'modulo': + c.execute("SELECT id FROM workers WHERE modulo_id=?", (id,)) + if c.fetchone(): + flash("No puedes eliminar un Módulo que tiene Trabajadores asignados.", "danger") + else: + c.execute("DELETE FROM modulos WHERE id=?", (id,)) + flash("Módulo eliminado.", "info") + conn.commit() + except Exception as e: + flash("Error al eliminar el registro.", "danger") + finally: + conn.close() + + return redirect(url_for('manage_structure')) + +@app.route('/admin/productos', methods=['GET', 'POST']) +@admin_required +def manage_products(): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + if request.method == 'POST': + name = request.form.get('name').strip() + zona_id = request.form.get('zona_id') + + # Strip the formatting dots before trying to convert to math + raw_price = request.form.get('price').replace('.', '') + raw_commission = request.form.get('commission').replace('.', '') + + if not zona_id: + flash("Debes seleccionar una Zona.", "danger") + else: + try: + # Force strictly whole numbers + price = int(raw_price) + commission = int(raw_commission) + + c.execute("INSERT INTO productos (zona_id, name, price, commission) VALUES (?, ?, ?, ?)", + (zona_id, name, price, commission)) + conn.commit() + flash("Producto guardado exitosamente.", "success") + except ValueError: + flash("El precio y la comisión deben ser números enteros válidos.", "danger") + + return redirect(url_for('manage_products')) + + c.execute("SELECT id, name FROM zonas ORDER BY name") + zonas = c.fetchall() + + c.execute('''SELECT p.id, p.name, p.price, p.commission, z.name + FROM productos p + JOIN zonas z ON p.zona_id = z.id + ORDER BY z.name, p.name''') + productos = c.fetchall() + + conn.close() + return render_template('admin_productos.html', zonas=zonas, productos=productos) + +@app.route('/admin/productos/edit/', methods=['GET', 'POST']) +@admin_required +def edit_product(id): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + if request.method == 'POST': + name = request.form.get('name').strip() + zona_id = request.form.get('zona_id') + + # Strip formatting dots + raw_price = request.form.get('price').replace('.', '') + raw_commission = request.form.get('commission').replace('.', '') + + try: + price = int(raw_price) + commission = int(raw_commission) + + c.execute("UPDATE productos SET zona_id=?, name=?, price=?, commission=? WHERE id=?", + (zona_id, name, price, commission, id)) + conn.commit() + flash("Producto actualizado exitosamente.", "success") + conn.close() + return redirect(url_for('manage_products')) + except ValueError: + flash("El precio y la comisión deben ser números enteros válidos.", "danger") + + # GET request - fetch data for the form + c.execute("SELECT id, name FROM zonas ORDER BY name") + zonas = c.fetchall() + + c.execute("SELECT id, zona_id, name, price, commission FROM productos WHERE id=?", (id,)) + producto = c.fetchone() + conn.close() + + if not producto: + return redirect(url_for('manage_products')) + + return render_template('edit_producto.html', zonas=zonas, producto=producto) + +@app.route('/admin/productos/delete/', methods=['POST']) +@admin_required +def delete_product(id): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + c.execute("DELETE FROM productos WHERE id=?", (id,)) + conn.commit() + conn.close() + flash("Producto eliminado.", "info") + return redirect(url_for('manage_products')) + +@app.route('/admin/rendiciones') +@admin_required +def admin_rendiciones(): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + # Fetch all rendiciones, newest first + c.execute(''' + SELECT r.id, r.fecha, w.name, m.name, r.turno, + (r.venta_tarjeta + r.venta_mp + r.venta_efectivo) as total_declarado, + r.gastos + FROM rendiciones r + JOIN workers w ON r.worker_id = w.id + JOIN modulos m ON r.modulo_id = m.id + ORDER BY r.fecha DESC, r.id DESC + ''') + rendiciones = c.fetchall() + conn.close() + + return render_template('admin_rendiciones.html', rendiciones=rendiciones) + +@app.route('/admin/rendiciones/') +@admin_required +def view_rendicion(id): + conn = sqlite3.connect(DB_NAME) + c = conn.cursor() + + # Get Header Info + c.execute(''' + SELECT r.id, r.fecha, w.name, m.name, r.turno, + r.venta_tarjeta, r.venta_mp, r.venta_efectivo, r.gastos, r.observaciones + FROM rendiciones r + JOIN workers w ON r.worker_id = w.id + JOIN modulos m ON r.modulo_id = m.id + WHERE r.id = ? + ''', (id,)) + rendicion = c.fetchone() + + if not rendicion: + conn.close() + flash("Rendición no encontrada.", "danger") + return redirect(url_for('admin_rendiciones')) + + # Get Line Items + c.execute(''' + SELECT p.name, ri.cantidad, ri.precio_historico, ri.comision_historica, + (ri.cantidad * ri.precio_historico) as total_linea, + (ri.cantidad * ri.comision_historica) as total_comision + FROM rendicion_items ri + JOIN productos p ON ri.producto_id = p.id + WHERE ri.rendicion_id = ? + ''', (id,)) + items = c.fetchall() + conn.close() + + # Calculate the mathematical truth vs what they declared + total_calculado = sum(item[4] for item in items) + comision_total = sum(item[5] for item in items) + + return render_template('admin_rendicion_detail.html', + rendicion=rendicion, + items=items, + total_calculado=total_calculado, + comision_total=comision_total) + +if __name__ == '__main__': + init_db() + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/rendiciones.db b/rendiciones.db new file mode 100644 index 0000000..c2191df Binary files /dev/null and b/rendiciones.db differ diff --git a/templates/admin_productos.html b/templates/admin_productos.html new file mode 100644 index 0000000..0a82f6b --- /dev/null +++ b/templates/admin_productos.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} +{% block content %} +

Catálogo de Productos por Zona

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+
Agregar Nuevo Producto
+
+
+
+
+ + +
+
+ + +
+
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+
+ +
+
+
+
+
+ +
+
+ + + + + + + + + + + + {% for prod in productos %} + + + + + + + + {% else %} + + + + {% endfor %} + +
ZonaProductoPrecioComisiónAcciones
{{ prod[4] }}{{ prod[1] }}${{ "{:,.0f}".format(prod[2]).replace(',', '.') }}${{ "{:,.0f}".format(prod[3]).replace(',', '.') }} + Editar +
+ +
+
No hay productos registrados.
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/admin_rendicion_detail.html b/templates/admin_rendicion_detail.html new file mode 100644 index 0000000..0e2b010 --- /dev/null +++ b/templates/admin_rendicion_detail.html @@ -0,0 +1,116 @@ +{% extends "base.html" %} +{% block content %} + +
+

Detalle de Rendición #{{ rendicion[0] }}

+ Volver al Historial +
+ +
+
+
+
Productos Vendidos
+
+ + + + + + + + + + + + {% for item in items %} + + + + + + + + {% else %} + + + + {% endfor %} + + + + + + + + +
ProductoCant.Precio Un.Total LíneaComisión
{{ item[0] }}{{ item[1] }}${{ "{:,.0f}".format(item[2]).replace(',', '.') }}${{ "{:,.0f}".format(item[4]).replace(',', '.') }}${{ "{:,.0f}".format(item[5]).replace(',', '.') }}
No se registraron productos en esta rendición.
Total Calculado por Sistema:${{ "{:,.0f}".format(total_calculado).replace(',', '.') }}${{ "{:,.0f}".format(comision_total).replace(',', '.') }}
+
+
+
+ +
+
+
Declaración del Trabajador
+
+
+
Fecha
+
{{ rendicion[1] }}
+ +
Trabajador
+
{{ rendicion[2] }}
+ +
Módulo
+
{{ rendicion[3] }}
+ +
Turno
+
{{ rendicion[4] }}
+
+ +
+ +
+ Tarjetas: + ${{ "{:,.0f}".format(rendicion[5]).replace(',', '.') }} +
+
+ Mercado Pago: + ${{ "{:,.0f}".format(rendicion[6]).replace(',', '.') }} +
+
+ Efectivo: + ${{ "{:,.0f}".format(rendicion[7]).replace(',', '.') }} +
+ + {% set total_declarado = rendicion[5] + rendicion[6] + rendicion[7] %} + +
+ Total Declarado: + ${{ "{:,.0f}".format(total_declarado).replace(',', '.') }} +
+ + {% if total_declarado != total_calculado %} +
+ ⚠️ Discrepancia: El total declarado (${{ "{:,.0f}".format(total_declarado).replace(',', '.') }}) no coincide con la suma de los productos vendidos (${{ "{:,.0f}".format(total_calculado).replace(',', '.') }}). +
+ {% endif %} +
+
+ +
+
Gastos y Observaciones
+
+
+ Monto Gastos: + -${{ "{:,.0f}".format(rendicion[8]).replace(',', '.') }} +
+
+ Observaciones: +

+ {{ rendicion[9] if rendicion[9] else "Sin observaciones." }} +

+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin_rendiciones.html b/templates/admin_rendiciones.html new file mode 100644 index 0000000..b39f122 --- /dev/null +++ b/templates/admin_rendiciones.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block content %} +

Historial de Rendiciones

+ +
+
+ + + + + + + + + + + + + + {% for r in rendiciones %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
FechaTrabajadorMóduloTurnoTotal DeclaradoGastosAcciones
{{ r[1] }}{{ r[2] }}{{ r[3] }}{{ r[4] }}${{ "{:,.0f}".format(r[5]).replace(',', '.') }}${{ "{:,.0f}".format(r[6]).replace(',', '.') }} + Ver Detalle +
Aún no hay rendiciones enviadas.
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin_structure.html b/templates/admin_structure.html new file mode 100644 index 0000000..58413c1 --- /dev/null +++ b/templates/admin_structure.html @@ -0,0 +1,112 @@ +{% extends "base.html" %} +{% block content %} +

Estructura Operativa

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+
+
+
+
Gestión de Zonas
+
+
+
+ +
+ + +
+
+ + + + + + + + + + {% for zona in zonas %} + + + + + {% else %} + + + + {% endfor %} + +
NombreAcciones
{{ zona[1] }} +
+ +
+
No hay zonas registradas.
+
+
+
+ +
+
+
+
Gestión de Módulos
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + {% for modulo in modulos %} + + + + + + {% else %} + + + + {% endfor %} + +
MóduloZonaAcciones
{{ modulo[1] }}{{ modulo[2] }} +
+ +
+
No hay módulos registrados.
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin_workers.html b/templates/admin_workers.html new file mode 100644 index 0000000..2e51745 --- /dev/null +++ b/templates/admin_workers.html @@ -0,0 +1,108 @@ +{% extends "base.html" %} +{% block content %} +

Gestión de Trabajadores

+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+
Agregar Nuevo Trabajador
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + {% for worker in workers %} + + + + + + + + {% else %} + + + + {% endfor %} + +
RUTNombreTeléfonoMóduloAcciones
{{ worker[1] }}{{ worker[2] }}{{ worker[3] }}{{ worker[4] }} + Editar +
+ +
+
No hay trabajadores registrados.
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..654e9bc --- /dev/null +++ b/templates/base.html @@ -0,0 +1,19 @@ + + + + + + Sistema de Rendiciones + + + + + {% include 'navbar.html' %} + +
+ {% block content %}{% endblock %} +
+ + + + \ No newline at end of file diff --git a/templates/edit_producto.html b/templates/edit_producto.html new file mode 100644 index 0000000..68c8eae --- /dev/null +++ b/templates/edit_producto.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Editar Producto

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+
+
+ Cancelar + +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/edit_worker.html b/templates/edit_worker.html new file mode 100644 index 0000000..9160f64 --- /dev/null +++ b/templates/edit_worker.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Editar Trabajador

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Cancelar + +
+
+
+
+ +
+
+ Opciones de Seguridad +
+
+
+
Restablecer Contraseña
+ Genera una nueva contraseña aleatoria si el trabajador olvidó la suya. +
+
+ +
+
+
+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..2998012 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+
+

Iniciar Sesión

+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 0000000..d057089 --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,40 @@ + \ No newline at end of file diff --git a/templates/worker_dashboard.html b/templates/worker_dashboard.html new file mode 100644 index 0000000..99f722e --- /dev/null +++ b/templates/worker_dashboard.html @@ -0,0 +1,138 @@ +{% extends "base.html" %} +{% block content %} + +
+

Rendición de Caja

+
+
Módulo: {{ modulo_name }}
+
Zona: {{ zona_name }}
+
+
+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message|safe }}
+ {% endfor %} + {% endif %} +{% endwith %} + +
+
+
Datos del Turno
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
Detalle de Productos Vendidos
+
+ + + + + + {% if has_commission %} + + {% endif %} + + + + + {% for prod in productos %} + + + + + {% if has_commission %} + + {% endif %} + + + + {% else %} + + + + {% endfor %} + +
Producto / Rango de PrecioPrecioComisiónCantidad
{{ prod[1] }}${{ "{:,.0f}".format(prod[2]).replace(',', '.') }}${{ "{:,.0f}".format(prod[3]).replace(',', '.') }} + +
+ No hay productos configurados para esta zona. +
+
+
+ +
+
Resumen Financiero
+
+
+
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+
+ +
+ -$ + +
+
+
+
+ + +
+
+
+ + +
+ + +{% endblock %} \ No newline at end of file