Created
April 23, 2025 21:24
-
-
Save joaquin-papagianacopoulos/3b266f599ffb00af22f2ea40f0bf68d7 to your computer and use it in GitHub Desktop.
to claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from kivymd.app import MDApp | |
from kivymd.uix.textfield import MDTextField | |
from kivymd.uix.button import MDRaisedButton, MDFlatButton, MDIconButton | |
from kivymd.uix.menu import MDDropdownMenu | |
from kivymd.uix.list import OneLineListItem, TwoLineListItem | |
from kivymd.uix.dialog import MDDialog | |
from kivymd.uix.label import MDLabel | |
from kivymd.uix.boxlayout import MDBoxLayout | |
from kivymd.uix.screen import Screen | |
from kivymd.uix.scrollview import MDScrollView | |
from kivy.metrics import dp, sp | |
from kivy.core.window import Window | |
import sqlite3 | |
import os | |
from fpdf import FPDF # Solo usamos FPDF en lugar de ReportLab | |
from functools import partial | |
from datetime import datetime | |
import csv | |
from kivy.uix.filechooser import FileChooserListView | |
from kivy.uix.popup import Popup | |
from kivy.config import Config | |
from kivy.utils import platform | |
import sys | |
# Asegurar que los widgets tengan tamaño adecuado para dedos | |
TOUCH_BUTTON_HEIGHT = dp(56) # Altura mínima recomendada para botones táctiles | |
TOUCH_ITEM_HEIGHT = dp(48) # Altura mínima para elementos de lista | |
SPACING_TOUCH = dp(12) # Espaciado adecuado para dedos | |
LIST_ITEM_HEIGHT = dp(72) # Altura para elementos de lista con doble línea | |
try: | |
from android.permissions import request_permissions, Permission, check_permission | |
request_permissions([Permission.INTERNET, Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_EXTERNAL_STORAGE]) | |
except ImportError: | |
print("No se están gestionando permisos porque no estamos en Android.") | |
# Configuración específica para Android | |
def configure_for_android(): | |
if platform == 'android': | |
# Evitar que el teclado empuje los widgets hacia arriba | |
Window.softinput_mode = "below_target" | |
# Ajustar colores de la barra de estado | |
try: | |
from android.runnable import run_on_ui_thread | |
from jnius import autoclass | |
Color = autoclass("android.graphics.Color") | |
activity = autoclass('org.kivy.android.PythonActivity').mActivity | |
@run_on_ui_thread | |
def set_status_bar_color(color): | |
window = activity.getWindow() | |
window.setStatusBarColor(color) | |
# Hacer el texto de la barra de estado oscuro si el fondo es claro | |
window.getDecorView().setSystemUiVisibility(1) | |
# Establecer color de la barra de estado (puedes ajustar según tu app) | |
set_status_bar_color(Color.parseColor('#303F9F')) | |
except Exception as e: | |
print(f"No se pudo configurar la barra de estado: {str(e)}") | |
def check_permissions(): | |
"""Solicita permisos necesarios en Android de forma más robusta""" | |
if platform == 'android': | |
try: | |
from android.permissions import request_permissions, Permission, check_permission | |
permissions = [ | |
Permission.INTERNET, | |
Permission.WRITE_EXTERNAL_STORAGE, | |
Permission.READ_EXTERNAL_STORAGE | |
] | |
# Comprobar si ya tenemos los permisos | |
missing_permissions = [] | |
for perm in permissions: | |
if not check_permission(perm): | |
missing_permissions.append(perm) | |
# Solicitar solo permisos que faltan | |
if missing_permissions: | |
request_permissions(missing_permissions) | |
return True | |
except: | |
print("Error al comprobar/solicitar permisos") | |
return False | |
return True | |
def get_app_dir(): | |
"""Obtiene el directorio adecuado para almacenar datos de la aplicación""" | |
if platform == 'android': | |
try: | |
from android.storage import primary_external_storage_path | |
base_dir = primary_external_storage_path() | |
app_dir = os.path.join(base_dir, "DistriApp") | |
# Crear el directorio si no existe | |
if not os.path.exists(app_dir): | |
os.makedirs(app_dir) | |
return app_dir | |
except ImportError: | |
print("No se pudo importar android.storage (no estás en Android o falta dependencia)") | |
return os.getcwd() | |
else: | |
return os.path.dirname(os.path.abspath(__file__)) | |
def get_db_path(): | |
app_dir = get_app_dir() | |
db_file = 'distriapp.db' | |
return os.path.join(app_dir, db_file) | |
# Evitar que escanee archivos del sistema | |
Config.set('kivy', 'log_level', 'warning') | |
Config.set('kivy', 'log_dir', 'logs') | |
Config.set('kivy', 'log_name', 'kivy_%y-%m-%d_%_.txt') | |
Config.set('kivy', 'log_enable', 0) | |
# Desactivar círculos de contacto | |
Config.set('input', 'mouse', 'mouse,disable_multitouch') | |
def conectar_bd(): | |
"""Conecta a la base de datos SQLite""" | |
db_path = get_db_path() | |
return sqlite3.connect(db_path) | |
def inicializar_bd(): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute('''CREATE TABLE IF NOT EXISTS productos ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
nombre TEXT UNIQUE, | |
costo REAL, | |
precio_venta REAL, | |
stock INTEGER DEFAULT 0, | |
codigo TEXT | |
)''') | |
cursor.execute('''CREATE TABLE IF NOT EXISTS pedidos ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
cliente TEXT, | |
producto TEXT, | |
cantidad INTEGER, | |
costo REAL, | |
zona TEXT, | |
fecha DATE DEFAULT (date('now')) | |
)''') | |
cursor.execute('''CREATE TABLE IF NOT EXISTS clientes ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
nombre TEXT UNIQUE | |
)''') | |
conn.commit() | |
cursor.close() | |
conn.close() | |
def obtener_clientes(texto_ingresado): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT nombre FROM clientes WHERE nombre LIKE ? LIMIT 5", (f"%{texto_ingresado}%",)) | |
clientes = [cliente[0] for cliente in cursor.fetchall()] | |
cursor.close() | |
conn.close() | |
return clientes | |
def obtener_productos(texto_ingresado): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT DISTINCT nombre FROM productos WHERE nombre LIKE ? LIMIT 5", (f"%{texto_ingresado}%",)) | |
productos = [producto[0] for producto in cursor.fetchall()] | |
cursor.close() | |
conn.close() | |
return productos | |
def insertar_pedido(cliente, producto, cantidad, costo, zona): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("INSERT INTO pedidos (cliente, producto, cantidad, costo, zona) VALUES (?, ?, ?, ?, ?)", | |
(cliente, producto, cantidad, costo, zona)) | |
conn.commit() | |
cursor.close() | |
conn.close() | |
def obtener_costo_producto(nombre_producto): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT costo FROM productos WHERE nombre = ?", (nombre_producto,)) | |
resultado = cursor.fetchone() | |
cursor.close() | |
conn.close() | |
return resultado[0] if resultado else "" | |
def obtener_stock_producto(nombre_producto): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT stock FROM productos WHERE nombre = ?", (nombre_producto,)) | |
resultado = cursor.fetchone() | |
cursor.close() | |
conn.close() | |
return resultado[0] if resultado else 0 | |
def obtener_producto_por_codigo(codigo): | |
"""Busca un producto por su código""" | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT nombre FROM productos WHERE codigo = ?", (codigo,)) | |
resultado = cursor.fetchone() | |
cursor.close() | |
conn.close() | |
return resultado[0] if resultado else None | |
# --- Funciones de utilidad --- | |
def mostrar_notificacion(mensaje): | |
dialog = MDDialog( | |
title="Notificación", | |
text=mensaje, | |
buttons=[MDFlatButton( | |
text="OK", | |
on_release=lambda x: dialog.dismiss(), | |
font_size=sp(16) | |
)] | |
) | |
dialog.open() | |
def crear_menu_sugerencias(app, items, campo): | |
menu_items = [ | |
{"text": item, "viewclass": "OneLineListItem", "on_release": lambda x=item: app.seleccionar_sugerencia(x, campo)} | |
for item in items | |
] | |
if hasattr(app, 'menu') and app.menu: | |
app.menu.dismiss() | |
app.menu = MDDropdownMenu(caller=campo, items=menu_items, width_mult=4) | |
app.menu.open() | |
def actualizar_stock_desde_csv(archivo): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
try: | |
with open(archivo, 'r', encoding='utf-8-sig') as f: | |
lector = csv.DictReader(f) | |
required = {'nombre', 'costo', 'precio_venta', 'stock'} | |
if not required.issubset(lector.fieldnames): | |
missing = required - set(lector.fieldnames) | |
raise ValueError(f"Faltan columnas: {', '.join(missing)}") | |
for idx, fila in enumerate(lector, 2): | |
try: | |
# Verificar si ya existe el producto | |
cursor.execute("SELECT id FROM productos WHERE nombre = ?", (fila['nombre'].strip(),)) | |
producto_existe = cursor.fetchone() | |
if producto_existe: | |
# Actualizar producto existente | |
cursor.execute(''' | |
UPDATE productos SET | |
costo = ?, | |
precio_venta = ?, | |
stock = ? | |
WHERE nombre = ? | |
''', ( | |
float(fila['costo']), | |
float(fila['precio_venta']), | |
int(fila['stock']), | |
fila['nombre'].strip() | |
)) | |
else: | |
# Insertar nuevo producto | |
cursor.execute(''' | |
INSERT INTO productos (nombre, costo, precio_venta, stock) | |
VALUES (?, ?, ?, ?) | |
''', ( | |
fila['nombre'].strip(), | |
float(fila['costo']), | |
float(fila['precio_venta']), | |
int(fila['stock']) | |
)) | |
except Exception as e: | |
raise ValueError(f"Línea {idx}: {str(e)}") | |
conn.commit() | |
return True | |
except Exception as e: | |
conn.rollback() | |
raise e | |
finally: | |
cursor.close() | |
conn.close() | |
def obtener_ventas_diarias(): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute(''' | |
SELECT DATE(fecha) as dia, SUM(cantidad * costo) as total | |
FROM pedidos | |
GROUP BY dia | |
ORDER BY dia DESC | |
LIMIT 30 | |
''') | |
resultados = cursor.fetchall() | |
cursor.close() | |
conn.close() | |
return resultados | |
class PedidoApp(MDApp): | |
def build(self): | |
inicializar_bd() | |
check_permissions() | |
if platform == 'android': | |
configure_for_android() | |
self.menu = None | |
self.productos_temporal = [] | |
# Theme para la aplicación - colores más modernos para Android | |
self.theme_cls.primary_palette = "Blue" | |
self.theme_cls.accent_palette = "Amber" | |
self.theme_cls.theme_style = "Light" | |
self.screen = Screen() | |
# Para dispositivos móviles es mejor usar una organización vertical | |
# en lugar de horizontal cuando hay muchos elementos | |
if platform == 'android': | |
main_layout = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, padding=SPACING_TOUCH) | |
# Panel superior - Formulario de ingreso | |
top_panel = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, padding=SPACING_TOUCH, size_hint_y=0.5) | |
# Los campos de entrada más grandes para facilitar la entrada táctil | |
self.cliente = MDTextField(hint_text='Cliente ✍️', mode="rectangle", font_size=sp(16)) | |
self.cliente.bind(text=self.sugerir_clientes) | |
self.producto = MDTextField(hint_text='Producto 🔍', mode="rectangle", font_size=sp(16)) | |
self.producto.bind(text=self.sugerir_productos) | |
input_row1 = MDBoxLayout(orientation='horizontal', spacing=SPACING_TOUCH, size_hint_y=None, height=dp(56)) | |
self.cantidad = MDTextField(hint_text='Cantidad 🔢', input_filter='int', mode="rectangle", font_size=sp(16), size_hint_x=0.5) | |
self.costo = MDTextField(hint_text='Costo 💲', input_filter='float', mode="rectangle", font_size=sp(16), size_hint_x=0.5) | |
input_row1.add_widget(self.cantidad) | |
input_row1.add_widget(self.costo) | |
# Botones más grandes para facilitar el toque | |
self.zona = MDRaisedButton( | |
text='📍 Zona', | |
on_release=self.mostrar_zonas, | |
size_hint=(1, None), | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
# Agregar elementos al panel superior | |
top_panel.add_widget(self.cliente) | |
top_panel.add_widget(self.producto) | |
top_panel.add_widget(input_row1) | |
top_panel.add_widget(self.zona) | |
# Botones principales | |
buttons_layout1 = MDBoxLayout(orientation='horizontal', spacing=SPACING_TOUCH, size_hint_y=None, height=TOUCH_BUTTON_HEIGHT) | |
self.boton_registrar = MDRaisedButton( | |
text='✅ Registrar', | |
on_release=self.registrar_pedido, | |
size_hint_x=0.5, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
self.boton_pedidos_hoy = MDRaisedButton( | |
text='📄 Pedidos Hoy', | |
on_release=self.ver_pedidos_dia, | |
size_hint_x=0.5, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
buttons_layout1.add_widget(self.boton_registrar) | |
buttons_layout1.add_widget(self.boton_pedidos_hoy) | |
top_panel.add_widget(buttons_layout1) | |
# Más botones | |
buttons_layout2 = MDBoxLayout(orientation='horizontal', spacing=SPACING_TOUCH, size_hint_y=None, height=TOUCH_BUTTON_HEIGHT) | |
self.boton_modificar = MDRaisedButton( | |
text='✏️ Modificar', | |
on_release=self.mostrar_clientes_para_editar, | |
size_hint_x=0.5, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
self.boton_csv = MDRaisedButton( | |
text='📤 Subir CSV', | |
on_release=self.mostrar_file_chooser, | |
size_hint_x=0.5, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
buttons_layout2.add_widget(self.boton_modificar) | |
buttons_layout2.add_widget(self.boton_csv) | |
top_panel.add_widget(buttons_layout2) | |
# Más botones | |
buttons_layout3 = MDBoxLayout(orientation='horizontal', spacing=SPACING_TOUCH, size_hint_y=None, height=TOUCH_BUTTON_HEIGHT) | |
self.boton_estadisticas = MDRaisedButton( | |
text='📊 Estadísticas', | |
on_release=self.mostrar_estadisticas, | |
size_hint_x=0.5, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
self.boton_productos_dia = MDRaisedButton( | |
text='📦 PRODUCTOS', | |
on_release=self.generar_productos_por_dia, | |
md_bg_color=(0.8, 0.2, 0.2, 1), | |
size_hint_x=0.5, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
buttons_layout3.add_widget(self.boton_estadisticas) | |
buttons_layout3.add_widget(self.boton_productos_dia) | |
top_panel.add_widget(buttons_layout3) | |
# Panel inferior - Lista de productos | |
bottom_panel = MDBoxLayout(orientation='vertical', size_hint_y=0.5) | |
# Etiqueta para la lista | |
bottom_panel.add_widget( | |
MDLabel( | |
text="Productos en pedido actual", | |
size_hint_y=None, | |
height=dp(36), | |
font_style="H6", | |
halign="center" | |
) | |
) | |
# Lista de productos con scroll | |
self.lista_productos = MDScrollView() | |
self.contenedor_productos = MDBoxLayout(orientation='vertical', size_hint_y=None, spacing=SPACING_TOUCH) | |
self.contenedor_productos.bind(minimum_height=self.contenedor_productos.setter('height')) | |
self.lista_productos.add_widget(self.contenedor_productos) | |
bottom_panel.add_widget(self.lista_productos) | |
# Botones de control | |
controles = MDBoxLayout( | |
size_hint_y=None, | |
height=TOUCH_BUTTON_HEIGHT, | |
spacing=SPACING_TOUCH, | |
padding=(0, SPACING_TOUCH, 0, SPACING_TOUCH) | |
) | |
for btn in [ | |
MDRaisedButton( | |
text='✏️ Editar', | |
on_release=self.editar_orden_actual, | |
size_hint_x=1/3, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
), | |
MDRaisedButton( | |
text='🗑️ Vaciar', | |
on_release=self.vaciar_orden_actual, | |
size_hint_x=1/3, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
), | |
MDRaisedButton( | |
text='🚀 Enviar', | |
on_release=self.guardar_pedido_completo, | |
size_hint_x=1/3, | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
]: | |
controles.add_widget(btn) | |
bottom_panel.add_widget(controles) | |
# Agregar los paneles al layout principal | |
main_layout.add_widget(top_panel) | |
main_layout.add_widget(bottom_panel) | |
else: | |
# Mantener el diseño horizontal original para tablets y escritorio | |
main_layout = MDBoxLayout(orientation='horizontal', spacing=20, padding=20) | |
# Panel izquierdo | |
left_panel = MDBoxLayout(orientation='vertical', size_hint=(0.4, 1), spacing=15, padding=(10, 10, 10, 10)) | |
self.cliente = MDTextField(hint_text='Cliente ✍️') | |
self.cliente.bind(text=self.sugerir_clientes) | |
self.producto = MDTextField(hint_text='Producto 🔍') | |
self.producto.bind(text=self.sugerir_productos) | |
self.cantidad = MDTextField(hint_text='Cantidad 🔢', input_filter='int') | |
self.costo = MDTextField(hint_text='Costo 💲', input_filter='float') | |
self.zona = MDRaisedButton(text='📍 Zona', on_release=self.mostrar_zonas) | |
# Botones principales - Primera fila | |
left_panel.add_widget(self.cliente) | |
left_panel.add_widget(self.producto) | |
left_panel.add_widget(self.cantidad) | |
left_panel.add_widget(self.costo) | |
left_panel.add_widget(self.zona) | |
# Crear botones principales con MDBoxLayout para una mejor organización | |
buttons_layout1 = MDBoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=dp(48)) | |
buttons_layout2 = MDBoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=dp(48)) | |
buttons_layout3 = MDBoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=dp(48)) | |
# Organizar botones en filas | |
self.boton_registrar = MDRaisedButton(text='✅ Registrar', on_release=self.registrar_pedido, size_hint_x=0.5) | |
self.boton_pedidos_hoy = MDRaisedButton(text='📄 Pedidos Hoy', on_release=self.ver_pedidos_dia, size_hint_x=0.5) | |
buttons_layout1.add_widget(self.boton_registrar) | |
buttons_layout1.add_widget(self.boton_pedidos_hoy) | |
self.boton_modificar = MDRaisedButton(text='✏️ Modificar', on_release=self.mostrar_clientes_para_editar, size_hint_x=0.5) | |
self.boton_csv = MDRaisedButton(text='📤 Subir CSV', on_release=self.mostrar_file_chooser, size_hint_x=0.5) | |
buttons_layout2.add_widget(self.boton_modificar) | |
buttons_layout2.add_widget(self.boton_csv) | |
self.boton_estadisticas = MDRaisedButton(text='📊 Estadísticas', on_release=self.mostrar_estadisticas, size_hint_x=0.5) | |
self.boton_productos_dia = MDRaisedButton( | |
text='📦 PRODUCTOS POR DÍA', | |
on_release=self.generar_productos_por_dia, | |
md_bg_color=(0.8, 0.2, 0.2, 1), | |
size_hint_x=0.5 | |
) | |
buttons_layout3.add_widget(self.boton_estadisticas) | |
buttons_layout3.add_widget(self.boton_productos_dia) | |
left_panel.add_widget(buttons_layout1) | |
left_panel.add_widget(buttons_layout2) | |
left_panel.add_widget(buttons_layout3) | |
# Panel derecho | |
right_panel = MDBoxLayout(orientation='vertical', size_hint=(0.6, 1), padding=(10, 10, 10, 10)) | |
self.lista_productos = MDScrollView() | |
self.contenedor_productos = MDBoxLayout(orientation='vertical', size_hint_y=None, spacing=10) | |
self.contenedor_productos.bind(minimum_height=self.contenedor_productos.setter('height')) | |
self.lista_productos.add_widget(self.contenedor_productos) | |
right_panel.add_widget(self.lista_productos) | |
# Botones de control | |
controles = MDBoxLayout(size_hint_y=None, height=dp(60), spacing=10, padding=(0, 10, 0, 0)) | |
for btn in [ | |
MDRaisedButton(text='✏️ Editar', on_release=self.editar_orden_actual, size_hint_x=1/3), | |
MDRaisedButton(text='🗑️ Vaciar', on_release=self.vaciar_orden_actual, size_hint_x=1/3), | |
MDRaisedButton(text='🚀 Enviar', on_release=self.guardar_pedido_completo, size_hint_x=1/3) | |
]: | |
controles.add_widget(btn) | |
right_panel.add_widget(controles) | |
main_layout.add_widget(left_panel) | |
main_layout.add_widget(right_panel) | |
self.screen.add_widget(main_layout) | |
return self.screen | |
def mostrar_file_chooser(self, instance): | |
content = MDBoxLayout(orientation='vertical') | |
file_chooser = FileChooserListView(filters=['*.csv']) | |
btn = MDRaisedButton( | |
text="Cargar CSV", | |
on_release=lambda x: self.procesar_csv(file_chooser.selection, popup), | |
size_hint=(1, None), | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
content.add_widget(file_chooser) | |
content.add_widget(btn) | |
popup = Popup(title="Seleccionar CSV", content=content, size_hint=(0.9, 0.9)) | |
popup.open() | |
def editar_orden_actual(self, instance): | |
if not self.productos_temporal: | |
mostrar_notificacion("⚠️ No hay productos en la orden actual") | |
return | |
# Crear un BoxLayout con padding superior adicional para evitar superposición con el título | |
scroll_container = MDScrollView(size_hint=(1, None), height=dp(300)) | |
main_container = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, size_hint_y=None) | |
main_container.bind(minimum_height=main_container.setter('height')) | |
# Añadir un espacio vacío en la parte superior para evitar superposición con el título | |
spacer = MDBoxLayout(size_hint_y=None, height=dp(20)) | |
main_container.add_widget(spacer) | |
for producto in self.productos_temporal: | |
item = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=LIST_ITEM_HEIGHT, padding=(5, 5, 5, 5)) | |
# Contenedor para el texto con padding superior | |
texto_container = MDBoxLayout(orientation='vertical', size_hint_x=0.8, padding=(0, 10, 0, 0)) | |
texto_container.add_widget(TwoLineListItem( | |
text=producto['producto'], | |
secondary_text=f"Cant: {producto['cantidad']} | Costo: ${producto['costo']}", | |
divider=None, | |
_no_ripple_effect=True, | |
font_style="Subtitle1" | |
)) | |
item.add_widget(texto_container) | |
btn_eliminar = MDIconButton( | |
icon='delete', | |
on_release=partial(self.eliminar_de_orden_actual, producto), | |
size_hint_x=0.2, | |
icon_size=dp(32) | |
) | |
item.add_widget(btn_eliminar) | |
main_container.add_widget(item) | |
scroll_container.add_widget(main_container) | |
self.dialog_edicion_actual = MDDialog( | |
title="Editar orden actual", | |
type="custom", | |
content_cls=scroll_container, | |
buttons=[ | |
MDFlatButton( | |
text="Cerrar", | |
on_release=lambda x: self.dialog_edicion_actual.dismiss(), | |
font_size=sp(16) | |
) | |
], | |
size_hint=(0.9, None), | |
height=dp(400) | |
) | |
self.dialog_edicion_actual.open() | |
def eliminar_de_orden_actual(self, producto, instance): | |
self.productos_temporal.remove(producto) | |
self.actualizar_lista_temporal() | |
if hasattr(self, 'dialog_edicion_actual') and self.dialog_edicion_actual: | |
self.dialog_edicion_actual.dismiss() | |
mostrar_notificacion("Producto eliminado de la orden actual") | |
def vaciar_orden_actual(self, instance): | |
self.productos_temporal.clear() | |
self.contenedor_productos.clear_widgets() | |
mostrar_notificacion("Orden vaciada completamente") | |
def guardar_pedido_completo(self, instance): | |
if not self.productos_temporal: | |
mostrar_notificacion("⚠️ No hay productos en la orden") | |
return | |
cliente = self.cliente.text.strip() | |
zona = self.zona.text.strip() | |
if not cliente: | |
mostrar_notificacion("⚠️ Debe seleccionar un cliente") | |
return | |
if zona == "📍 Zona": | |
mostrar_notificacion("⚠️ Debe seleccionar una zona") | |
return | |
try: | |
for producto in self.productos_temporal: | |
insertar_pedido( | |
cliente=cliente, | |
producto=producto['producto'], | |
cantidad=producto['cantidad'], | |
costo=producto['costo'], | |
zona=zona | |
) | |
mostrar_notificacion("✅ Pedido guardado exitosamente!") | |
self.vaciar_orden_actual(None) | |
except Exception as e: | |
mostrar_notificacion(f"❌ Error al guardar: {str(e)}") | |
def actualizar_lista_temporal(self): | |
self.contenedor_productos.clear_widgets() | |
for producto in self.productos_temporal: | |
item = MDBoxLayout( | |
orientation='horizontal', | |
size_hint_y=None, | |
height=LIST_ITEM_HEIGHT, | |
padding=(SPACING_TOUCH/2, SPACING_TOUCH, SPACING_TOUCH/2, SPACING_TOUCH) | |
) | |
# Crear un contenedor box para el TwoLineListItem con padding superior | |
texto_container = MDBoxLayout(orientation='vertical', size_hint_x=0.85, padding=(0, 8, 0, 0)) | |
# Agregar el TwoLineListItem al contenedor | |
lista_item = TwoLineListItem( | |
text=producto['producto'], | |
secondary_text=f"Cant: {producto['cantidad']} | Costo: ${producto['costo']}", | |
divider=None, | |
_no_ripple_effect=True, | |
font_style="Subtitle1" | |
) | |
texto_container.add_widget(lista_item) | |
item.add_widget(texto_container) | |
# Botón eliminar | |
btn_eliminar = MDIconButton( | |
icon="delete", | |
theme_text_color="Error", | |
on_release=partial(self.eliminar_de_orden_actual, producto), | |
size_hint_x=0.15, | |
icon_size=dp(32) | |
) | |
item.add_widget(btn_eliminar) | |
self.contenedor_productos.add_widget(item) | |
def mostrar_estadisticas(self, instance): | |
"""Muestra un panel completo de estadísticas con diferentes métricas y gráficos""" | |
try: | |
# Crear un contenedor con tabs para diferentes tipos de estadísticas | |
from kivymd.uix.tab import MDTabsBase | |
from kivymd.uix.floatlayout import MDFloatLayout | |
class Tab(MDFloatLayout, MDTabsBase): | |
'''Clase para implementar cada pestaña''' | |
pass | |
# Obtener datos para las estadísticas | |
datos_ventas = self.obtener_datos_estadisticas() | |
# Creamos un contenedor principal con scroll | |
scroll_container = MDScrollView(size_hint=(1, None), height=dp(500)) | |
main_container = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, size_hint_y=None) | |
main_container.bind(minimum_height=main_container.setter('height')) | |
# --- Sección 1: Resumen de ventas --- | |
seccion_resumen = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(200), padding=(10, 10, 10, 10)) | |
# Título de la sección | |
titulo_resumen = MDBoxLayout(size_hint_y=None, height=dp(40)) | |
titulo_resumen.add_widget(OneLineListItem( | |
text="📊 Resumen de Ventas", | |
font_style="H6", | |
divider=None | |
)) | |
seccion_resumen.add_widget(titulo_resumen) | |
# Datos del resumen en una cuadrícula | |
datos_grid = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(150)) | |
# Obtener datos para el resumen | |
total_ventas, ganancia_total, prod_mas_vendidos = datos_ventas['resumen'] | |
# Columna 1: Total Facturado | |
col1 = MDBoxLayout(orientation='vertical', padding=(5, 5, 5, 5)) | |
col1.add_widget(OneLineListItem(text="Total Facturado", divider=None)) | |
col1.add_widget(MDRaisedButton( | |
text=f"${total_ventas:.2f}", | |
size_hint=(1, 0.6), | |
md_bg_color=(0.2, 0.6, 0.2, 1) | |
)) | |
datos_grid.add_widget(col1) | |
# Columna 2: Ganancia | |
col2 = MDBoxLayout(orientation='vertical', padding=(5, 5, 5, 5)) | |
col2.add_widget(OneLineListItem(text="Ganancia Total", divider=None)) | |
col2.add_widget(MDRaisedButton( | |
text=f"${ganancia_total:.2f}", | |
size_hint=(1, 0.6), | |
md_bg_color=(0.2, 0.4, 0.8, 1) | |
)) | |
datos_grid.add_widget(col2) | |
# Columna 3: Productos Vendidos - Calculamos la suma total de cantidades | |
col3 = MDBoxLayout(orientation='vertical', padding=(5, 5, 5, 5)) | |
col3.add_widget(OneLineListItem(text="Productos Vendidos", divider=None)) | |
# Calcular total de productos vendidos de forma segura | |
total_unidades = 0 | |
for producto_info in prod_mas_vendidos: | |
if len(producto_info) >= 2: # Asegurarse de que hay al menos 2 elementos | |
total_unidades += producto_info[1] # El segundo elemento debería ser la cantidad | |
col3.add_widget(MDRaisedButton( | |
text=f"{total_unidades} unidades", | |
size_hint=(1, 0.6), | |
md_bg_color=(0.8, 0.4, 0.2, 1) | |
)) | |
datos_grid.add_widget(col3) | |
seccion_resumen.add_widget(datos_grid) | |
main_container.add_widget(seccion_resumen) | |
# Separador | |
separador1 = MDBoxLayout(size_hint_y=None, height=dp(1), md_bg_color=(0.7, 0.7, 0.7, 1)) | |
main_container.add_widget(separador1) | |
# --- Sección 2: Productos más vendidos --- | |
seccion_productos = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(250), padding=(10, 10, 10, 10)) | |
# Título de la sección | |
titulo_productos = MDBoxLayout(size_hint_y=None, height=dp(40)) | |
titulo_productos.add_widget(OneLineListItem( | |
text="🏆 Productos Más Vendidos", | |
font_style="H6", | |
divider=None | |
)) | |
seccion_productos.add_widget(titulo_productos) | |
# Lista de productos más vendidos | |
lista_productos = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(200)) | |
# Encabezados | |
encabezados = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(30)) | |
encabezados.add_widget(MDLabel(text="Producto", size_hint_x=0.5)) | |
encabezados.add_widget(MDLabel(text="Cantidad", size_hint_x=0.25)) | |
encabezados.add_widget(MDLabel(text="Ingresos", size_hint_x=0.25)) | |
lista_productos.add_widget(encabezados) | |
# Mostrar los 5 productos más vendidos | |
for idx, producto_info in enumerate(datos_ventas['productos_top'][:5]): | |
if len(producto_info) >= 3: # Verificar que tiene al menos 3 elementos | |
producto, cantidad, ingreso = producto_info | |
item = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(30), | |
md_bg_color=(0.9, 0.9, 0.9, 1) if idx % 2 == 0 else (1, 1, 1, 1)) | |
item.add_widget(MDLabel(text=producto, size_hint_x=0.5)) | |
item.add_widget(MDLabel(text=str(cantidad), size_hint_x=0.25)) | |
item.add_widget(MDLabel(text=f"${ingreso:.2f}", size_hint_x=0.25)) | |
lista_productos.add_widget(item) | |
seccion_productos.add_widget(lista_productos) | |
main_container.add_widget(seccion_productos) | |
# Separador | |
separador2 = MDBoxLayout(size_hint_y=None, height=dp(1), md_bg_color=(0.7, 0.7, 0.7, 1)) | |
main_container.add_widget(separador2) | |
# --- Sección 3: Ventas por día --- | |
seccion_dias = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(250), padding=(10, 10, 10, 10)) | |
# Título de la sección | |
titulo_dias = MDBoxLayout(size_hint_y=None, height=dp(40)) | |
titulo_dias.add_widget(OneLineListItem( | |
text="📅 Ventas por Día", | |
font_style="H6", | |
divider=None | |
)) | |
seccion_dias.add_widget(titulo_dias) | |
# Lista de ventas por día | |
lista_dias = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(200)) | |
# Encabezados | |
encabezados_dias = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(30)) | |
encabezados_dias.add_widget(MDLabel(text="Fecha", size_hint_x=0.33)) | |
encabezados_dias.add_widget(MDLabel(text="Ventas", size_hint_x=0.33)) | |
encabezados_dias.add_widget(MDLabel(text="Variación", size_hint_x=0.34)) | |
lista_dias.add_widget(encabezados_dias) | |
# Mostrar las ventas de los últimos 7 días | |
ventas_dias = datos_ventas.get('ventas_dias', []) | |
ultimo_valor = None | |
for idx, dia_info in enumerate(ventas_dias[:7]): # Limitar a 7 días | |
if len(dia_info) >= 2: # Verificar que tiene al menos 2 elementos | |
fecha, total = dia_info | |
variacion = "---" | |
color = (0, 0, 0, 1) # Negro por defecto | |
if ultimo_valor is not None: | |
if total > ultimo_valor: | |
variacion = f"↑ {((total/ultimo_valor)-1)*100:.1f}%" | |
color = (0, 0.7, 0, 1) # Verde para aumento | |
elif total < ultimo_valor: | |
variacion = f"↓ {((ultimo_valor/total)-1)*100:.1f}%" | |
color = (0.7, 0, 0, 1) # Rojo para disminución | |
else: | |
variacion = "= 0%" | |
ultimo_valor = total | |
item = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(30), | |
md_bg_color=(0.9, 0.9, 0.9, 1) if idx % 2 == 0 else (1, 1, 1, 1)) | |
# Verificar si fecha es un objeto datetime o string | |
fecha_texto = fecha | |
if hasattr(fecha, 'strftime'): | |
fecha_texto = fecha.strftime('%d/%m/%Y') | |
item.add_widget(MDLabel(text=fecha_texto, size_hint_x=0.33)) | |
item.add_widget(MDLabel(text=f"${total:.2f}", size_hint_x=0.33)) | |
variacion_label = MDLabel(text=variacion, size_hint_x=0.34, theme_text_color="Custom", | |
text_color=color) | |
item.add_widget(variacion_label) | |
lista_dias.add_widget(item) | |
seccion_dias.add_widget(lista_dias) | |
main_container.add_widget(seccion_dias) | |
# --- Sección 4: Predicciones --- | |
if 'predicciones' in datos_ventas and datos_ventas['predicciones']: | |
seccion_predicciones = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(200), padding=(10, 10, 10, 10)) | |
# Título con indicador BETA | |
titulo_predicciones = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(40)) | |
titulo_predicciones.add_widget(MDLabel( | |
text="🔮 Predicciones de Venta", | |
font_style="H6" | |
)) | |
# Etiqueta BETA | |
beta_label = MDRaisedButton( | |
text="BETA", | |
md_bg_color=(0.8, 0.2, 0.8, 1), | |
text_color=(1, 1, 1, 1), | |
size_hint=(None, None), | |
size=(dp(60), dp(30)), | |
pos_hint={'center_y': 0.5} | |
) | |
titulo_predicciones.add_widget(beta_label) | |
seccion_predicciones.add_widget(titulo_predicciones) | |
# Mostrar predicciones | |
predicciones_container = MDBoxLayout(orientation='vertical', spacing=10) | |
for fecha, valor_esperado in datos_ventas['predicciones']: | |
pred_item = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(40)) | |
# Verificar si fecha es datetime o string | |
fecha_texto = fecha | |
if hasattr(fecha, 'strftime'): | |
fecha_texto = fecha.strftime('%d/%m/%Y') | |
pred_item.add_widget(MDLabel( | |
text=f"Predicción para {fecha_texto}:", | |
size_hint_x=0.6 | |
)) | |
pred_item.add_widget(MDRaisedButton( | |
text=f"${valor_esperado:.2f}", | |
size_hint_x=0.4, | |
md_bg_color=(0.6, 0.3, 0.8, 1) | |
)) | |
predicciones_container.add_widget(pred_item) | |
seccion_predicciones.add_widget(predicciones_container) | |
main_container.add_widget(seccion_predicciones) | |
# Ajustar altura total | |
main_container.height = sum(child.height for child in main_container.children) + dp(50) | |
scroll_container.add_widget(main_container) | |
# Crear y mostrar el diálogo | |
self.dialog_estadisticas = MDDialog( | |
title="Estadísticas y Análisis", | |
type="custom", | |
content_cls=scroll_container, | |
buttons=[ | |
MDRaisedButton( | |
text="Exportar PDF", | |
on_release=self.exportar_estadisticas_pdf, | |
font_size=sp(16) | |
), | |
MDFlatButton( | |
text="Cerrar", | |
on_release=lambda x: self.dialog_estadisticas.dismiss(), | |
font_size=sp(16) | |
) | |
], | |
size_hint=(0.9, None), | |
height=dp(600) | |
) | |
self.dialog_estadisticas.open() | |
except Exception as e: | |
import traceback | |
error_detalle = traceback.format_exc() | |
mostrar_notificacion(f"❌ Error al mostrar estadísticas: {str(e)}") | |
print(error_detalle) | |
def obtener_datos_estadisticas(self): | |
"""Obtiene todos los datos necesarios para las estadísticas""" | |
resultado = {} | |
try: | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
# 1. Obtener ventas de los últimos 30 días | |
cursor.execute(""" | |
SELECT DATE(fecha) as dia, SUM(cantidad * costo) as total | |
FROM pedidos | |
GROUP BY dia | |
ORDER BY dia DESC | |
LIMIT 30 | |
""") | |
ventas_dias_raw = cursor.fetchall() | |
# Convertir fechas a objetos datetime si es posible | |
ventas_dias = [] | |
from datetime import datetime | |
for fecha_str, total in ventas_dias_raw: | |
try: | |
# Intentar convertir la fecha (formato SQLite: YYYY-MM-DD) | |
fecha_obj = datetime.strptime(fecha_str, '%Y-%m-%d') | |
ventas_dias.append((fecha_obj, total)) | |
except: | |
# Si falla, usar el string original | |
ventas_dias.append((fecha_str, total)) | |
resultado['ventas_dias'] = ventas_dias | |
# 2. Obtener productos más vendidos | |
cursor.execute(""" | |
SELECT producto, SUM(cantidad) as total_cantidad, | |
SUM(cantidad * costo) as ingreso_total | |
FROM pedidos | |
GROUP BY producto | |
ORDER BY total_cantidad DESC | |
LIMIT 10 | |
""") | |
productos_top = cursor.fetchall() | |
resultado['productos_top'] = productos_top | |
# 3. Calcular ganancia (asumiendo que tenemos datos de costo y precio de venta) | |
cursor.execute(""" | |
SELECT SUM(p.cantidad * (pr.precio_venta - pr.costo)) as ganancia_total | |
FROM pedidos p | |
JOIN productos pr ON p.producto = pr.nombre | |
""") | |
ganancia_result = cursor.fetchone() | |
ganancia = ganancia_result[0] if ganancia_result and ganancia_result[0] is not None else 0 | |
# 4. Calcular facturación total | |
cursor.execute(""" | |
SELECT SUM(cantidad * costo) as total_facturado | |
FROM pedidos | |
""") | |
facturacion_result = cursor.fetchone() | |
facturacion = facturacion_result[0] if facturacion_result and facturacion_result[0] is not None else 0 | |
# Datos para resumen | |
resultado['resumen'] = (facturacion, ganancia, productos_top) | |
# 5. Datos para predicciones - Necesitamos histórico por fecha | |
cursor.execute(""" | |
SELECT DATE(fecha) as dia, SUM(cantidad * costo) as total | |
FROM pedidos | |
GROUP BY dia | |
ORDER BY dia ASC | |
""") | |
datos_historicos_raw = cursor.fetchall() | |
# Convertir las fechas a objetos datetime | |
datos_historicos = [] | |
for fecha_str, total in datos_historicos_raw: | |
try: | |
fecha_obj = datetime.strptime(fecha_str, '%Y-%m-%d') | |
datos_historicos.append((fecha_obj, total)) | |
except: | |
# Si hay error con la fecha, usar la string original (menos ideal) | |
datos_historicos.append((fecha_str, total)) | |
# Si tenemos suficientes datos, hacer predicciones | |
if len(datos_historicos) >= 5: # Necesitamos al menos 5 puntos para una regresión simple | |
predicciones = self.calcular_predicciones(datos_historicos) | |
resultado['predicciones'] = predicciones | |
cursor.close() | |
conn.close() | |
except Exception as e: | |
print(f"Error obteniendo datos de estadísticas: {str(e)}") | |
import traceback | |
traceback.print_exc() | |
return resultado | |
def calcular_predicciones(self, datos_historicos): | |
""" | |
Implementa una regresión lineal simple para predecir ventas futuras | |
basándose en los datos históricos. | |
""" | |
try: | |
import numpy as np | |
from datetime import datetime, timedelta | |
# Convertir fechas a números (días desde el primer registro) | |
fecha_base = datos_historicos[0][0] # Primera fecha como referencia | |
# Verificar si fecha_base es un objeto datetime | |
if not hasattr(fecha_base, 'days'): | |
# Si no es datetime, intentar convertirlo | |
try: | |
from datetime import datetime | |
if isinstance(fecha_base, str): | |
fecha_base = datetime.strptime(fecha_base, '%Y-%m-%d') | |
except: | |
# Si no podemos convertir, no podemos hacer predicciones | |
return [] | |
# Asegurar que todas las fechas son datetime | |
valid_data = [] | |
for fecha, venta in datos_historicos: | |
if not hasattr(fecha, 'days'): | |
try: | |
if isinstance(fecha, str): | |
fecha = datetime.strptime(fecha, '%Y-%m-%d') | |
valid_data.append((fecha, venta)) | |
except: | |
# Omitir datos que no podemos convertir | |
continue | |
else: | |
valid_data.append((fecha, venta)) | |
if not valid_data: | |
return [] | |
x = [(fecha - fecha_base).days for fecha, _ in valid_data] | |
y = [venta for _, venta in valid_data] | |
# Si no hay suficiente variación en los datos, no podemos predecir | |
if len(set(y)) <= 1: | |
return [] | |
# Convertir a arrays de numpy | |
x = np.array(x).reshape(-1, 1) | |
y = np.array(y) | |
# Calcular la regresión lineal (y = mx + b) | |
n = len(x) | |
m = (n * np.sum(x * y) - np.sum(x) * np.sum(y)) / (n * np.sum(x * x) - np.sum(x) ** 2) | |
b = (np.sum(y) - m * np.sum(x)) / n | |
# Predecir para los próximos 7 días | |
ultima_fecha = valid_data[-1][0] | |
predicciones = [] | |
for i in range(1, 8): | |
fecha_prediccion = ultima_fecha + timedelta(days=i) | |
dias_desde_base = (fecha_prediccion - fecha_base).days | |
valor_predicho = m * dias_desde_base + b | |
# Asegurar que las predicciones no sean negativas | |
valor_predicho = max(0, valor_predicho) | |
predicciones.append((fecha_prediccion, valor_predicho)) | |
return predicciones | |
except Exception as e: | |
print(f"Error en el cálculo de predicciones: {str(e)}") | |
import traceback | |
traceback.print_exc() | |
return [] | |
def exportar_estadisticas_pdf(self, instance): | |
"""Exporta las estadísticas actuales a un PDF usando FPDF""" | |
try: | |
# Obtener datos para el PDF | |
datos = self.obtener_datos_estadisticas() | |
# Crear nombre de archivo con timestamp | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
app_dir = get_app_dir() | |
reportes_dir = os.path.join(app_dir, "reportes") | |
os.makedirs(reportes_dir, exist_ok=True) | |
filename = os.path.join(reportes_dir, f"Estadisticas_{timestamp}.pdf") | |
# Crear PDF con FPDF | |
pdf = FPDF() | |
pdf.add_page() | |
# Configurar márgenes y fuentes | |
pdf.set_margins(10, 10, 10) | |
pdf.set_auto_page_break(True, margin=15) | |
# Título | |
pdf.set_font('Arial', 'B', 16) | |
pdf.cell(0, 10, "Reporte de Estadísticas", 0, 1, 'C') | |
pdf.ln(5) | |
# Fecha del reporte | |
pdf.set_font('Arial', '', 12) | |
pdf.cell(0, 10, f"Generado el: {datetime.now().strftime('%d/%m/%Y %H:%M')}", 0, 1, 'L') | |
pdf.ln(5) | |
# 1. Resumen de ventas | |
pdf.set_font('Arial', 'B', 14) | |
pdf.cell(0, 10, "Resumen de Ventas", 0, 1, 'L') | |
pdf.ln(2) | |
total_ventas, ganancia_total, _ = datos['resumen'] | |
# Verificar que la facturación no sea cero para evitar división por cero | |
rentabilidad = 0 | |
if total_ventas > 0: | |
rentabilidad = (ganancia_total/total_ventas*100) | |
# Tabla de resumen | |
pdf.set_fill_color(150, 150, 150) | |
pdf.set_text_color(255, 255, 255) | |
pdf.set_font('Arial', 'B', 10) | |
col_widths = [60, 60, 60] | |
pdf.cell(col_widths[0], 10, "Total Facturado", 1, 0, 'C', 1) | |
pdf.cell(col_widths[1], 10, "Ganancia Total", 1, 0, 'C', 1) | |
pdf.cell(col_widths[2], 10, "Rentabilidad", 1, 1, 'C', 1) | |
pdf.set_fill_color(255, 255, 255) | |
pdf.set_text_color(0, 0, 0) | |
pdf.set_font('Arial', '', 10) | |
pdf.cell(col_widths[0], 10, f"${total_ventas:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[1], 10, f"${ganancia_total:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[2], 10, f"{rentabilidad:.1f}%", 1, 1, 'C') | |
pdf.ln(10) | |
# 2. Productos más vendidos | |
pdf.set_font('Arial', 'B', 14) | |
pdf.cell(0, 10, "Productos Más Vendidos", 0, 1, 'L') | |
pdf.ln(2) | |
# Tabla de productos más vendidos | |
pdf.set_fill_color(150, 150, 150) | |
pdf.set_text_color(255, 255, 255) | |
pdf.set_font('Arial', 'B', 10) | |
col_widths = [100, 40, 40] | |
pdf.cell(col_widths[0], 10, "Producto", 1, 0, 'C', 1) | |
pdf.cell(col_widths[1], 10, "Cantidad", 1, 0, 'C', 1) | |
pdf.cell(col_widths[2], 10, "Ingresos", 1, 1, 'C', 1) | |
pdf.set_fill_color(255, 255, 255) | |
pdf.set_text_color(0, 0, 0) | |
pdf.set_font('Arial', '', 10) | |
for producto_info in datos['productos_top'][:5]: # Top 5 | |
if len(producto_info) >= 3: | |
producto, cantidad, ingreso = producto_info | |
pdf.cell(col_widths[0], 10, producto, 1, 0, 'L') | |
pdf.cell(col_widths[1], 10, str(int(cantidad)), 1, 0, 'C') | |
pdf.cell(col_widths[2], 10, f"${ingreso:.2f}", 1, 1, 'C') | |
pdf.ln(10) | |
# 3. Ventas por día | |
pdf.set_font('Arial', 'B', 14) | |
pdf.cell(0, 10, "Ventas de los Últimos Días", 0, 1, 'L') | |
pdf.ln(2) | |
# Tabla de ventas por día | |
pdf.set_fill_color(150, 150, 150) | |
pdf.set_text_color(255, 255, 255) | |
pdf.set_font('Arial', 'B', 10) | |
col_widths = [80, 80] | |
pdf.cell(col_widths[0], 10, "Fecha", 1, 0, 'C', 1) | |
pdf.cell(col_widths[1], 10, "Total", 1, 1, 'C', 1) | |
pdf.set_fill_color(255, 255, 255) | |
pdf.set_text_color(0, 0, 0) | |
pdf.set_font('Arial', '', 10) | |
for dia_info in datos.get('ventas_dias', [])[:7]: # Últimos 7 días | |
if len(dia_info) >= 2: | |
fecha, total = dia_info | |
# Verificar si fecha es un objeto datetime o string | |
fecha_str = fecha | |
if hasattr(fecha, 'strftime'): | |
fecha_str = fecha.strftime('%d/%m/%Y') | |
else: | |
# Si es otro formato, intentar convertir o usar como está | |
try: | |
if isinstance(fecha, str) and len(fecha) >= 10: | |
fecha_str = datetime.strptime(fecha[:10], '%Y-%m-%d').strftime('%d/%m/%Y') | |
except: | |
pass | |
pdf.cell(col_widths[0], 10, str(fecha_str), 1, 0, 'C') | |
pdf.cell(col_widths[1], 10, f"${total:.2f}", 1, 1, 'C') | |
pdf.ln(10) | |
# 4. Predicciones (si hay disponibles) | |
if 'predicciones' in datos and datos['predicciones']: | |
pdf.set_font('Arial', 'B', 14) | |
pdf.cell(0, 10, "Predicciones de Ventas (BETA)", 0, 1, 'L') | |
pdf.ln(2) | |
# Nota sobre las predicciones | |
pdf.set_font('Arial', 'I', 10) | |
pdf.multi_cell(0, 10, "Nota: Las predicciones se basan en un modelo simple de regresión lineal y deben considerarse como estimaciones aproximadas.", 0, 'L') | |
pdf.ln(5) | |
# Tabla de predicciones | |
pdf.set_fill_color(150, 150, 150) | |
pdf.set_text_color(255, 255, 255) | |
pdf.set_font('Arial', 'B', 10) | |
col_widths = [80, 80] | |
pdf.cell(col_widths[0], 10, "Fecha", 1, 0, 'C', 1) | |
pdf.cell(col_widths[1], 10, "Venta Esperada", 1, 1, 'C', 1) | |
pdf.set_fill_color(255, 255, 255) | |
pdf.set_text_color(0, 0, 0) | |
pdf.set_font('Arial', '', 10) | |
for pred_info in datos['predicciones']: | |
if len(pred_info) >= 2: | |
fecha, valor = pred_info | |
# Verificar si fecha es un objeto datetime | |
fecha_str = fecha | |
if hasattr(fecha, 'strftime'): | |
fecha_str = fecha.strftime('%d/%m/%Y') | |
pdf.cell(col_widths[0], 10, str(fecha_str), 1, 0, 'C') | |
pdf.cell(col_widths[1], 10, f"${valor:.2f}", 1, 1, 'C') | |
# Guardar PDF | |
pdf.output(filename) | |
mostrar_notificacion(f"✅ Reporte exportado exitosamente: {filename}") | |
except Exception as e: | |
import traceback | |
error_detalle = traceback.format_exc() | |
mostrar_notificacion(f"❌ Error al exportar estadísticas: {str(e)}") | |
print(error_detalle) | |
def registrar_pedido(self, instance): | |
cliente = self.cliente.text.strip() | |
producto = self.producto.text.strip() | |
cantidad = self.cantidad.text.strip() | |
costo = self.costo.text.strip() | |
zona = self.zona.text.strip() | |
if not all([cliente, producto, cantidad, costo]) or zona == "📍 Zona": | |
mostrar_notificacion("⚠️ Todos los campos son obligatorios.") | |
return | |
# Verificar stock disponible | |
stock_actual = obtener_stock_producto(producto) | |
if stock_actual < int(cantidad): | |
mostrar_notificacion(f"⚠️ Stock insuficiente. Disponible: {stock_actual}") | |
return | |
# Insertar cliente si no existe | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
try: | |
cursor.execute("INSERT OR IGNORE INTO clientes (nombre) VALUES (?)", (cliente,)) | |
conn.commit() | |
except: | |
pass | |
cursor.close() | |
conn.close() | |
# Agregar a lista temporal | |
self.productos_temporal.append({ | |
'producto': producto, | |
'cantidad': int(cantidad), | |
'costo': float(costo), | |
'zona': zona, | |
'stock': stock_actual | |
}) | |
self.actualizar_lista_temporal() | |
# Limpiar campos | |
self.producto.text = '' | |
self.cantidad.text = '' | |
self.costo.text = '' | |
def sugerir_clientes(self, instance, texto): | |
if texto.strip(): | |
clientes = obtener_clientes(texto.strip()) | |
if clientes: | |
crear_menu_sugerencias(self, clientes, self.cliente) | |
def sugerir_productos(self, instance, texto): | |
if texto.strip(): | |
productos = obtener_productos(texto.strip()) | |
if productos: | |
crear_menu_sugerencias(self, productos, self.producto) | |
def seleccionar_sugerencia(self, texto, campo): | |
campo.text = texto | |
if self.menu: | |
self.menu.dismiss() | |
if campo == self.producto: | |
self.costo.text = str(obtener_costo_producto(texto)) | |
def mostrar_zonas(self, instance): | |
zonas = ["Bernal", "Avellaneda #1", "Avellaneda #2", "Quilmes Centro", "Solano"] | |
if platform == 'android': | |
content = MDBoxLayout( | |
orientation='vertical', | |
size_hint_y=None, | |
height=dp(44 * len(zonas)), | |
spacing=dp(4) | |
) | |
for zona in zonas: | |
btn = MDRaisedButton( | |
text=zona, | |
size_hint=(1, None), | |
height=dp(44), | |
font_size=sp(16), | |
on_release=lambda x, z=zona: self.seleccionar_zona_dialogo(z, dialog) | |
) | |
content.add_widget(btn) | |
dialog = MDDialog( | |
title="Selecciona una zona", | |
type="custom", | |
content_cls=content, | |
size_hint=(0.9, None) | |
) | |
dialog.open() | |
else: | |
menu_items = [ | |
{"text": zona, "viewclass": "OneLineListItem", "on_release": partial(self.seleccionar_zona, zona)} | |
for zona in zonas | |
] | |
if hasattr(self, "menu_zonas") and self.menu_zonas: | |
self.menu_zonas.dismiss() | |
self.menu_zonas = MDDropdownMenu(caller=self.zona, items=menu_items, width_mult=4) | |
self.menu_zonas.open() | |
def seleccionar_zona_dialogo(self, zona, dialog): | |
"""Selecciona una zona desde el diálogo y lo cierra""" | |
self.zona.text = zona | |
dialog.dismiss() | |
def seleccionar_zona(self, zona, *args): | |
self.zona.text = zona | |
if hasattr(self, "menu_zonas") and self.menu_zonas: | |
self.menu_zonas.dismiss() | |
def ver_pedidos_dia(self, instance): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT DISTINCT cliente FROM pedidos WHERE fecha = DATE('now')") | |
clientes = [cliente[0] for cliente in cursor.fetchall()] | |
cursor.close() | |
conn.close() | |
if not clientes: | |
mostrar_notificacion("📄 No hay pedidos registrados hoy.") | |
return | |
content =content = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, size_hint_y=None) | |
content.height = (len(clientes) + 1) * TOUCH_BUTTON_HEIGHT # +1 para el botón de descarga | |
for cliente in clientes: | |
btn = MDRaisedButton( | |
text=cliente, | |
on_release=partial(self.mostrar_detalle_cliente, cliente), | |
size_hint=(1, None), | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
content.add_widget(btn) | |
btn_descargar = MDRaisedButton( | |
text="Descargar PDF para cada cliente", | |
on_release=lambda x: self.generar_pdf_todos_clientes(clientes), | |
size_hint=(1, None), | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
content.add_widget(btn_descargar) | |
MDDialog( | |
title="Pedidos del día", | |
type="custom", | |
content_cls=content, | |
size_hint=(0.9, None) | |
).open() | |
def generar_pdf_todos_clientes(self, clientes): | |
"""Genera un PDF individual para cada cliente con sus pedidos del día""" | |
try: | |
# Crear directorio para guardar los PDFs si no existe | |
app_dir = get_app_dir() | |
directorio = os.path.join(app_dir, "pedidos_clientes") | |
if not os.path.exists(directorio): | |
os.makedirs(directorio) | |
# Contador de PDFs generados | |
pdfs_generados = 0 | |
# Generar un PDF para cada cliente | |
for cliente in clientes: | |
# Obtener los pedidos del cliente | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute(""" | |
SELECT producto, cantidad, costo, zona | |
FROM pedidos | |
WHERE cliente = ? AND fecha = DATE('now') | |
""", (cliente,)) | |
pedidos = cursor.fetchall() | |
cursor.close() | |
conn.close() | |
if not pedidos: | |
continue | |
# Generar nombre de archivo único para cada cliente | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
filename = os.path.join(directorio, f"Pedido_{cliente}_{timestamp}.pdf") | |
# Crear el PDF con FPDF en lugar de ReportLab | |
pdf = FPDF() | |
pdf.add_page() | |
# Configurar márgenes y fuentes | |
pdf.set_margins(10, 10, 10) | |
pdf.set_auto_page_break(True, margin=15) | |
# Encabezado | |
pdf.set_font('Arial', 'B', 16) | |
pdf.cell(0, 10, f"Pedido de {cliente} - {datetime.now().strftime('%d/%m/%Y')}", 0, 1, 'C') | |
pdf.ln(5) | |
# Tabla de productos | |
pdf.set_fill_color(200, 200, 200) | |
pdf.set_font('Arial', 'B', 10) | |
# Encabezados | |
col_widths = [60, 25, 30, 30, 35] | |
pdf.cell(col_widths[0], 10, "Producto", 1, 0, 'C', 1) | |
pdf.cell(col_widths[1], 10, "Cantidad", 1, 0, 'C', 1) | |
pdf.cell(col_widths[2], 10, "Costo Unit.", 1, 0, 'C', 1) | |
pdf.cell(col_widths[3], 10, "Total", 1, 0, 'C', 1) | |
pdf.cell(col_widths[4], 10, "Zona", 1, 1, 'C', 1) | |
# Datos | |
pdf.set_font('Arial', '', 10) | |
total_cliente = 0 | |
for producto, cantidad, costo, zona in pedidos: | |
total = cantidad * costo | |
total_cliente += total | |
pdf.cell(col_widths[0], 10, producto, 1, 0, 'L') | |
pdf.cell(col_widths[1], 10, str(cantidad), 1, 0, 'C') | |
pdf.cell(col_widths[2], 10, f"${costo:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[3], 10, f"${total:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[4], 10, zona, 1, 1, 'C') | |
# Fila de total | |
pdf.set_font('Arial', 'B', 10) | |
pdf.cell(col_widths[0], 10, "TOTAL", 1, 0, 'R') | |
pdf.cell(col_widths[1] + col_widths[2], 10, "", 1, 0, 'C') | |
pdf.cell(col_widths[3], 10, f"${total_cliente:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[4], 10, "", 1, 1, 'C') | |
# Guardar PDF | |
pdf.output(filename) | |
pdfs_generados += 1 | |
# Mostrar notificación con el resultado | |
if pdfs_generados > 0: | |
mostrar_notificacion(f"✅ Se generaron {pdfs_generados} archivos PDF en la carpeta '{directorio}'") | |
else: | |
mostrar_notificacion("⚠️ No se generó ningún PDF. No hay pedidos para procesar.") | |
except Exception as e: | |
import traceback | |
error_detalle = traceback.format_exc() | |
mostrar_notificacion(f"❌ Error al generar PDFs: {str(e)}") | |
def mostrar_dialogo_simple(self, titulo, texto): | |
"""Muestra un diálogo simple con un botón para cerrar.""" | |
content = MDBoxLayout( | |
orientation='vertical', | |
spacing=SPACING_TOUCH, | |
padding=SPACING_TOUCH, | |
size_hint_y=None, | |
height=dp(120) | |
) | |
# Añadir texto con scroll para mensajes largos | |
scroll = MDScrollView(size_hint=(1, 1)) | |
label = MDLabel( | |
text=texto, | |
size_hint_y=None, | |
font_size=sp(16), | |
halign="left" | |
) | |
label.bind(texture_size=label.setter('size')) | |
scroll.add_widget(label) | |
content.add_widget(scroll) | |
# Crear el diálogo | |
self.dialog = MDDialog( | |
title=titulo, | |
type="custom", | |
content_cls=content, | |
buttons=[ | |
MDFlatButton( | |
text="Cerrar", | |
on_release=lambda x: self.dialog.dismiss(), | |
font_size=sp(16) | |
), | |
], | |
size_hint=(0.9, None) | |
) | |
self.dialog.open() | |
def generar_productos_por_dia(self, *args): | |
"""Genera un PDF con los productos vendidos en el día actual, acumulando cantidades.""" | |
try: | |
# Obtener la fecha actual | |
fecha_actual = datetime.now().strftime("%Y-%m-%d") | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
# Consulta SQL para obtener productos vendidos hoy agrupados y sumados | |
consulta = """ | |
SELECT producto, SUM(cantidad) as cantidad_total | |
FROM pedidos | |
WHERE fecha = DATE('now') | |
GROUP BY producto | |
ORDER BY producto | |
""" | |
cursor.execute(consulta) | |
resultados = cursor.fetchall() | |
# Cerrar la conexión a la base de datos | |
conn.close() | |
# Verificar si hay resultados | |
if not resultados: | |
self.mostrar_dialogo_simple("Información", "No hay productos vendidos registrados para hoy.") | |
return | |
# Crear el PDF con FPDF | |
pdf = FPDF() | |
pdf.add_page() | |
# Configurar márgenes y fuentes | |
pdf.set_margins(10, 10, 10) | |
pdf.set_auto_page_break(True, margin=15) | |
# Título | |
pdf.set_font('Arial', 'B', 16) | |
pdf.cell(0, 10, 'Reporte de Productos por Día', 0, 1, 'C') | |
# Fecha | |
pdf.set_font('Arial', 'B', 12) | |
pdf.cell(0, 10, f'Fecha: {fecha_actual}', 0, 1, 'L') | |
pdf.ln(5) | |
# Crear la tabla | |
# Encabezados | |
pdf.set_fill_color(200, 200, 200) | |
pdf.set_font('Arial', 'B', 11) | |
# Definir anchos de columnas | |
ancho_producto = 120 | |
ancho_cantidad = 60 | |
altura_celda = 10 | |
# Dibujar encabezados | |
pdf.cell(ancho_producto, altura_celda, 'Producto', 1, 0, 'C', 1) | |
pdf.cell(ancho_cantidad, altura_celda, 'Cantidad', 1, 1, 'C', 1) | |
# Dibujar filas de datos | |
pdf.set_font('Arial', '', 10) | |
for producto, cantidad in resultados: | |
pdf.cell(ancho_producto, altura_celda, producto, 1, 0, 'L') | |
pdf.cell(ancho_cantidad, altura_celda, str(cantidad), 1, 1, 'C') | |
# Crear directorio para reportes si no existe | |
app_dir = get_app_dir() | |
directorio = os.path.join(app_dir, "reportes") | |
if not os.path.exists(directorio): | |
os.makedirs(directorio) | |
# Generar nombre de archivo único | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
nombre_archivo = os.path.join(directorio, f"productos_por_dia_{timestamp}.pdf") | |
# Guardar el PDF | |
pdf.output(nombre_archivo) | |
# Mostrar mensaje de éxito | |
self.mostrar_dialogo_simple("Éxito", f"Reporte generado exitosamente.\nArchivo: {nombre_archivo}") | |
except Exception as e: | |
# Mostrar mensaje de error con detalles para ayudar en la depuración | |
import traceback | |
error_detalle = traceback.format_exc() | |
self.mostrar_dialogo_simple("Error", f"No se pudo generar el reporte: {str(e)}\n\nDetalles: {error_detalle}") | |
def mostrar_detalle_cliente(self, cliente, instance): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute(""" | |
SELECT producto, cantidad, costo, zona | |
FROM pedidos | |
WHERE cliente = ? AND fecha = DATE('now') | |
""", (cliente,)) | |
pedidos = cursor.fetchall() | |
cursor.close() | |
conn.close() | |
if not pedidos: | |
mostrar_notificacion(f"No hay pedidos para {cliente} en la fecha actual") | |
return | |
content = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, size_hint_y=None) | |
content.height = len(pedidos) * TOUCH_ITEM_HEIGHT + dp(20) | |
for producto, cantidad, costo, zona in pedidos: | |
total = cantidad * costo | |
item = TwoLineListItem( | |
text=f"{producto} - {cantidad} unidades", | |
secondary_text=f"Costo: ${costo:.2f} | Total: ${total:.2f} | Zona: {zona}", | |
font_style="Subtitle1" | |
) | |
content.add_widget(item) | |
# Crear el diálogo | |
dialog = MDDialog( | |
title=f"Detalle de pedidos de {cliente}", | |
type="custom", | |
content_cls=content, | |
buttons=[ | |
MDFlatButton( | |
text="Cerrar", | |
on_release=lambda x: dialog.dismiss(), | |
font_size=sp(16) | |
), | |
MDRaisedButton( | |
text="PDF", | |
on_release=lambda x: self.generar_pdf_cliente(cliente), | |
font_size=sp(16) | |
) | |
], | |
size_hint=(0.9, None) | |
) | |
dialog.open() | |
def generar_pdf_cliente(self, cliente): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute(""" | |
SELECT producto, cantidad, costo, zona | |
FROM pedidos | |
WHERE cliente = ? AND fecha = DATE('now') | |
""", (cliente,)) | |
pedidos = cursor.fetchall() | |
cursor.close() | |
conn.close() | |
app_dir = get_app_dir() | |
filename = os.path.join(app_dir, f"Pedido_{cliente}_{datetime.now().strftime('%Y-%m-%d')}.pdf") | |
# Crear el PDF con FPDF en lugar de ReportLab | |
pdf = FPDF() | |
pdf.add_page() | |
# Configurar márgenes y fuentes | |
pdf.set_margins(10, 10, 10) | |
pdf.set_auto_page_break(True, margin=15) | |
# Encabezado | |
pdf.set_font('Arial', 'B', 16) | |
pdf.cell(0, 10, f"Pedido de {cliente} - {datetime.now().strftime('%d/%m/%Y')}", 0, 1, 'C') | |
pdf.ln(5) | |
# Tabla de productos | |
pdf.set_fill_color(200, 200, 200) | |
pdf.set_font('Arial', 'B', 10) | |
# Encabezados | |
col_widths = [60, 25, 30, 30, 35] | |
pdf.cell(col_widths[0], 10, "Producto", 1, 0, 'C', 1) | |
pdf.cell(col_widths[1], 10, "Cantidad", 1, 0, 'C', 1) | |
pdf.cell(col_widths[2], 10, "Costo Unit.", 1, 0, 'C', 1) | |
pdf.cell(col_widths[3], 10, "Total", 1, 0, 'C', 1) | |
pdf.cell(col_widths[4], 10, "Zona", 1, 1, 'C', 1) | |
# Datos | |
pdf.set_font('Arial', '', 10) | |
total_cliente = 0 | |
for producto, cantidad, costo, zona in pedidos: | |
total = cantidad * costo | |
total_cliente += total | |
pdf.cell(col_widths[0], 10, producto, 1, 0, 'L') | |
pdf.cell(col_widths[1], 10, str(cantidad), 1, 0, 'C') | |
pdf.cell(col_widths[2], 10, f"${costo:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[3], 10, f"${total:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[4], 10, zona, 1, 1, 'C') | |
# Fila de total | |
pdf.set_font('Arial', 'B', 10) | |
pdf.cell(col_widths[0], 10, "TOTAL", 1, 0, 'R') | |
pdf.cell(col_widths[1] + col_widths[2], 10, "", 1, 0, 'C') | |
pdf.cell(col_widths[3], 10, f"${total_cliente:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[4], 10, "", 1, 1, 'C') | |
# Guardar PDF | |
pdf.output(filename) | |
mostrar_notificacion(f"✅ PDF generado: {filename}") | |
def generar_pdf_pedidos(self): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute(""" | |
SELECT cliente, producto, SUM(cantidad) as cantidad_total, AVG(costo) as costo_prom, zona | |
FROM pedidos | |
WHERE fecha = DATE('now') | |
GROUP BY cliente, producto, zona | |
""") | |
pedidos = cursor.fetchall() | |
cursor.close() | |
conn.close() | |
app_dir = get_app_dir() | |
filename = os.path.join(app_dir, f"Resumen_Pedidos_{datetime.now().strftime('%Y-%m-%d')}.pdf") | |
# Crear el PDF con FPDF en lugar de ReportLab | |
pdf = FPDF() | |
pdf.add_page() | |
# Configurar márgenes y fuentes | |
pdf.set_margins(10, 10, 10) | |
pdf.set_auto_page_break(True, margin=15) | |
# Encabezado | |
pdf.set_font('Arial', 'B', 16) | |
pdf.cell(0, 10, f"Resumen Diario de Pedidos - {datetime.now().strftime('%d/%m/%Y')}", 0, 1, 'C') | |
pdf.ln(5) | |
# Tabla de productos | |
pdf.set_fill_color(50, 80, 180) # Color azul similar a HexColor("#3B5998") | |
pdf.set_text_color(255, 255, 255) # Texto blanco | |
pdf.set_font('Arial', 'B', 8) | |
# Encabezados | |
col_widths = [50, 45, 20, 25, 25, 25] | |
pdf.cell(col_widths[0], 10, "Cliente", 1, 0, 'C', 1) | |
pdf.cell(col_widths[1], 10, "Producto", 1, 0, 'C', 1) | |
pdf.cell(col_widths[2], 10, "Cantidad", 1, 0, 'C', 1) | |
pdf.cell(col_widths[3], 10, "Costo Prom.", 1, 0, 'C', 1) | |
pdf.cell(col_widths[4], 10, "Zona", 1, 0, 'C', 1) | |
pdf.cell(col_widths[5], 10, "Total", 1, 1, 'C', 1) | |
# Datos | |
pdf.set_text_color(0, 0, 0) # Regresar a texto negro | |
pdf.set_font('Arial', '', 8) | |
total_general = 0 | |
for cliente, producto, cantidad, costo, zona in pedidos: | |
total = cantidad * costo | |
total_general += total | |
pdf.cell(col_widths[0], 8, cliente, 1, 0, 'L') | |
pdf.cell(col_widths[1], 8, producto, 1, 0, 'L') | |
pdf.cell(col_widths[2], 8, str(int(cantidad)), 1, 0, 'C') | |
pdf.cell(col_widths[3], 8, f"${costo:.2f}", 1, 0, 'C') | |
pdf.cell(col_widths[4], 8, zona, 1, 0, 'C') | |
pdf.cell(col_widths[5], 8, f"${total:.2f}", 1, 1, 'C') | |
# Fila de total general | |
pdf.set_font('Arial', 'B', 8) | |
pdf.cell(col_widths[0] + col_widths[1] + col_widths[2] + col_widths[3] + col_widths[4], 10, "TOTAL GENERAL", 1, 0, 'R') | |
pdf.cell(col_widths[5], 10, f"${total_general:.2f}", 1, 1, 'C') | |
# Guardar PDF | |
pdf.output(filename) | |
mostrar_notificacion(f"✅ PDF generado: {filename}") | |
def mostrar_clientes_para_editar(self, instance): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT DISTINCT cliente FROM pedidos WHERE fecha = DATE('now')") | |
clientes = [cliente[0] for cliente in cursor.fetchall()] | |
cursor.close() | |
conn.close() | |
if not clientes: | |
mostrar_notificacion("🙈 No hay pedidos registrados hoy.") | |
return | |
content = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, size_hint_y=None) | |
content.height = len(clientes) * TOUCH_BUTTON_HEIGHT | |
for cliente in clientes: | |
btn = MDRaisedButton( | |
text=cliente, | |
on_release=lambda x, c=cliente: self.abrir_edicion_cliente(c), | |
size_hint=(1, None), | |
height=TOUCH_BUTTON_HEIGHT, | |
font_size=sp(16) | |
) | |
content.add_widget(btn) | |
self.dialog_seleccion_cliente = MDDialog( | |
title="Seleccione un cliente para editar", | |
type="custom", | |
content_cls=content, | |
buttons=[ | |
MDFlatButton( | |
text="Cancelar", | |
on_release=lambda x: self.dialog_seleccion_cliente.dismiss(), | |
font_size=sp(16) | |
) | |
], | |
size_hint=(0.9, None) | |
) | |
self.dialog_seleccion_cliente.open() | |
def abrir_edicion_cliente(self, cliente): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("SELECT id, producto, cantidad, costo, zona FROM pedidos WHERE cliente = ? AND fecha = DATE('now')", (cliente,)) | |
pedidos = cursor.fetchall() | |
cursor.close() | |
conn.close() | |
if hasattr(self, 'dialog_seleccion_cliente') and self.dialog_seleccion_cliente: | |
self.dialog_seleccion_cliente.dismiss() | |
# Crear un BoxLayout con scroll para admitir muchos elementos | |
scroll_container = MDScrollView(size_hint=(1, None), height=dp(400)) | |
main_container = MDBoxLayout(orientation='vertical', spacing=SPACING_TOUCH, size_hint_y=None) | |
main_container.bind(minimum_height=main_container.setter('height')) | |
# Añadir un espacio vacío en la parte superior | |
spacer = MDBoxLayout(size_hint_y=None, height=dp(20)) | |
main_container.add_widget(spacer) | |
self.edicion_pedidos = [] | |
self.cliente_actual_edicion = cliente | |
for pedido in pedidos: | |
id_pedido, producto, cantidad, costo, zona = pedido | |
# Contenedor principal para el ítem | |
item = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(140), padding=(5, 10, 5, 5)) | |
# Información del producto y zona | |
info_container = MDBoxLayout(orientation='vertical', size_hint_y=None, height=dp(50)) | |
producto_label = TwoLineListItem( | |
text=producto, | |
secondary_text=f"Zona: {zona}", | |
divider=None, | |
_no_ripple_effect=True, | |
font_style="Subtitle1" | |
) | |
info_container.add_widget(producto_label) | |
item.add_widget(info_container) | |
# Campos de edición en layout horizontal | |
campos_container = MDBoxLayout(orientation='horizontal', size_hint_y=None, height=dp(56), spacing=SPACING_TOUCH) | |
# Campo de cantidad | |
cantidad_container = MDBoxLayout(orientation='vertical', size_hint_x=0.5, spacing=2) | |
cantidad_label = MDLabel( | |
text="Cantidad", | |
size_hint_y=None, | |
height=dp(20), | |
font_style="Caption" | |
) | |
cantidad_container.add_widget(cantidad_label) | |
cantidad_input = MDTextField( | |
text=str(cantidad), | |
input_filter='int', | |
size_hint_y=None, | |
height=dp(48), | |
mode="rectangle", | |
halign="center", | |
font_size=sp(16) | |
) | |
cantidad_container.add_widget(cantidad_input) | |
campos_container.add_widget(cantidad_container) | |
# Campo de costo | |
costo_container = MDBoxLayout(orientation='vertical', size_hint_x=0.5, spacing=2) | |
costo_label = MDLabel( | |
text="Costo", | |
size_hint_y=None, | |
height=dp(20), | |
font_style="Caption" | |
) | |
costo_container.add_widget(costo_label) | |
costo_input = MDTextField( | |
text=str(costo), | |
input_filter='float', | |
size_hint_y=None, | |
height=dp(48), | |
mode="rectangle", | |
halign="center", | |
font_size=sp(16) | |
) | |
costo_container.add_widget(costo_input) | |
campos_container.add_widget(costo_container) | |
item.add_widget(campos_container) | |
# Botón de eliminar | |
btn_container = MDBoxLayout(size_hint_y=None, height=dp(56), padding=(0, 8, 0, 0)) | |
btn_eliminar = MDRaisedButton( | |
text="Eliminar pedido", | |
theme_text_color="Custom", | |
text_color=(1, 1, 1, 1), | |
md_bg_color=(0.8, 0.2, 0.2, 1), | |
on_release=partial(self.eliminar_pedido, id_pedido), | |
size_hint_x=1, | |
height=dp(48), | |
font_size=sp(16) | |
) | |
btn_container.add_widget(btn_eliminar) | |
item.add_widget(btn_container) | |
# Añadir un separador visual entre elementos | |
separador = MDBoxLayout( | |
size_hint_y=None, | |
height=dp(1), | |
md_bg_color=(0.7, 0.7, 0.7, 1) | |
) | |
main_container.add_widget(item) | |
main_container.add_widget(separador) | |
self.edicion_pedidos.append((id_pedido, cantidad_input, costo_input)) | |
scroll_container.add_widget(main_container) | |
self.dialog_edicion = MDDialog( | |
title=f"Editar pedidos de {cliente}", | |
type="custom", | |
content_cls=scroll_container, | |
buttons=[ | |
MDRaisedButton( | |
text="Guardar", | |
on_release=self.guardar_cambios, | |
font_size=sp(16) | |
), | |
MDFlatButton( | |
text="Cancelar", | |
on_release=lambda x: self.dialog_edicion.dismiss(), | |
font_size=sp(16) | |
) | |
], | |
size_hint=(0.9, None), | |
height=dp(500) | |
) | |
self.dialog_edicion.open() | |
def eliminar_pedido(self, id_pedido, instance): | |
"""Muestra un diálogo de confirmación antes de eliminar un pedido""" | |
try: | |
# Crear el diálogo de confirmación | |
self.dialog_confirmacion = MDDialog( | |
title="Confirmar eliminación", | |
text="¿Estás seguro de que deseas eliminar este pedido? Esta acción no se puede deshacer.", | |
buttons=[ | |
MDFlatButton( | |
text="Cancelar", | |
on_release=lambda x: self.dialog_confirmacion.dismiss(), | |
font_size=sp(16) | |
), | |
MDRaisedButton( | |
text="Eliminar", | |
theme_text_color="Custom", | |
text_color=(1, 1, 1, 1), | |
md_bg_color=(0.8, 0.2, 0.2, 1), | |
on_release=lambda x: self.confirmar_eliminacion(id_pedido), | |
font_size=sp(16) | |
), | |
] | |
) | |
# Mostrar el diálogo | |
self.dialog_confirmacion.open() | |
except Exception as e: | |
mostrar_notificacion(f"❌ Error al intentar eliminar: {str(e)}") | |
def confirmar_eliminacion(self, id_pedido): | |
"""Elimina el pedido después de la confirmación del usuario""" | |
try: | |
# Cerrar el diálogo de confirmación | |
if hasattr(self, 'dialog_confirmacion') and self.dialog_confirmacion: | |
self.dialog_confirmacion.dismiss() | |
# Eliminar el pedido de la base de datos | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
cursor.execute("DELETE FROM pedidos WHERE id = ?", (id_pedido,)) | |
conn.commit() | |
cursor.close() | |
conn.close() | |
# Cerrar el diálogo de edición | |
if hasattr(self, 'dialog_edicion') and self.dialog_edicion: | |
self.dialog_edicion.dismiss() | |
# Mostrar notificación y reabrir la pantalla de edición para refrescar | |
mostrar_notificacion("✅ Pedido eliminado correctamente") | |
# Volver a abrir la pantalla de edición actualizada | |
if hasattr(self, 'cliente_actual_edicion'): | |
self.abrir_edicion_cliente(self.cliente_actual_edicion) | |
except Exception as e: | |
mostrar_notificacion(f"❌ Error al eliminar: {str(e)}") | |
def guardar_cambios(self, instance): | |
conn = conectar_bd() | |
cursor = conn.cursor() | |
try: | |
for id_pedido, cantidad_input, costo_input in self.edicion_pedidos: | |
cursor.execute( | |
"UPDATE pedidos SET cantidad = ?, costo = ? WHERE id = ?", | |
(int(cantidad_input.text), float(costo_input.text), id_pedido) | |
) | |
conn.commit() | |
mostrar_notificacion("✅ Cambios guardados exitosamente!") | |
except Exception as e: | |
conn.rollback() | |
mostrar_notificacion(f"❌ Error: {str(e)}") | |
finally: | |
cursor.close() | |
conn.close() | |
self.dialog_edicion.dismiss() | |
def procesar_csv(self, seleccion, popup): | |
try: | |
popup.dismiss() | |
if not seleccion: | |
raise ValueError("No se seleccionó archivo") | |
ruta = seleccion[0] | |
if not ruta.lower().endswith('.csv'): | |
raise ValueError("Solo archivos CSV") | |
actualizar_stock_desde_csv(ruta) | |
mostrar_notificacion("✅ CSV procesado correctamente") | |
except Exception as e: | |
mostrar_notificacion(f"❌ Error: {str(e)}") | |
def mostrar_scanner_codigo_barras(self, instance): | |
"""Muestra un escáner de código de barras (requiere permiso de cámara)""" | |
if platform == 'android': | |
try: | |
from android.permissions import request_permissions, Permission, check_permission | |
if not check_permission(Permission.CAMERA): | |
request_permissions([Permission.CAMERA]) | |
mostrar_notificacion("❗ Se necesita permiso de cámara para escanear") | |
return | |
# Usar ZBarCam para escanear códigos de barras (necesitas incluirlo en requirements) | |
mostrar_notificacion("Esta función requiere zbarlight. Añádelo a requirements en buildozer.spec") | |
except ImportError: | |
mostrar_notificacion("❌ No se pudo importar el módulo de permisos Android") | |
else: | |
mostrar_notificacion("📱 El escáner de códigos solo está disponible en Android") | |
# Ejecutar la aplicación | |
if __name__ == '__main__': | |
PedidoApp().run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment