From 606ed947227e7ca44db855e4d7c1c47b7356ab79 Mon Sep 17 00:00:00 2001 From: Shiro-Nek0 Date: Mon, 22 Jun 2026 03:35:06 -0400 Subject: [PATCH] feat: add complementos feature for linking multiple accessories and quantities to products --- database.py | 13 ++++++ models/models.py | 21 +++++++++ routes/admin_bp.py | 75 +++++++++++++++++++++++++++++-- static/js/admin_productos.js | 13 ++++++ templates/admin_productos.html | 32 +++++++++++++- templates/macros/modals.html | 81 ++++++++++++++++++++++++++++++++++ 6 files changed, 231 insertions(+), 4 deletions(-) diff --git a/database.py b/database.py index e84e396..3445884 100644 --- a/database.py +++ b/database.py @@ -279,6 +279,19 @@ def init_db(): FOREIGN KEY (rendicion_id) REFERENCES rendiciones(id), FOREIGN KEY (producto_id) REFERENCES productos(id))''') + # Complementos tables + c.execute('''CREATE TABLE IF NOT EXISTS complementos + (id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL)''') + c.execute('''CREATE TABLE IF NOT EXISTS producto_complementos + (id INTEGER PRIMARY KEY AUTOINCREMENT, + producto_id INTEGER NOT NULL, + complemento_id INTEGER NOT NULL, + cantidad INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (producto_id) REFERENCES productos(id) ON DELETE CASCADE, + FOREIGN KEY (complemento_id) REFERENCES complementos(id) ON DELETE CASCADE, + UNIQUE(producto_id, complemento_id))''') + # Migrate: add bank fields if missing for col in ['nombre_banco', 'numero_cuenta', 'tipo_cuenta', 'rut_banco']: try: diff --git a/models/models.py b/models/models.py index 53c5e94..9a18657 100644 --- a/models/models.py +++ b/models/models.py @@ -100,3 +100,24 @@ class RendicionItem(db.Model): cantidad = db.Column(db.Integer, nullable=False) precio_historico = db.Column(db.Integer, nullable=False) comision_historica = db.Column(db.Integer, nullable=False) + + +class Complemento(db.Model): + __tablename__ = 'complementos' + __table_args__ = _TABLE_ARGS + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, unique=True, nullable=False) + + +class ProductoComplemento(db.Model): + __tablename__ = 'producto_complementos' + __table_args__ = _TABLE_ARGS + + id = db.Column(db.Integer, primary_key=True) + producto_id = db.Column(db.Integer, db.ForeignKey('productos.id', ondelete='CASCADE'), nullable=False) + complemento_id = db.Column(db.Integer, db.ForeignKey('complementos.id', ondelete='CASCADE'), nullable=False) + cantidad = db.Column(db.Integer, nullable=False, default=1) + + producto = db.relationship('Producto', backref=db.backref('complementos_assoc', lazy=True, cascade="all, delete-orphan")) + complemento = db.relationship('Complemento', backref=db.backref('productos_assoc', lazy=True)) diff --git a/routes/admin_bp.py b/routes/admin_bp.py index 9d198e5..032b534 100644 --- a/routes/admin_bp.py +++ b/routes/admin_bp.py @@ -6,7 +6,7 @@ from datetime import date, datetime from models.models import ( db, Zona, Modulo, Producto, PrecioHistorico, - Worker, Rendicion, RendicionItem, + Worker, Rendicion, RendicionItem, Complemento, ProductoComplemento, ) from utils import ( admin_required, validate_rut, format_rut, validate_phone, @@ -336,7 +336,13 @@ def manage_products(): productos = Producto.query.order_by(Producto.name).all() productos_dict = {} for p in productos: - productos_dict[p.id] = {'id': p.id, 'name': p.name, 'precios': {}, 'futuros': []} + productos_dict[p.id] = { + 'id': p.id, + 'name': p.name, + 'precios': {}, + 'futuros': [], + 'complementos': [] + } for z in zonas: z_id, z_name = z price, comm = price_map.get((p.id, z_id), (None, None)) @@ -364,7 +370,23 @@ def manage_products(): 'fecha': ph.fecha_activacion, }) - return render_template('admin_productos.html', zonas=zonas, productos=productos_dict.values()) + # Fetch associated complementos + assoc_rows = ( + db.session.query(ProductoComplemento.id, ProductoComplemento.producto_id, ProductoComplemento.cantidad, Complemento.name) + .join(Complemento, ProductoComplemento.complemento_id == Complemento.id) + .all() + ) + for assoc_id, prod_id, cantidad, comp_name in assoc_rows: + if prod_id in productos_dict: + productos_dict[prod_id]['complementos'].append({ + 'id': assoc_id, + 'name': comp_name, + 'cantidad': cantidad + }) + + complementos_catalogo = Complemento.query.order_by(Complemento.name).all() + + return render_template('admin_productos.html', zonas=zonas, productos=productos_dict.values(), complementos_catalogo=complementos_catalogo) @admin_bp.route('/productos/delete/', methods=['POST']) @@ -383,6 +405,53 @@ def delete_product(id): return redirect(url_for('admin.manage_products')) +@admin_bp.route('/productos//complementos/add', methods=['POST']) +@admin_required +def add_producto_complemento(prod_id): + comp_id = request.form.get('complemento_id') + comp_name_nuevo = request.form.get('complemento_nombre_nuevo', '').strip() + cantidad = int(request.form.get('cantidad', 1) or 1) + + if comp_id == '__nuevo__': + if not comp_name_nuevo: + flash("Debes ingresar el nombre del nuevo complemento.", "danger") + return redirect(url_for('admin.manage_products')) + + comp = Complemento.query.filter_by(name=comp_name_nuevo).first() + if not comp: + comp = Complemento(name=comp_name_nuevo) + db.session.add(comp) + db.session.flush() + else: + comp = db.session.get(Complemento, int(comp_id)) + + if not comp: + flash("Complemento no encontrado.", "danger") + return redirect(url_for('admin.manage_products')) + + assoc = ProductoComplemento.query.filter_by(producto_id=prod_id, complemento_id=comp.id).first() + if assoc: + assoc.cantidad += cantidad + else: + assoc = ProductoComplemento(producto_id=prod_id, complemento_id=comp.id, cantidad=cantidad) + db.session.add(assoc) + + db.session.commit() + flash("Complemento vinculado al producto exitosamente.", "success") + return redirect(url_for('admin.manage_products')) + + +@admin_bp.route('/productos/complementos/delete/', methods=['POST']) +@admin_required +def delete_producto_complemento(assoc_id): + assoc = db.session.get(ProductoComplemento, assoc_id) + if assoc: + db.session.delete(assoc) + db.session.commit() + flash("Complemento desvinculado del producto.", "info") + return redirect(url_for('admin.manage_products')) + + @admin_bp.route('/productos/precios/', methods=['POST']) @admin_required def update_product_prices(id): diff --git a/static/js/admin_productos.js b/static/js/admin_productos.js index f229266..4f9e0ce 100644 --- a/static/js/admin_productos.js +++ b/static/js/admin_productos.js @@ -39,4 +39,17 @@ document.addEventListener("DOMContentLoaded", function () { }); }); } + + window.toggleNuevoComplementoInput = function(prodId, selectEl) { + const wrapper = document.getElementById(`nuevo_comp_wrapper_${prodId}`); + if (wrapper) { + if (selectEl.value === '__nuevo__') { + wrapper.style.display = 'block'; + wrapper.querySelector('input').setAttribute('required', 'required'); + } else { + wrapper.style.display = 'none'; + wrapper.querySelector('input').removeAttribute('required'); + } + } + }; }); diff --git a/templates/admin_productos.html b/templates/admin_productos.html index e066b16..578c27f 100644 --- a/templates/admin_productos.html +++ b/templates/admin_productos.html @@ -1,5 +1,5 @@ {% extends "macros/base.html" %} -{% from 'macros/modals.html' import confirm_modal, edit_product_modal, add_product_modal %} +{% from 'macros/modals.html' import confirm_modal, edit_product_modal, add_product_modal, manage_complementos_modal %} {% from "macros/ui.html" import flashed_messages %} {% block title %}Catálogo de Productos{% endblock %} @@ -28,6 +28,7 @@ Producto Maestro + Complementos de Regalo Acciones @@ -35,10 +36,22 @@ {% for prod in productos %} {{ prod.name }} + + {% if prod.complementos %} + {% for comp in prod.complementos %} + {{ comp.name }} (x{{ comp.cantidad }}) + {% endfor %} + {% else %} + Sin complementos + {% endif %} + + @@ -54,6 +67,7 @@ btn_class='btn-danger', btn_text='Eliminar' ) }} + {{ manage_complementos_modal(prod, complementos_catalogo) }} {% endfor %} @@ -174,6 +188,22 @@ diff --git a/templates/macros/modals.html b/templates/macros/modals.html index f83caad..29590f1 100644 --- a/templates/macros/modals.html +++ b/templates/macros/modals.html @@ -799,4 +799,85 @@ +{% endmacro %} + + +{% macro manage_complementos_modal(prod, complementos_catalogo) %} + {% endmacro %} \ No newline at end of file