diff --git a/database.py b/database.py index 2521ef4..e84e396 100644 --- a/database.py +++ b/database.py @@ -246,6 +246,7 @@ def init_db(): worker_id INTEGER NOT NULL, worker_comision BOOLEAN DEFAULT 1, companion_id INTEGER, + companion2_id INTEGER, modulo_id INTEGER NOT NULL, fecha DATE NOT NULL, hora_entrada TEXT NOT NULL, @@ -253,6 +254,7 @@ def init_db(): companion_hora_entrada TEXT, companion_hora_salida TEXT, companion_comision BOOLEAN DEFAULT 0, + companion2_comision BOOLEAN DEFAULT 0, venta_debito INTEGER DEFAULT 0, venta_credito INTEGER DEFAULT 0, venta_mp INTEGER DEFAULT 0, @@ -265,6 +267,7 @@ def init_db(): observaciones TEXT, FOREIGN KEY (worker_id) REFERENCES workers(id), FOREIGN KEY (companion_id) REFERENCES workers(id), + FOREIGN KEY (companion2_id) REFERENCES workers(id), FOREIGN KEY (modulo_id) REFERENCES modulos(id))''') c.execute('''CREATE TABLE IF NOT EXISTS rendicion_items (id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -283,6 +286,13 @@ def init_db(): except sqlite3.OperationalError: pass # column already exists + # Migrate: add companion2 columns + for col in ['companion2_id', 'companion2_comision']: + try: + c.execute(f"ALTER TABLE rendiciones ADD COLUMN {col}") + except sqlite3.OperationalError: + pass + c.execute("SELECT id FROM workers WHERE is_admin = 1") if not c.fetchone(): admin_pass = generate_password_hash("admin123") diff --git a/generar_unificado.py b/generar_unificado.py index a3433e8..87449c0 100644 --- a/generar_unificado.py +++ b/generar_unificado.py @@ -122,13 +122,29 @@ def generar_historico_definitivo(dias_atras=180): companion_id = None comp_in, comp_out = None, None companion_comision = 0 + companion2_id = None + companion2_comision = 0 if random.random() < 0.70: - posibles_comp = [w for w in todos_los_trabajadores if w != worker_id] + posibles_comp = [w for w in workers_modulo if w != worker_id] if posibles_comp: companion_id = random.choice(posibles_comp) comp_in, comp_out = hora_entrada, hora_salida comp_tipo = workers_tipo.get(companion_id, "Part Time") companion_comision = 1 if comp_tipo == "Full Time" else random.choice([0, 1]) + + # Acompañante 2 (25% probability if companion 1 is present) + if random.random() < 0.25: + posibles_comp2 = [w for w in posibles_comp if w != companion_id] + if posibles_comp2: + companion2_id = random.choice(posibles_comp2) + comp2_tipo = workers_tipo.get(companion2_id, "Part Time") + companion2_comision = 1 if comp2_tipo == "Full Time" else random.choice([0, 1]) + + # If there is no companion, comision should be disabled (0) + if companion_id is None: + companion_comision = 0 + if companion2_id is None: + companion2_comision = 0 num_prods = random.randint(1, 5) prods_elegidos = random.sample(productos, min(num_prods, len(productos))) @@ -171,16 +187,16 @@ def generar_historico_definitivo(dias_atras=180): c.execute(''' INSERT INTO rendiciones - (worker_id, worker_comision, companion_id, modulo_id, fecha, + (worker_id, worker_comision, companion_id, companion2_id, modulo_id, fecha, hora_entrada, hora_salida, companion_hora_entrada, companion_hora_salida, - companion_comision, + companion_comision, companion2_comision, venta_debito, venta_credito, venta_mp, venta_efectivo, boletas_debito, boletas_credito, boletas_mp, boletas_efectivo, gastos, observaciones) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (worker_id, worker_comision, companion_id, modulo_id, fecha_str, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (worker_id, worker_comision, companion_id, companion2_id, modulo_id, fecha_str, hora_entrada, hora_salida, comp_in, comp_out, - companion_comision, + companion_comision, companion2_comision, debito, credito, mp, efectivo, b_debito, b_credito, b_mp, b_efectivo, gastos, tipo_registro)) diff --git a/models/models.py b/models/models.py index 61eb5e7..53c5e94 100644 --- a/models/models.py +++ b/models/models.py @@ -69,6 +69,7 @@ class Rendicion(db.Model): worker_id = db.Column(db.Integer, db.ForeignKey('workers.id'), nullable=False) worker_comision = db.Column(db.Boolean, default=True) companion_id = db.Column(db.Integer, db.ForeignKey('workers.id')) + companion2_id = db.Column(db.Integer, db.ForeignKey('workers.id')) modulo_id = db.Column(db.Integer, db.ForeignKey('modulos.id'), nullable=False) fecha = db.Column(db.Date, nullable=False) hora_entrada = db.Column(db.String, nullable=False) @@ -76,6 +77,7 @@ class Rendicion(db.Model): companion_hora_entrada = db.Column(db.String) companion_hora_salida = db.Column(db.String) companion_comision = db.Column(db.Boolean, default=False) + companion2_comision = db.Column(db.Boolean, default=False) venta_debito = db.Column(db.Integer, default=0) venta_credito = db.Column(db.Integer, default=0) venta_mp = db.Column(db.Integer, default=0) diff --git a/requirements.txt b/requirements.txt index 78684e4..759c603 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Flask==3.1.3 Flask-SQLAlchemy==3.1.1 Werkzeug==3.1.6 SQLAlchemy==2.0.45 +openpyxl==3.1.5 diff --git a/routes/admin_bp.py b/routes/admin_bp.py index 4008611..b869419 100644 --- a/routes/admin_bp.py +++ b/routes/admin_bp.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify +from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify, send_file from werkzeug.security import generate_password_hash from sqlalchemy import func, and_ from sqlalchemy.exc import IntegrityError @@ -489,8 +489,18 @@ def edit_rendicion(id): if companion_id and worker_id == companion_id: flash("Error: No puedes asignarte a ti mismo como acompañante.", "danger") return redirect(url_for('admin.admin_rendiciones')) + companion2_id = request.form.get('companion2_id') or None + if companion2_id and worker_id == companion2_id: + flash("Error: No puedes asignarte a ti mismo como acompañante 2.", "danger") + return redirect(url_for('admin.admin_rendiciones')) worker_comision = 1 if request.form.get('worker_comision') else 0 companion_comision = 1 if request.form.get('companion_comision') else 0 + companion2_comision = 1 if request.form.get('companion2_comision') else 0 + + if not companion_id: + companion_comision = 0 + if not companion2_id: + companion2_comision = 0 def clean_money(val): if not val: @@ -527,8 +537,10 @@ def edit_rendicion(id): rendicion.worker_id = int(worker_id) rendicion.modulo_id = int(modulo_id) rendicion.companion_id = int(companion_id) if companion_id else None + rendicion.companion2_id = int(companion2_id) if companion2_id else None rendicion.worker_comision = bool(worker_comision) rendicion.companion_comision = bool(companion_comision) + rendicion.companion2_comision = bool(companion2_comision) rendicion.venta_debito = debito rendicion.venta_credito = credito rendicion.venta_mp = mp @@ -653,3 +665,129 @@ def report_modulo_calculo_iva(modulo_id): workers_list=workers_list, worker_actual=worker_id, fecha_inicio=fecha_inicio, fecha_fin=fecha_fin, anios_disponibles=anios_list) + + +@admin_bp.route('/reportes/modulo//exportar_excel') +@admin_required +def report_modulo_exportar_excel(modulo_id): + fecha_inicio, fecha_fin, worker_id = get_report_params() + data = report_service.get_modulo_periodo_data(modulo_id, fecha_inicio, fecha_fin, worker_id) + mod_name, _, _ = report_service.get_modulo_workers_and_anios(modulo_id) + + import io + import openpyxl + from openpyxl.styles import Font, Alignment, PatternFill, Border, Side + from openpyxl.utils import get_column_letter + + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Detalle Ventas" + + thin = Side(style='thin') + border = Border(left=thin, right=thin, top=thin, bottom=thin) + center = Alignment(horizontal='center', vertical='center') + + # ── column colors matching web table ── + col_colors = { + 2: '198754', # Venta Total → text-success green + 3: '0DCAF0', # Comisión → text-info cyan + 4: 'DC3545', # Gastos → text-danger red + 5: '6C757D', # Crédito → text-muted gray + 6: '6C757D', # Débito → text-muted gray + 7: '6C757D', # Mercado Pago → text-muted gray + 8: '6C757D', # Efectivo/Dep. → text-muted gray + 9: 'e5904d', # Red. Crédito → custom orange + 10: 'e5904d', # Red. Débito → custom orange + 11: 'e5904d', # Red. MP → custom orange + 12: '20c997', # REDELCOM Neto → teal + 13: 'FFC107', # Efectivo - Gastos → text-warning + 14: '0D6EFD', # Venta Total Neto → text-primary blue + } + + hdr_fill = PatternFill(start_color="2B303A", end_color="2B303A", fill_type="solid") + ws.merge_cells('A1:O1') + ws['A1'] = f"Resumen Financiero — {mod_name} ({fecha_inicio} a {fecha_fin})" + ws['A1'].font = Font(bold=True, size=14) + + headers = ['Día', 'Venta Total', 'Comisión', 'Gastos', + 'Crédito', 'Débito', 'Mercado Pago', 'Efectivo/Dep.', + 'Red. Crédito', 'Red. Débito', 'Red. MP', + 'REDELCOM Neto', 'Efectivo - Gastos', 'Venta Total Neto'] + for col, h in enumerate(headers, 1): + cell = ws.cell(row=3, column=col, value=h) + cell.fill = hdr_fill + cell.alignment = center + cell.border = border + font_color = col_colors.get(col, 'FFFFFF') + cell.font = Font(bold=True, color=font_color, size=11) + + for row_idx, dia in enumerate(data['dias_en_periodo'], 4): + d = data['data_por_dia'][dia] + vals = [ + dia, + d['venta_total'], + d['comision'], + d['gastos'], + d['credito'], + d['debito'], + d['mp'], + d['efectivo'], + d['credito'] * 0.97620, + d['debito'] * 0.98453, + d['mp'] * 0.98691, + d['credito'] * 0.97620 + d['debito'] * 0.98453 + d['mp'] * 0.98691, + d['efectivo'] - d['gastos'], + d['credito'] * 0.97620 + d['debito'] * 0.98453 + d['mp'] * 0.98691 + d['efectivo'] - d['gastos'], + ] + for col, v in enumerate(vals, 1): + cell = ws.cell(row=row_idx, column=col, value=v) + cell.border = border + if col == 1: + cell.alignment = center + else: + cell.number_format = '#,##0' + if col in col_colors: + cell.font = Font(color=col_colors[col]) + + total_row = 4 + len(data['dias_en_periodo']) + totals = data['totales_mes'] + total_vals = [ + 'TOTAL', + totals['venta_total'], + totals['comision'], + totals['gastos'], + totals['credito'], + totals['debito'], + totals['mp'], + totals['efectivo'], + totals['credito'] * 0.97620, + totals['debito'] * 0.98453, + totals['mp'] * 0.98691, + totals['credito'] * 0.97620 + totals['debito'] * 0.98453 + totals['mp'] * 0.98691, + totals['efectivo'] - totals['gastos'], + totals['credito'] * 0.97620 + totals['debito'] * 0.98453 + totals['mp'] * 0.98691 + totals['efectivo'] - totals['gastos'], + ] + total_fill = PatternFill(start_color="2B303A", end_color="2B303A", fill_type="solid") + for col, v in enumerate(total_vals, 1): + cell = ws.cell(row=total_row, column=col, value=v) + cell.fill = total_fill + cell.border = border + if col == 1: + cell.alignment = center + cell.font = Font(bold=True, color="FFFFFF", size=11) + else: + cell.number_format = '#,##0' + font_color = col_colors.get(col, 'FFFFFF') + cell.font = Font(bold=True, color=font_color, size=11) + + for col in range(1, 16): + ws.column_dimensions[get_column_letter(col)].width = 16 + ws.column_dimensions['A'].width = 8 + + output = io.BytesIO() + wb.save(output) + output.seek(0) + + filename = f"reporte_{mod_name}_{fecha_inicio}_{fecha_fin}.xlsx".replace(' ', '_') + return send_file(output, as_attachment=True, download_name=filename, + mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') diff --git a/services/rendiciones_service.py b/services/rendiciones_service.py index 52ac2af..4cdf795 100644 --- a/services/rendiciones_service.py +++ b/services/rendiciones_service.py @@ -31,6 +31,7 @@ def get_filtered_rendiciones(fecha_inicio, fecha_fin, zona_id, modulo_id): fin = datetime.strptime(fecha_fin, '%Y-%m-%d').date() Companion = aliased(Worker) + Companion2 = aliased(Worker) filters = [ Rendicion.fecha >= inicio, @@ -52,11 +53,14 @@ def get_filtered_rendiciones(fecha_inicio, fecha_fin, zona_id, modulo_id): Companion.name.label('companion_name'), Rendicion.worker_id, Rendicion.companion_id, Rendicion.modulo_id, Rendicion.worker_comision, Rendicion.companion_comision, + Companion2.name.label('companion2_name'), + Rendicion.companion2_id, Rendicion.companion2_comision, Rendicion.boletas_debito, Rendicion.boletas_credito, Rendicion.boletas_mp, Rendicion.boletas_efectivo, ).join(Worker, Rendicion.worker_id == Worker.id ).join(Modulo, Rendicion.modulo_id == Modulo.id ).outerjoin(Companion, Rendicion.companion_id == Companion.id + ).outerjoin(Companion2, Rendicion.companion2_id == Companion2.id ).filter(*filters ).order_by(Rendicion.fecha.desc(), Rendicion.id.desc()).all() diff --git a/static/js/admin_rendiciones.js b/static/js/admin_rendiciones.js index 6ce69a6..5f0fc1e 100644 --- a/static/js/admin_rendiciones.js +++ b/static/js/admin_rendiciones.js @@ -23,6 +23,13 @@ document.addEventListener("DOMContentLoaded", function () { window.updateComisionToggle(select, `cc_${id}`); }; + window.toggleComp2Div = function (id, select) { + const compDiv = document.getElementById(`comp2_com_div_${id}`); + if (compDiv) compDiv.style.display = select.value ? 'flex' : 'none'; + window.updateBadge(select, `badge_comp2_${id}`); + window.updateComisionToggle(select, `cc2_${id}`); + }; + window.updateComisionToggle = function (selectElement, toggleId) { const option = selectElement.options[selectElement.selectedIndex]; const tipoJornada = option ? option.getAttribute('data-tipo') : null; @@ -99,6 +106,9 @@ document.addEventListener("DOMContentLoaded", function () { const compSelect = this.querySelector('select[name="companion_id"]'); if (compSelect && compSelect.value) window.updateBadge(compSelect, `badge_comp_${rid}`); + + const comp2Select = this.querySelector('select[name="companion2_id"]'); + if (comp2Select && comp2Select.value) window.updateBadge(comp2Select, `badge_comp2_${rid}`); }); modal.addEventListener('hidden.bs.modal', function () { diff --git a/templates/admin_rendiciones.html b/templates/admin_rendiciones.html index c0b3bbb..6cf03a4 100644 --- a/templates/admin_rendiciones.html +++ b/templates/admin_rendiciones.html @@ -80,8 +80,8 @@ - {{ rendicion_detail_modal(r, r[20], r[21], r[22]) }} - {{ edit_rendicion_modal(r, r[20], workers, modulos) }} + {{ rendicion_detail_modal(r, r[23], r[24], r[25]) }} + {{ edit_rendicion_modal(r, r[23], workers, modulos) }} {{ confirm_modal( id='deleteRendicion' ~ r[0], diff --git a/templates/admin_report_modulo.html b/templates/admin_report_modulo.html index d07e920..0626dec 100644 --- a/templates/admin_report_modulo.html +++ b/templates/admin_report_modulo.html @@ -42,10 +42,10 @@
-
+
-
Días Trabajados
-

{{ dias_activos }} / 31

+
Venta Total Neto
+

${{ "{:,.0f}".format(totales_mes.credito * 0.97620 + totales_mes.debito * 0.98453 + totales_mes.mp * 0.98691 + totales_mes.efectivo - totales_mes.gastos).replace(',', '.') }}

@@ -63,9 +63,9 @@
@@ -77,12 +77,20 @@ COMISIÓN GASTOS MEDIOS DE PAGO + COMISIÓN REDELCOM + TOTALES NETOS Crédito Débito Mercado Pago Efectivo/Dep. + Crédito (-2.38%) + Débito (-1.547%) + MP (-1.309%) + REDELCOM Neto + Efectivo - Gastos + Venta Total Neto @@ -105,6 +113,17 @@ {{ ("$" ~ "{:,.0f}".format(d.debito).replace(',', '.')) if d.debito > 0 else "-" }} {{ ("$" ~ "{:,.0f}".format(d.mp).replace(',', '.')) if d.mp > 0 else "-" }} {{ ("$" ~ "{:,.0f}".format(d.efectivo).replace(',', '.')) if d.efectivo > 0 else "-" }} + {% set red_credito = d.credito * 0.97620 %} + {% set red_debito = d.debito * 0.98453 %} + {% set red_mp = d.mp * 0.98691 %} + {% set redelcom_neto = red_credito + red_debito + red_mp %} + {% set efectivo_menos_gastos = d.efectivo - d.gastos %} + {{ ("$" ~ "{:,.0f}".format(red_credito).replace(',', '.')) if red_credito > 0 else "-" }} + {{ ("$" ~ "{:,.0f}".format(red_debito).replace(',', '.')) if red_debito > 0 else "-" }} + {{ ("$" ~ "{:,.0f}".format(red_mp).replace(',', '.')) if red_mp > 0 else "-" }} + {{ ("$" ~ "{:,.0f}".format(redelcom_neto).replace(',', '.')) if redelcom_neto > 0 else "-" }} + {{ ("$" ~ "{:,.0f}".format(efectivo_menos_gastos).replace(',', '.')) if efectivo_menos_gastos != 0 else "-" }} + {{ ("$" ~ "{:,.0f}".format(redelcom_neto + efectivo_menos_gastos).replace(',', '.')) if (redelcom_neto + efectivo_menos_gastos) != 0 else "-" }} {% endfor %} @@ -125,6 +144,14 @@ ${{ "{:,.0f}".format(totales_mes.debito).replace(',', '.') }} ${{ "{:,.0f}".format(totales_mes.mp).replace(',', '.') }} ${{ "{:,.0f}".format(totales_mes.efectivo).replace(',', '.') }} + ${{ "{:,.0f}".format(totales_mes.credito * 0.97620).replace(',', '.') }} + ${{ "{:,.0f}".format(totales_mes.debito * 0.98453).replace(',', '.') }} + ${{ "{:,.0f}".format(totales_mes.mp * 0.98691).replace(',', '.') }} + {% set t_redelcom = totales_mes.credito * 0.97620 + totales_mes.debito * 0.98453 + totales_mes.mp * 0.98691 %} + {% set t_efectivo_menos = totales_mes.efectivo - totales_mes.gastos %} + ${{ "{:,.0f}".format(t_redelcom).replace(',', '.') }} + ${{ "{:,.0f}".format(t_efectivo_menos).replace(',', '.') }} + ${{ "{:,.0f}".format(t_redelcom + t_efectivo_menos).replace(',', '.') }} diff --git a/templates/macros/modals.html b/templates/macros/modals.html index ca19025..178d29e 100644 --- a/templates/macros/modals.html +++ b/templates/macros/modals.html @@ -328,7 +328,7 @@
{{ rendicion[2] }} {% if session.get('is_admin') %} - {% if rendicion[14] %}$ Si Recibe Comision{% else %}$ No Recibe Comision{% endif %} + {% if rendicion[14] %}Si Recibe Comision{% else %}No Recibe Comision{% endif %} {% endif %}
@@ -339,7 +339,21 @@ {{ rendicion[10] }} {% if session.get('is_admin') %} - {% if rendicion[15] %}$ Si Recibe Comision{% else %}$ No Recibe Comision{% endif %} + {% if rendicion[15] %}Si Recibe Comision{% else %}No Recibe Comision{% endif %} + + {% endif %} + {% else %} + Sin acompañante + {% endif %} + + +
Acompañante 2
+
+ {% if rendicion[16] %} + {{ rendicion[16] }} + {% if session.get('is_admin') %} + + {% if rendicion[18] %}Si Recibe Comision{% else %}No Recibe Comision{% endif %} {% endif %} {% else %} @@ -352,19 +366,19 @@
- Débito (x{{ rendicion[16] }}): + Débito (x{{ rendicion[19] or 0 }}): ${{ "{:,.0f}".format(rendicion[4] or 0).replace(',', '.') }}
- Crédito (x{{ rendicion[17] }}): + Crédito (x{{ rendicion[20] or 0 }}): ${{ "{:,.0f}".format(rendicion[5] or 0).replace(',', '.') }}
- Mercado Pago (x{{ rendicion[18] }}): + Mercado Pago (x{{ rendicion[21] or 0 }}): ${{ "{:,.0f}".format(rendicion[6] or 0).replace(',', '.') }}
- Efectivo (x{{ rendicion[19] }}): + Efectivo (x{{ rendicion[22] or 0 }}): ${{ "{:,.0f}".format(rendicion[7] or 0).replace(',', '.') }}
@@ -451,7 +465,7 @@ Total Calculado por Sistema: - ${{ "{:,.0f}".format(rendicion[21] or 0).replace(',', '.') }} + ${{ "{:,.0f}".format(rendicion[24] or 0).replace(',', '.') }} @@ -484,7 +498,7 @@
- +
@@ -503,7 +517,26 @@
- + +
+
+ + +
+ + +
+
+
+ +
@@ -514,7 +547,7 @@
Boletas - +
@@ -522,7 +555,7 @@
Boletas - +
@@ -530,7 +563,7 @@
Boletas - +
@@ -538,7 +571,7 @@
Boletas - +