diff --git a/database.py b/database.py index fdd47e6..a1a9397 100644 --- a/database.py +++ b/database.py @@ -71,10 +71,20 @@ 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_base: - c.execute("INSERT INTO productos (zona_id, name, price, commission) VALUES (?, ?, ?, ?)", - (zona_id, name, price, commission)) + + # Fecha de activación artificial en el pasado para que las ventas históricas funcionen + fecha_base_precios = (datetime.date.today() - datetime.timedelta(days=365*3)).strftime('%Y-%m-%d 00:00:00') + + for name, price, commission in productos_base: + c.execute("INSERT OR IGNORE INTO productos (name) VALUES (?)", (name,)) + c.execute("SELECT id FROM productos WHERE name = ?", (name,)) + p_id = c.fetchone()[0] + + for zona_id in zonas_ids: + c.execute('''INSERT INTO precios_historicos + (producto_id, zona_id, price, commission, fecha_activacion) + VALUES (?, ?, ?, ?, ?)''', + (p_id, zona_id, price, commission, fecha_base_precios)) conn.commit() c.execute("SELECT COUNT(*) FROM workers WHERE is_admin = 0") @@ -165,7 +175,19 @@ def populateDefaults(): rend_id = c.lastrowid - c.execute("SELECT id, price, commission FROM productos WHERE zona_id = (SELECT zona_id FROM modulos WHERE id = ?)", (m_id,)) + # Buscar el precio activo en el momento exacto de la venta histórica + c.execute(''' + SELECT p.id, ph.price, ph.commission + FROM productos p + JOIN precios_historicos ph ON p.id = ph.producto_id + WHERE ph.zona_id = (SELECT zona_id FROM modulos WHERE id = ?) + AND ph.fecha_activacion = ( + SELECT MAX(fecha_activacion) + FROM precios_historicos + WHERE producto_id = p.id AND zona_id = ph.zona_id + AND fecha_activacion <= ? + ) + ''', (m_id, fecha_str + ' 23:59:59')) prods_zona = c.fetchall() if prods_zona: @@ -182,7 +204,7 @@ def populateDefaults(): conn.commit() conn.close() - + def init_db(): conn = get_db_connection() c = conn.cursor() @@ -194,12 +216,20 @@ def init_db(): zona_id INTEGER NOT NULL, name TEXT NOT NULL, FOREIGN KEY (zona_id) REFERENCES zonas(id))''') + # 1. Catálogo Maestro (Solo el nombre) c.execute('''CREATE TABLE IF NOT EXISTS productos (id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL)''') + + # 2. Historial de Precios por Zona + c.execute('''CREATE TABLE IF NOT EXISTS precios_historicos + (id INTEGER PRIMARY KEY AUTOINCREMENT, + producto_id INTEGER NOT NULL, zona_id INTEGER NOT NULL, - name TEXT NOT NULL, - price REAL NOT NULL, - commission REAL NOT NULL, + price INTEGER NOT NULL, + commission INTEGER NOT NULL, + fecha_activacion DATETIME NOT NULL, + FOREIGN KEY (producto_id) REFERENCES productos(id), FOREIGN KEY (zona_id) REFERENCES zonas(id))''') c.execute('''CREATE TABLE IF NOT EXISTS workers (id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/routes_admin.py b/routes_admin.py index cfc1442..768fb4e 100644 --- a/routes_admin.py +++ b/routes_admin.py @@ -1,5 +1,5 @@ import sqlite3 -from flask import app, render_template, request, redirect, url_for, flash, session +from flask import app, render_template, request, redirect, url_for, flash, session, jsonify from werkzeug.security import generate_password_hash from datetime import date, datetime from database import get_db_connection @@ -211,87 +211,165 @@ def register_admin_routes(app): if request.method == 'POST': name = request.form.get('name').strip() - zona_id = request.form.get('zona_id') - - 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: - 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, p.zona_id - 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 = get_db_connection() - c = conn.cursor() - - if request.method == 'POST': - name = request.form.get('name').strip() - zona_id = request.form.get('zona_id') - - raw_price = request.form.get('price').replace('.', '') - raw_commission = request.form.get('commission').replace('.', '') - try: - price = int(raw_price) - commission = int(raw_commission) + # 1. Crear producto maestro + c.execute("INSERT INTO productos (name) VALUES (?)", (name,)) + prod_id = c.lastrowid + + # 2. Inicializar precios en 0 para todas las zonas activas + c.execute("SELECT id FROM zonas") + zonas = c.fetchall() + for zona in zonas: + c.execute('''INSERT INTO precios_historicos + (producto_id, zona_id, price, commission, is_active) + VALUES (?, ?, 0, 0, 1)''', (prod_id, zona[0])) - 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") + flash("Producto maestro creado. No olvides configurar sus precios.", "success") + except sqlite3.IntegrityError: + flash("Ese producto ya existe en el catálogo.", "danger") + + return redirect(url_for('manage_products')) 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() + # Obtener productos y el precio VIGENTE por zona usando una subquery + c.execute(''' + SELECT p.id, p.name, + z.id as zona_id, z.name as zona_name, + ph.price, ph.commission + FROM productos p + CROSS JOIN zonas z + LEFT JOIN precios_historicos ph + ON p.id = ph.producto_id AND z.id = ph.zona_id + AND ph.fecha_activacion = ( + SELECT MAX(fecha_activacion) + FROM precios_historicos + WHERE producto_id = p.id AND zona_id = z.id + AND fecha_activacion <= datetime('now', 'localtime') + ) + ORDER BY p.name, z.name + ''') + + # Agrupar datos para la vista: {prod_id: {'name': 'Lentes', 'precios': {zona_id: {'price': X, 'comm': Y}}}} + raw_data = c.fetchall() + productos_dict = {} + for row in raw_data: + p_id, p_name, z_id, z_name, price, comm = row + if p_id not in productos_dict: + # Añadimos la lista 'futuros' + productos_dict[p_id] = {'id': p_id, 'name': p_name, 'precios': {}, 'futuros': []} + productos_dict[p_id]['precios'][z_id] = { + 'zona_name': z_name, + 'price': price or 0, + 'commission': comm or 0 + } + + # BÚSQUEDA DE PRECIOS PROGRAMADOS (Futuro) + c.execute(''' + SELECT ph.id, ph.producto_id, z.name, ph.price, ph.commission, ph.fecha_activacion + FROM precios_historicos ph + JOIN zonas z ON ph.zona_id = z.id + WHERE ph.fecha_activacion > datetime('now', 'localtime') + ORDER BY ph.fecha_activacion ASC + ''') + future_data = c.fetchall() + + for row in future_data: + ph_id, p_id, z_name, price, comm, fecha = row + if p_id in productos_dict: + productos_dict[p_id]['futuros'].append({ + 'id': ph_id, + 'zona_name': z_name, + 'price': price, + 'commission': comm, + 'fecha': fecha + }) + conn.close() + return render_template('admin_productos.html', zonas=zonas, productos=productos_dict.values()) - if not producto: - return redirect(url_for('manage_products')) - - return render_template('edit_producto.html', zonas=zonas, producto=producto) + from datetime import datetime @app.route('/admin/productos/delete/', methods=['POST']) @admin_required def delete_product(id): conn = get_db_connection() c = conn.cursor() - c.execute("DELETE FROM productos WHERE id=?", (id,)) + try: + # Borrar historial de precios primero + c.execute("DELETE FROM precios_historicos WHERE producto_id=?", (id,)) + # Borrar producto maestro + c.execute("DELETE FROM productos WHERE id=?", (id,)) + conn.commit() + flash("Producto maestro y su historial eliminados.", "info") + except sqlite3.IntegrityError: + # Si hay ventas asociadas en rendicion_items, SQLite bloqueará el borrado + flash("No puedes eliminar este producto porque ya tiene ventas registradas. Cámbiale el precio a 0 en su lugar.", "danger") + finally: + conn.close() + return redirect(url_for('manage_products')) + + @app.route('/admin/productos/precios/', methods=['POST']) + @admin_required + def update_product_prices(id): + conn = get_db_connection() + c = conn.cursor() + + c.execute("SELECT id FROM zonas") + zonas = c.fetchall() + + # Obtener fecha programada o usar la actual + fecha_input = request.form.get('fecha_activacion') + if fecha_input: + fecha_activacion = fecha_input.replace('T', ' ') + ':00' + else: + fecha_activacion = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + for zona in zonas: + z_id = str(zona[0]) + new_price = int(request.form.get(f'price_{z_id}', '0').replace('.', '')) + new_comm = int(request.form.get(f'comm_{z_id}', '0').replace('.', '')) + + c.execute('''INSERT INTO precios_historicos + (producto_id, zona_id, price, commission, fecha_activacion) + VALUES (?, ?, ?, ?, ?)''', (id, z_id, new_price, new_comm, fecha_activacion)) + conn.commit() conn.close() - flash("Producto eliminado.", "info") + flash(f"Precios actualizados. Entrarán en vigencia el {fecha_activacion}.", "success") return redirect(url_for('manage_products')) + + @app.route('/admin/productos/precios/cancelar/', methods=['POST']) + @admin_required + def cancel_scheduled_price(id): + conn = get_db_connection() + c = conn.cursor() + c.execute("DELETE FROM precios_historicos WHERE id = ?", (id,)) + conn.commit() + conn.close() + flash("Cambio de precio programado cancelado.", "info") + return redirect(url_for('manage_products')) + + @app.route('/admin/api/productos//historial') + @admin_required + def api_product_history(id): + conn = get_db_connection() + c = conn.cursor() + c.execute(''' + SELECT z.name, ph.price, ph.fecha_activacion + FROM precios_historicos ph + JOIN zonas z ON ph.zona_id = z.id + WHERE ph.producto_id = ? + ORDER BY ph.fecha_activacion ASC + ''', (id,)) # <-- Añade la coma para que sea una tupla + rows = c.fetchall() + conn.close() + + # Convertimos la data a una lista de diccionarios que JS entienda felizmente + history = [{'zona': r[0], 'price': r[1], 'fecha': r[2]} for r in rows] + return jsonify(history) @app.route('/admin/rendiciones') @admin_required diff --git a/routes_worker.py b/routes_worker.py index 06da270..1786b69 100644 --- a/routes_worker.py +++ b/routes_worker.py @@ -133,7 +133,14 @@ def register_worker_routes(app): prod_id = int(key.split('_')[1]) cantidad = int(value) - c.execute("SELECT price, commission FROM productos WHERE id = ?", (prod_id,)) + # Buscar el precio vigente al momento de la venta + c.execute(''' + SELECT price, commission + FROM precios_historicos + WHERE producto_id = ? AND zona_id = ? + AND fecha_activacion <= datetime('now', 'localtime') + ORDER BY fecha_activacion DESC LIMIT 1 + ''', (prod_id, zona_id)) prod_data = c.fetchone() if prod_data: @@ -153,7 +160,20 @@ def register_worker_routes(app): ''', (session['user_id'], modulo_id)) otros_trabajadores = c.fetchall() - c.execute("SELECT id, name, price, commission FROM productos WHERE zona_id = ? ORDER BY name", (zona_id,)) + # Buscar solo el precio vigente actual para esta zona + c.execute(''' + SELECT p.id, p.name, ph.price, ph.commission + FROM productos p + JOIN precios_historicos ph ON p.id = ph.producto_id + WHERE ph.zona_id = ? + AND ph.fecha_activacion = ( + SELECT MAX(fecha_activacion) + FROM precios_historicos + WHERE producto_id = p.id AND zona_id = ? + AND fecha_activacion <= datetime('now', 'localtime') + ) + ORDER BY p.name + ''', (zona_id, zona_id)) # Nota: zona_id se pasa dos veces productos = c.fetchall() conn.close() diff --git a/templates/admin_productos.html b/templates/admin_productos.html index 270845c..6cca5b8 100644 --- a/templates/admin_productos.html +++ b/templates/admin_productos.html @@ -21,103 +21,254 @@ {{ edit_product_modal(zonas) }}
-
Agregar Nuevo Producto
+
Agregar Producto Maestro
-
- - -
-
- - +
+
- -
- $ - -
-
-
- -
- $ - -
-
-
- +
+
+ + +
+
- - - - + {% for prod in productos %} - - - - - + + - {% else %} - - - {% endfor %}
ZonaProductoPrecioComisiónProducto Maestro Acciones
{{ prod[4] }}{{ prod[1] }}${{ "{:,.0f}".format(prod[2]).replace(',', '.') }}${{ "{:,.0f}".format(prod[3]).replace(',', '.') }}
{{ prod.name }} - - - + {{ confirm_modal( - id='deleteProd' ~ prod[0], + id='deleteProd' ~ prod.id, title='Eliminar Producto', - message='¿Estás seguro de que deseas eliminar "' ~ prod[1] ~ '"?', - action_url=url_for('delete_product', id=prod[0]), + message='¿Eliminar "' ~ prod.name ~ '"? Esto fallará si el producto ya tiene ventas registradas.', + action_url=url_for('delete_product', id=prod.id), btn_class='btn-danger', - btn_text='Eliminar permanentemente' + btn_text='Eliminar' ) }}
No hay productos registrados.
+ +{% for prod in productos %} + +{% endfor %} + + +
+ + + + {% endblock %} {% block scripts %} + + + + {% endblock %} \ No newline at end of file diff --git a/templates/admin_workers.html b/templates/admin_workers.html index 7d27af8..c11ac05 100644 --- a/templates/admin_workers.html +++ b/templates/admin_workers.html @@ -59,6 +59,34 @@ +
+
+
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+
@@ -74,7 +102,7 @@ {% for worker in workers %} - + @@ -207,5 +235,40 @@ document.querySelectorAll('.phone-input, #phoneInput').forEach(inp => { inp.addEventListener('input', () => formatPhone(inp)); }); + + + // --- LÓGICA DE FILTRADO DE TRABAJADORES --- + const searchInputWorker = document.getElementById('searchWorker'); + const moduleSelectFilter = document.getElementById('filterModule'); + const typeSelectFilter = document.getElementById('filterType'); + const workerRows = document.querySelectorAll('.worker-row'); + + function filterWorkers() { + const searchTerm = searchInputWorker.value.toLowerCase(); + const selectedModule = moduleSelectFilter.value; + const selectedType = typeSelectFilter.value; + + workerRows.forEach(row => { + // Asumiendo que celda 0 es RUT y celda 1 es Nombre + const rut = row.cells[0].textContent.toLowerCase(); + const name = row.cells[1].textContent.toLowerCase(); + const rowModule = row.getAttribute('data-modulo'); + const rowType = row.getAttribute('data-tipo'); + + const matchesSearch = rut.includes(searchTerm) || name.includes(searchTerm); + const matchesModule = selectedModule === 'all' || rowModule === selectedModule; + const matchesType = selectedType === 'all' || rowType === selectedType; + + if (matchesSearch && matchesModule && matchesType) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + }); + } + + searchInputWorker.addEventListener('input', filterWorkers); + moduleSelectFilter.addEventListener('change', filterWorkers); + typeSelectFilter.addEventListener('change', filterWorkers); {% endblock %} diff --git a/templates/macros/modals.html b/templates/macros/modals.html index fbf8168..b96acdd 100644 --- a/templates/macros/modals.html +++ b/templates/macros/modals.html @@ -215,7 +215,7 @@
{{ rendicion[2] }} {% if session.get('is_admin') %} - {% if rendicion[14] %}$ Sí{% else %}$ No{% endif %} + {% if rendicion[14] %}$ Si Recibe Comision{% else %}$ No Recibe Comision{% endif %} {% endif %}
@@ -226,7 +226,7 @@ {{ rendicion[10] }} {% if session.get('is_admin') %} - {% if rendicion[15] %}$ Sí{% else %}$ No{% endif %} + {% if rendicion[15] %}$ Si Recibe Comision{% else %}$ No Recibe Comision{% endif %} {% endif %} {% else %} diff --git a/templates/worker_dashboard.html b/templates/worker_dashboard.html index bd41480..9421aea 100644 --- a/templates/worker_dashboard.html +++ b/templates/worker_dashboard.html @@ -214,6 +214,10 @@ + + @@ -277,6 +281,8 @@ } }); displayTotalProductos.value = granTotal.toLocaleString('es-CL'); + + checkWarnings(); } inputsCantidad.forEach(input => { @@ -292,6 +298,41 @@ calcularVentaProductos(); }); }); + + function checkWarnings() { + const getVal = (id) => { + const el = document.getElementById(id); + if (!el || !el.value.trim() || el.value === '0') return 0; + return parseInt(el.value.replace(/\D/g, '')) || 0; + }; + + const totalProductos = getVal('total_productos_calc'); + const totalDeclarado = getVal('total_general'); + const gastos = getVal('gastos'); + const efectivo = getVal('venta_efectivo'); + + let warnings = []; + + // Comprobar diferencias en totales (solo si se ha ingresado algo para no mostrar el error de entrada) + if ((totalProductos > 0 || totalDeclarado > 0) && totalProductos !== totalDeclarado) { + warnings.push("El Total Venta por Productos no coincide con el Total Ventas Declaradas."); + } + + // Comprobar si los gastos superan el efectivo + if (gastos > efectivo) { + warnings.push("El Monto de Gastos es mayor que el Efectivo declarado."); + } + + const warningContainer = document.getElementById('discrepancy_warning'); + const warningText = document.getElementById('discrepancy_text'); + + if (warnings.length > 0) { + warningText.innerHTML = warnings.join("
"); + warningContainer.style.display = 'block'; + } else { + warningContainer.style.display = 'none'; + } + }
{{ worker[1] }} {{ worker[2] }} {{ worker[3] }}