Desarrollo Paso a Paso: Construyendo el Sistema Empresarial
¡Es hora de construir nuestra obra maestra! 🏗️ En esta sección transformaremos todos los planos y diseños en código funcional. Será como dirigir la construcción de un rascacielos: cada línea de código es un ladrillo que colocamos con precisión para crear algo extraordinario.
🚀 Metodología de Construcción: De Cimientos a Cúspide
🏗️ Fase 1: Cimientos Sólidos (Configuración y Utilidades)
Comenzamos construyendo los cimientos de nuestro sistema empresarial. Como en cualquier construcción, los cimientos determinan la solidez de toda la estructura.
Paso 1.1: Configuración Central del Sistema
# archivo: config.py
"""
Configuración centralizada del Sistema de Gestión Empresarial
Todos los parámetros del sistema en un solo lugar
"""
import os
from pathlib import Path
from datetime import datetime
import json
class ConfiguracionSistema:
"""Configuración única y centralizada del sistema (Patrón Singleton)"""
_instancia = None
_inicializado = False
def __new__(cls):
if cls._instancia is None:
cls._instancia = super().__new__(cls)
return cls._instancia
def __init__(self):
if not ConfiguracionSistema._inicializado:
self._inicializar_configuracion()
ConfiguracionSistema._inicializado = True
def _inicializar_configuracion(self):
"""Inicializa toda la configuración del sistema"""
# 📁 RUTAS DEL SISTEMA
self.BASE_DIR = Path(__file__).parent
self.DATOS_DIR = self.BASE_DIR / "datos"
self.RESPALDOS_DIR = self.BASE_DIR / "respaldos"
self.LOGS_DIR = self.BASE_DIR / "logs"
self.REPORTES_DIR = self.BASE_DIR / "reportes"
self.TEMPLATES_DIR = self.BASE_DIR / "templates"
# 💾 ARCHIVOS DE BASE DE DATOS
self.DB_PRODUCTOS = self.DATOS_DIR / "productos.json"
self.DB_VENTAS = self.DATOS_DIR / "ventas.json"
self.DB_USUARIOS = self.DATOS_DIR / "usuarios.json"
self.DB_CONFIGURACION = self.DATOS_DIR / "configuracion.json"
self.DB_CATEGORIAS = self.DATOS_DIR / "categorias.json"
# 🏢 PARÁMETROS DE NEGOCIO
self.STOCK_MINIMO_DEFAULT = 5
self.STOCK_CRITICO = 2
self.DESCUENTO_MAXIMO = 0.50 # 50% máximo
self.IMPUESTO_DEFAULT = 0.16 # 16% IVA
self.MARGEN_REPOSICION = 2.0 # Factor para sugerencias de reposición
# 💰 CONFIGURACIÓN FINANCIERA
self.MONEDA_SIMBOLO = "$"
self.MONEDA_CODIGO = "MXN"
self.PRECISION_DECIMAL = 2
self.PRECIO_MINIMO = 0.01
self.PRECIO_MAXIMO = 999999.99
# 🔔 CONFIGURACIÓN DE ALERTAS
self.ALERTAS_HABILITADAS = True
self.ALERTA_STOCK_BAJO = True
self.ALERTA_PRECIO_ALTO = True
self.ALERTA_VENTA_GRANDE = True
self.UMBRAL_VENTA_GRANDE = 10000.0
# 📊 CONFIGURACIÓN DE REPORTES
self.REPORTES_AUTOMATICOS = True
self.FORMATO_FECHA_REPORTE = "%d/%m/%Y %H:%M:%S"
self.PRODUCTOS_POR_PAGINA = 20
self.DIAS_HISTORIAL_DEFAULT = 30
# 🔐 CONFIGURACIÓN DE SEGURIDAD
self.RESPALDO_AUTOMATICO = True
self.RESPALDOS_MAXIMOS = 10
self.VALIDACION_ESTRICTA = True
self.LOG_OPERACIONES = True
# 🎨 CONFIGURACIÓN DE INTERFAZ
self.LIMPIAR_PANTALLA = True
self.MOSTRAR_LOGO = True
self.COLOR_EXITO = "verde"
self.COLOR_ERROR = "rojo"
self.COLOR_ADVERTENCIA = "amarillo"
# Crear directorios necesarios
self._crear_directorios()
# Cargar configuración personalizada si existe
self._cargar_configuracion_personalizada()
def _crear_directorios(self):
"""Crea todos los directorios necesarios"""
directorios = [
self.DATOS_DIR,
self.RESPALDOS_DIR,
self.LOGS_DIR,
self.REPORTES_DIR,
self.TEMPLATES_DIR
]
for directorio in directorios:
directorio.mkdir(parents=True, exist_ok=True)
def _cargar_configuracion_personalizada(self):
"""Carga configuración personalizada desde archivo"""
if self.DB_CONFIGURACION.exists():
try:
with open(self.DB_CONFIGURACION, 'r', encoding='utf-8') as f:
config_personalizada = json.load(f)
# Aplicar configuración personalizada
for clave, valor in config_personalizada.items():
if hasattr(self, clave):
setattr(self, clave, valor)
except Exception as e:
print(f"⚠️ Error cargando configuración personalizada: {e}")
def guardar_configuracion(self):
"""Guarda la configuración actual"""
config_data = {
attr: getattr(self, attr)
for attr in dir(self)
if not attr.startswith('_') and not callable(getattr(self, attr))
}
try:
with open(self.DB_CONFIGURACION, 'w', encoding='utf-8') as f:
json.dump(config_data, f, indent=2, ensure_ascii=False, default=str)
return True
except Exception as e:
print(f"❌ Error guardando configuración: {e}")
return False
def obtener_info_sistema(self):
"""Retorna información del sistema"""
return {
"version": "1.0.0",
"nombre": "Sistema de Gestión Empresarial",
"autor": "Tu Nombre",
"fecha_creacion": datetime.now().isoformat(),
"python_version": f"{os.sys.version_info.major}.{os.sys.version_info.minor}",
"directorio_base": str(self.BASE_DIR)
}
# Instancia global de configuración
config = ConfiguracionSistema()
Paso 1.2: Sistema de Logging Empresarial
# archivo: logger.py
"""
Sistema de logging empresarial para auditoría y debugging
"""
import logging
import logging.handlers
from datetime import datetime
from pathlib import Path
from config import config
class LoggerEmpresarial:
"""Sistema de logging centralizado para el sistema empresarial"""
def __init__(self):
self.logger = None
self._configurar_logger()
def _configurar_logger(self):
"""Configura el sistema de logging"""
# Crear logger principal
self.logger = logging.getLogger('SistemaEmpresarial')
self.logger.setLevel(logging.DEBUG)
# Evitar duplicación de logs
if self.logger.handlers:
return
# Formato de logs
formato = logging.Formatter(
'%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Handler para archivo principal
archivo_log = config.LOGS_DIR / "sistema.log"
handler_archivo = logging.handlers.RotatingFileHandler(
archivo_log,
maxBytes=5*1024*1024, # 5MB
backupCount=5,
encoding='utf-8'
)
handler_archivo.setLevel(logging.INFO)
handler_archivo.setFormatter(formato)
# Handler para errores críticos
archivo_errores = config.LOGS_DIR / "errores.log"
handler_errores = logging.handlers.RotatingFileHandler(
archivo_errores,
maxBytes=2*1024*1024, # 2MB
backupCount=3,
encoding='utf-8'
)
handler_errores.setLevel(logging.ERROR)
handler_errores.setFormatter(formato)
# Handler para consola (solo en desarrollo)
handler_consola = logging.StreamHandler()
handler_consola.setLevel(logging.WARNING)
handler_consola.setFormatter(logging.Formatter(
'%(levelname)s: %(message)s'
))
# Agregar handlers
self.logger.addHandler(handler_archivo)
self.logger.addHandler(handler_errores)
self.logger.addHandler(handler_consola)
def info(self, mensaje, **kwargs):
"""Log de información"""
self.logger.info(mensaje, extra=kwargs)
def warning(self, mensaje, **kwargs):
"""Log de advertencia"""
self.logger.warning(mensaje, extra=kwargs)
def error(self, mensaje, **kwargs):
"""Log de error"""
self.logger.error(mensaje, extra=kwargs)
def critical(self, mensaje, **kwargs):
"""Log crítico"""
self.logger.critical(mensaje, extra=kwargs)
def debug(self, mensaje, **kwargs):
"""Log de debug"""
self.logger.debug(mensaje, extra=kwargs)
def operacion(self, operacion, usuario="sistema", detalles=None):
"""Log específico para operaciones de negocio"""
mensaje = f"OPERACION: {operacion} | Usuario: {usuario}"
if detalles:
mensaje += f" | Detalles: {detalles}"
self.info(mensaje)
def venta(self, codigo_producto, cantidad, total, usuario="sistema"):
"""Log específico para ventas"""
self.operacion(
"VENTA",
usuario,
f"Producto: {codigo_producto}, Cantidad: {cantidad}, Total: ${total:.2f}"
)
def inventario(self, operacion, codigo_producto, detalles=""):
"""Log específico para operaciones de inventario"""
self.operacion(
f"INVENTARIO_{operacion}",
"sistema",
f"Producto: {codigo_producto} | {detalles}"
)
# Instancia global del logger
logger = LoggerEmpresarial()
Paso 1.3: Caja de Herramientas Empresarial (Utilidades)
# archivo: utilidades.py
"""
Caja de herramientas empresarial - Utilidades fundamentales
Todas las herramientas auxiliares que necesita nuestro sistema
"""
import json
import os
import csv
import hashlib
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple, Union
from pathlib import Path
import re
from config import config
from logger import logger
class ValidadorEmpresarial:
"""Validador de reglas de negocio empresarial"""
@staticmethod
def codigo_producto(codigo: str) -> Tuple[bool, str]:
"""Valida código de producto según estándares empresariales"""
if not codigo:
return False, "Código no puede estar vacío"
codigo = codigo.strip().upper()
if len(codigo) < 3:
return False, "Código debe tener mínimo 3 caracteres"
if len(codigo) > 10:
return False, "Código debe tener máximo 10 caracteres"
if not re.match(r'^[A-Z0-9]+$', codigo):
return False, "Código solo puede contener letras y números (sin espacios ni símbolos)"
# Verificar que no sea solo números
if codigo.isdigit():
return False, "Código debe contener al menos una letra"
return True, "Código válido"
@staticmethod
def precio_producto(precio: Union[int, float]) -> Tuple[bool, str]:
"""Valida precio según políticas empresariales"""
try:
precio = float(precio)
except (ValueError, TypeError):
return False, "Precio debe ser numérico"
if precio <= 0:
return False, "Precio debe ser positivo"
if precio < config.PRECIO_MINIMO:
return False, f"Precio mínimo permitido: ${config.PRECIO_MINIMO}"
if precio > config.PRECIO_MAXIMO:
return False, f"Precio máximo permitido: ${config.PRECIO_MAXIMO:,.2f}"
# Verificar precisión decimal
if round(precio, config.PRECISION_DECIMAL) != precio:
return False, f"Precio debe tener máximo {config.PRECISION_DECIMAL} decimales"
return True, "Precio válido"
@staticmethod
def stock_producto(stock: Union[int, str]) -> Tuple[bool, str]:
"""Valida stock según reglas empresariales"""
try:
stock = int(stock)
except (ValueError, TypeError):
return False, "Stock debe ser un número entero"
if stock < 0:
return False, "Stock no puede ser negativo"
if stock > 999999:
return False, "Stock máximo permitido: 999,999 unidades"
return True, "Stock válido"
@staticmethod
def nombre_producto(nombre: str) -> Tuple[bool, str]:
"""Valida nombre de producto"""
if not nombre or not nombre.strip():
return False, "Nombre no puede estar vacío"
nombre = nombre.strip()
if len(nombre) < 2:
return False, "Nombre debe tener mínimo 2 caracteres"
if len(nombre) > 100:
return False, "Nombre debe tener máximo 100 caracteres"
# Verificar caracteres válidos
if not re.match(r'^[a-zA-Z0-9\s\-_.,()]+$', nombre):
return False, "Nombre contiene caracteres no permitidos"
return True, "Nombre válido"
@staticmethod
def categoria_producto(categoria: str) -> Tuple[bool, str]:
"""Valida categoría de producto"""
if not categoria or not categoria.strip():
return True, "Categoría válida (se usará 'General')"
categoria = categoria.strip()
if len(categoria) > 50:
return False, "Categoría debe tener máximo 50 caracteres"
if not re.match(r'^[a-zA-Z0-9\s\-_]+$', categoria):
return False, "Categoría contiene caracteres no permitidos"
return True, "Categoría válida"
@staticmethod
def cantidad_venta(cantidad: Union[int, str]) -> Tuple[bool, str]:
"""Valida cantidad para venta"""
try:
cantidad = int(cantidad)
except (ValueError, TypeError):
return False, "Cantidad debe ser un número entero"
if cantidad <= 0:
return False, "Cantidad debe ser mayor a cero"
if cantidad > 1000:
return False, "Cantidad máxima por venta: 1,000 unidades"
return True, "Cantidad válida"
@staticmethod
def descuento(descuento: Union[int, float]) -> Tuple[bool, str]:
"""Valida descuento aplicado"""
try:
descuento = float(descuento)
except (ValueError, TypeError):
return False, "Descuento debe ser numérico"
if descuento < 0:
return False, "Descuento no puede ser negativo"
if descuento > config.DESCUENTO_MAXIMO:
return False, f"Descuento máximo permitido: {config.DESCUENTO_MAXIMO * 100}%"
return True, "Descuento válido"
class FormateadorEmpresarial:
"""Formateador de datos para presentación empresarial"""
@staticmethod
def moneda(cantidad: float, incluir_simbolo: bool = True) -> str:
"""Formatea cantidad como moneda"""
if incluir_simbolo:
return f"{config.MONEDA_SIMBOLO}{cantidad:,.{config.PRECISION_DECIMAL}f}"
else:
return f"{cantidad:,.{config.PRECISION_DECIMAL}f}"
@staticmethod
def fecha_empresarial(fecha: datetime) -> str:
"""Formatea fecha para reportes empresariales"""
return fecha.strftime(config.FORMATO_FECHA_REPORTE)
@staticmethod
def fecha_corta(fecha: datetime) -> str:
"""Formatea fecha en formato corto"""
return fecha.strftime("%d/%m/%Y")
@staticmethod
def porcentaje(decimal: float, decimales: int = 1) -> str:
"""Convierte decimal a porcentaje"""
return f"{decimal * 100:.{decimales}f}%"
@staticmethod
def numero_entero(numero: int) -> str:
"""Formatea número entero con separadores de miles"""
return f"{numero:,}"
@staticmethod
def codigo_producto(codigo: str) -> str:
"""Formatea código de producto"""
return codigo.upper().strip()
@staticmethod
def nombre_propio(texto: str) -> str:
"""Convierte texto a formato de nombre propio"""
return texto.strip().title()
@staticmethod
def texto_tabla(texto: str, ancho: int, alineacion: str = "izquierda") -> str:
"""Formatea texto para tablas con ancho fijo"""
texto = str(texto)[:ancho] # Truncar si es muy largo
if alineacion == "derecha":
return texto.rjust(ancho)
elif alineacion == "centro":
return texto.center(ancho)
else: # izquierda
return texto.ljust(ancho)
class GestorArchivos:
"""Gestor de persistencia empresarial con respaldos automáticos"""
@staticmethod
def cargar_json(archivo: Path, crear_si_no_existe: bool = True) -> Dict[str, Any]:
"""Carga datos desde archivo JSON con manejo robusto de errores"""
try:
if archivo.exists():
with open(archivo, 'r', encoding='utf-8') as f:
datos = json.load(f)
logger.debug(f"Datos cargados desde {archivo}")
return datos
else:
if crear_si_no_existe:
logger.info(f"Archivo {archivo} no existe, creando archivo vacío")
GestorArchivos.guardar_json({}, archivo)
return {}
else:
logger.warning(f"Archivo {archivo} no existe")
return {}
except json.JSONDecodeError as e:
logger.error(f"Error JSON en {archivo}: {e}")
# Intentar recuperar desde respaldo
return GestorArchivos._recuperar_desde_respaldo(archivo)
except Exception as e:
logger.error(f"Error cargando {archivo}: {e}")
return {}
@staticmethod
def guardar_json(datos: Dict[str, Any], archivo: Path, crear_respaldo: bool = True) -> bool:
"""Guarda datos en archivo JSON con respaldo automático"""
try:
# Crear respaldo si el archivo existe y está configurado
if crear_respaldo and archivo.exists() and config.RESPALDO_AUTOMATICO:
GestorArchivos._crear_respaldo(archivo)
# Crear directorio si no existe
archivo.parent.mkdir(parents=True, exist_ok=True)
# Guardar datos con formato bonito
with open(archivo, 'w', encoding='utf-8') as f:
json.dump(
datos,
f,
indent=2,
ensure_ascii=False,
default=str,
sort_keys=True
)
logger.debug(f"Datos guardados en {archivo}")
return True
except Exception as e:
logger.error(f"Error guardando {archivo}: {e}")
return False
@staticmethod
def _crear_respaldo(archivo: Path):
"""Crea respaldo de un archivo"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
nombre_respaldo = f"{archivo.stem}_backup_{timestamp}{archivo.suffix}"
archivo_respaldo = config.RESPALDOS_DIR / nombre_respaldo
# Copiar archivo
import shutil
shutil.copy2(archivo, archivo_respaldo)
logger.info(f"Respaldo creado: {archivo_respaldo}")
# Limpiar respaldos antiguos
GestorArchivos._limpiar_respaldos_antiguos(archivo.stem)
except Exception as e:
logger.error(f"Error creando respaldo de {archivo}: {e}")
@staticmethod
def _limpiar_respaldos_antiguos(prefijo: str):
"""Limpia respaldos antiguos manteniendo solo los más recientes"""
try:
patron = f"{prefijo}_backup_*"
respaldos = list(config.RESPALDOS_DIR.glob(patron))
if len(respaldos) > config.RESPALDOS_MAXIMOS:
# Ordenar por fecha de modificación (más reciente primero)
respaldos.sort(key=lambda x: x.stat().st_mtime, reverse=True)
# Eliminar los más antiguos
for respaldo in respaldos[config.RESPALDOS_MAXIMOS:]:
respaldo.unlink()
logger.debug(f"Respaldo antiguo eliminado: {respaldo}")
except Exception as e:
logger.error(f"Error limpiando respaldos antiguos: {e}")
@staticmethod
def _recuperar_desde_respaldo(archivo: Path) -> Dict[str, Any]:
"""Intenta recuperar datos desde el respaldo más reciente"""
try:
patron = f"{archivo.stem}_backup_*{archivo.suffix}"
respaldos = list(config.RESPALDOS_DIR.glob(patron))
if respaldos:
# Obtener el respaldo más reciente
respaldo_reciente = max(respaldos, key=lambda x: x.stat().st_mtime)
with open(respaldo_reciente, 'r', encoding='utf-8') as f:
datos = json.load(f)
logger.warning(f"Datos recuperados desde respaldo: {respaldo_reciente}")
return datos
else:
logger.error(f"No hay respaldos disponibles para {archivo}")
return {}
except Exception as e:
logger.error(f"Error recuperando desde respaldo: {e}")
return {}
@staticmethod
def exportar_csv(datos: List[Dict], archivo: Path, encabezados: List[str] = None) -> bool:
"""Exporta datos a archivo CSV"""
try:
if not datos:
logger.warning("No hay datos para exportar")
return False
# Crear directorio si no existe
archivo.parent.mkdir(parents=True, exist_ok=True)
# Determinar encabezados si no se proporcionan
if not encabezados:
encabezados = list(datos[0].keys())
with open(archivo, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=encabezados)
writer.writeheader()
writer.writerows(datos)
logger.info(f"Datos exportados a CSV: {archivo}")
return True
except Exception as e:
logger.error(f"Error exportando CSV {archivo}: {e}")
return False
class InterfazUsuario:
"""Utilidades para interfaz de usuario empresarial"""
@staticmethod
def limpiar_pantalla():
"""Limpia la pantalla de la consola"""
if config.LIMPIAR_PANTALLA:
os.system('cls' if os.name == 'nt' else 'clear')
@staticmethod
def pausar(mensaje: str = "Presiona Enter para continuar..."):
"""Pausa la ejecución hasta que el usuario presione Enter"""
try:
input(f"\n{mensaje}")
except KeyboardInterrupt:
print("\n👋 Operación cancelada por el usuario")
@staticmethod
def mostrar_titulo(titulo: str, caracter: str = "=", ancho: int = 60):
"""Muestra un título formateado empresarial"""
print(f"\n{caracter * ancho}")
print(f"{titulo.center(ancho)}")
print(f"{caracter * ancho}")
@staticmethod
def mostrar_subtitulo(subtitulo: str, caracter: str = "-", ancho: int = 50):
"""Muestra un subtítulo formateado"""
print(f"\n{caracter * ancho}")
print(f"{subtitulo.center(ancho)}")
print(f"{caracter * ancho}")
@staticmethod
def mostrar_mensaje_exito(mensaje: str):
"""Muestra mensaje de éxito"""
print(f"✅ {mensaje}")
@staticmethod
def mostrar_mensaje_error(mensaje: str):
"""Muestra mensaje de error"""
print(f"❌ {mensaje}")
@staticmethod
def mostrar_mensaje_advertencia(mensaje: str):
"""Muestra mensaje de advertencia"""
print(f"⚠️ {mensaje}")
@staticmethod
def mostrar_mensaje_info(mensaje: str):
"""Muestra mensaje informativo"""
print(f"ℹ️ {mensaje}")
@staticmethod
def confirmar_accion(mensaje: str, default: bool = False) -> bool:
"""Pide confirmación al usuario"""
opciones = "(s/N)" if not default else "(S/n)"
respuesta = input(f"{mensaje} {opciones}: ").strip().lower()
if not respuesta:
return default
return respuesta in ['s', 'si', 'sí', 'y', 'yes', '1']
@staticmethod
def obtener_entrada_usuario(
mensaje: str,
tipo: type = str,
validador=None,
obligatorio: bool = True,
valor_default=None
):
"""Obtiene entrada del usuario con validación robusta"""
while True:
try:
# Mostrar mensaje con valor default si existe
prompt = mensaje
if valor_default is not None:
prompt += f" [{valor_default}]"
prompt += ": "
entrada = input(prompt).strip()
# Usar valor default si no se ingresa nada
if not entrada and valor_default is not None:
entrada = str(valor_default)
# Verificar si es obligatorio
if not entrada and obligatorio:
InterfazUsuario.mostrar_mensaje_error("Este campo es obligatorio")
continue
# Si no es obligatorio y está vacío, retornar None
if not entrada and not obligatorio:
return None
# Convertir al tipo deseado
if tipo == int:
valor = int(entrada)
elif tipo == float:
valor = float(entrada)
elif tipo == bool:
valor = entrada.lower() in ['true', '1', 's', 'si', 'sí', 'yes']
else:
valor = entrada
# Aplicar validador si existe
if validador:
if callable(validador):
# Validador es una función
if hasattr(validador, '__call__'):
resultado = validador(valor)
if isinstance(resultado, tuple):
es_valido, mensaje_error = resultado
if not es_valido:
InterfazUsuario.mostrar_mensaje_error(mensaje_error)
continue
elif not resultado:
InterfazUsuario.mostrar_mensaje_error("Valor inválido")
continue
else:
# Validador es un valor/lista de valores válidos
if valor not in validador:
InterfazUsuario.mostrar_mensaje_error(f"Valor debe ser uno de: {validador}")
continue
return valor
except ValueError as e:
InterfazUsuario.mostrar_mensaje_error(f"Debe ingresar un {tipo.__name__} válido")
except KeyboardInterrupt:
print("\n👋 Operación cancelada por el usuario")
return None
except Exception as e:
InterfazUsuario.mostrar_mensaje_error(f"Error inesperado: {e}")
@staticmethod
def mostrar_menu(opciones: List[str], titulo: str = "MENÚ", mostrar_salir: bool = True) -> int:
"""Muestra un menú empresarial y devuelve la opción seleccionada"""
InterfazUsuario.mostrar_titulo(titulo)
# Mostrar opciones
for i, opcion in enumerate(opciones, 1):
print(f" {i}. {opcion}")
if mostrar_salir:
print(f" 0. Salir")
print() # Línea en blanco
# Obtener selección
while True:
try:
max_opcion = len(opciones)
min_opcion = 0 if mostrar_salir else 1
seleccion = int(input(f"Selecciona una opción ({min_opcion}-{max_opcion}): "))
if min_opcion <= seleccion <= max_opcion:
return seleccion
else:
InterfazUsuario.mostrar_mensaje_error(
f"Opción debe estar entre {min_opcion} y {max_opcion}"
)
except ValueError:
InterfazUsuario.mostrar_mensaje_error("Debe ingresar un número válido")
except KeyboardInterrupt:
print("\n👋 Saliendo del menú...")
return 0
@staticmethod
def mostrar_tabla(
datos: List[Dict],
encabezados: List[str] = None,
titulo: str = None,
max_filas: int = None
):
"""Muestra datos en formato de tabla empresarial"""
if not datos:
InterfazUsuario.mostrar_mensaje_info("No hay datos para mostrar")
return
# Determinar encabezados si no se proporcionan
if not encabezados:
encabezados = list(datos[0].keys())
# Mostrar título si se proporciona
if titulo:
InterfazUsuario.mostrar_subtitulo(titulo)
# Calcular anchos de columna
anchos = {}
for encabezado in encabezados:
ancho_encabezado = len(str(encabezado))
ancho_datos = max(len(str(fila.get(encabezado, ""))) for fila in datos)
anchos[encabezado] = max(ancho_encabezado, ancho_datos, 10) # Mínimo 10
# Mostrar encabezados
linea_separadora = "+" + "+".join("-" * (anchos[enc] + 2) for enc in encabezados) + "+"
print(linea_separadora)
encabezados_formateados = "|".join(
f" {enc.center(anchos[enc])} " for enc in encabezados
)
print(f"|{encabezados_formateados}|")
print(linea_separadora)
# Mostrar datos (limitados si se especifica max_filas)
datos_mostrar = datos[:max_filas] if max_filas else datos
for fila in datos_mostrar:
fila_formateada = "|".join(
f" {str(fila.get(enc, '')).ljust(anchos[enc])} " for enc in encabezados
)
print(f"|{fila_formateada}|")
print(linea_separadora)
# Mostrar información adicional si hay más filas
if max_filas and len(datos) > max_filas:
print(f"... y {len(datos) - max_filas} filas más")
print(f"Total de registros: {len(datos)}")
# Instancias globales para fácil acceso
validador = ValidadorEmpresarial()
formateador = FormateadorEmpresarial()
gestor_archivos = GestorArchivos()
interfaz = InterfazUsuario()
🏗️ Fase 2: Estructura Principal (Entidades de Negocio)
Con los cimientos sólidos, ahora construimos las entidades principales de nuestro sistema empresarial.
# archivo: reportes.py
"""
Módulo de generación de reportes para el sistema de inventario.
"""
from datetime import datetime
from typing import List, Dict
from inventario import GestorInventario, Producto
from utilidades import *
class GeneradorReportes:
"""Clase para generar diferentes tipos de reportes"""
def __init__(self, gestor: GestorInventario):
self.gestor = gestor
def reporte_inventario_completo(self):
"""Genera reporte completo del inventario"""
mostrar_titulo("REPORTE COMPLETO DE INVENTARIO", "=")
productos = self.gestor.listar_productos()
if not productos:
print("📦 No hay productos en el inventario")
return
# Estadísticas generales
stats = self.gestor.obtener_estadisticas()
print(f"📊 ESTADÍSTICAS GENERALES:")
print(f" Total de productos: {stats['total_productos']}")
print(f" Total de unidades: {stats['total_stock']}")
print(f" Valor total: {formatear_precio(stats['valor_total'])}")
print(f" Productos con stock bajo: {stats['productos_stock_bajo']}")
print(f" Categorías: {stats['categorias']}")
# Lista de productos
print(f"\n📦 PRODUCTOS:")
for producto in productos:
print(f" {producto}")
def reporte_stock_bajo(self):
"""Genera reporte de productos con stock bajo"""
mostrar_titulo("REPORTE DE STOCK BAJO", "⚠")
productos_bajo = self.gestor.obtener_productos_stock_bajo()
if not productos_bajo:
print("✅ Todos los productos tienen stock suficiente")
return
print(f"⚠️ {len(productos_bajo)} productos con stock bajo:")
for producto in productos_bajo:
print(f" {producto.codigo}: {producto.nombre}")
print(f" Stock actual: {producto.stock}")
print(f" Stock mínimo: {producto.stock_minimo}")
print(f" Sugerido reponer: {producto.stock_minimo * 2}")
print()
def reporte_por_categoria(self):
"""Genera reporte agrupado por categorías"""
mostrar_titulo("REPORTE POR CATEGORÍAS", "📂")
categorias = self.gestor.obtener_categorias()
for categoria in categorias:
productos = self.gestor.listar_productos(categoria)
valor_categoria = sum(p.precio * p.stock for p in productos)
print(f"\n📂 {categoria.upper()}:")
print(f" Productos: {len(productos)}")
print(f" Valor total: {formatear_precio(valor_categoria)}")
for producto in productos:
print(f" {producto}")
Programa principal
# archivo: main.py
"""
Sistema de Gestión de Inventario Inteligente
Programa principal con interfaz de usuario
"""
from inventario import GestorInventario
from reportes import GeneradorReportes
from utilidades import *
class SistemaInventario:
"""Clase principal del sistema"""
def __init__(self):
self.gestor = GestorInventario()
self.reportes = GeneradorReportes(self.gestor)
self.ejecutando = True
def mostrar_menu_principal(self):
"""Muestra el menú principal"""
opciones = [
"Gestión de Productos",
"Procesar Venta",
"Reportes",
"Buscar Productos",
"Alertas de Stock"
]
return mostrar_menu(opciones, "SISTEMA DE INVENTARIO")
def menu_gestion_productos(self):
"""Menú de gestión de productos"""
while True:
opciones = [
"Agregar Producto",
"Listar Productos",
"Actualizar Producto",
"Eliminar Producto"
]
opcion = mostrar_menu(opciones, "GESTIÓN DE PRODUCTOS")
if opcion == 0:
break
elif opcion == 1:
self.agregar_producto()
elif opcion == 2:
self.listar_productos()
elif opcion == 3:
self.actualizar_producto()
elif opcion == 4:
self.eliminar_producto()
def agregar_producto(self):
"""Interfaz para agregar producto"""
mostrar_titulo("AGREGAR PRODUCTO")
try:
codigo = obtener_entrada_usuario("Código del producto", str, validar_codigo_producto)
if not codigo:
return
nombre = obtener_entrada_usuario("Nombre del producto")
if not nombre:
return
precio = obtener_entrada_usuario("Precio", float, validar_precio)
if precio is None:
return
stock = obtener_entrada_usuario("Stock inicial", int, validar_stock)
if stock is None:
return
categoria = obtener_entrada_usuario("Categoría (opcional)", str) or "General"
stock_minimo = obtener_entrada_usuario("Stock mínimo (opcional)", int, validar_stock) or 5
exito, mensaje = self.gestor.agregar_producto(codigo, nombre, precio, stock, categoria, stock_minimo)
if exito:
print(f"✅ {mensaje}")
else:
print(f"❌ {mensaje}")
except KeyboardInterrupt:
print("\n👋 Operación cancelada")
pausar()
def procesar_venta(self):
"""Interfaz para procesar ventas"""
mostrar_titulo("PROCESAR VENTA")
try:
codigo = obtener_entrada_usuario("Código del producto")
if not codigo:
return
producto = self.gestor.obtener_producto(codigo)
if not producto:
print(f"❌ Producto {codigo} no encontrado")
pausar()
return
print(f"📦 Producto: {producto.nombre}")
print(f"💰 Precio: {formatear_precio(producto.precio)}")
print(f"📊 Stock disponible: {producto.stock}")
cantidad = obtener_entrada_usuario("Cantidad a vender", int, lambda x: x > 0)
if cantidad is None:
return
total = producto.precio * cantidad
print(f"\n💰 Total de la venta: {formatear_precio(total)}")
if confirmar_accion("¿Confirmar venta?"):
exito, mensaje, total_real = self.gestor.procesar_venta(codigo, cantidad)
if exito:
print(f"✅ {mensaje}")
print(f"💰 Total: {formatear_precio(total_real)}")
else:
print(f"❌ {mensaje}")
else:
print("👋 Venta cancelada")
except KeyboardInterrupt:
print("\n👋 Operación cancelada")
pausar()
def menu_reportes(self):
"""Menú de reportes"""
while True:
opciones = [
"Reporte Completo",
"Productos con Stock Bajo",
"Reporte por Categorías",
"Estadísticas Generales"
]
opcion = mostrar_menu(opciones, "REPORTES")
if opcion == 0:
break
elif opcion == 1:
self.reportes.reporte_inventario_completo()
pausar()
elif opcion == 2:
self.reportes.reporte_stock_bajo()
pausar()
elif opcion == 3:
self.reportes.reporte_por_categoria()
pausar()
elif opcion == 4:
self.mostrar_estadisticas()
pausar()
def ejecutar(self):
"""Ejecuta el sistema principal"""
print("🏪 Bienvenido al Sistema de Gestión de Inventario")
while self.ejecutando:
try:
opcion = self.mostrar_menu_principal()
if opcion == 0:
self.ejecutando = False
elif opcion == 1:
self.menu_gestion_productos()
elif opcion == 2:
self.procesar_venta()
elif opcion == 3:
self.menu_reportes()
elif opcion == 4:
self.buscar_productos()
elif opcion == 5:
self.mostrar_alertas()
except KeyboardInterrupt:
print("\n👋 Saliendo del sistema...")
self.ejecutando = False
print("¡Gracias por usar el Sistema de Inventario! 👋")
def main():
"""Función principal"""
sistema = SistemaInventario()
sistema.ejecutar()
if __name__ == "__main__":
main()
Pruebas del sistema
# archivo: test_sistema.py
"""
Pruebas básicas del sistema de inventario
"""
from inventario import GestorInventario, Producto
def test_crear_producto():
"""Prueba la creación de productos"""
producto = Producto("TEST01", "Producto de Prueba", 100.0, 10)
assert producto.codigo == "TEST01"
assert producto.nombre == "Producto de Prueba"
assert producto.precio == 100.0
assert producto.stock == 10
print("✅ Test crear producto: PASÓ")
def test_gestor_inventario():
"""Prueba el gestor de inventario"""
gestor = GestorInventario("test_productos.json")
# Agregar producto
exito, mensaje = gestor.agregar_producto("TEST02", "Producto Test", 50.0, 20)
assert exito, f"Error al agregar producto: {mensaje}"
# Obtener producto
producto = gestor.obtener_producto("TEST02")
assert producto is not None, "Producto no encontrado"
assert producto.nombre == "Producto Test"
# Procesar venta
exito, mensaje, total = gestor.procesar_venta("TEST02", 5)
assert exito, f"Error al procesar venta: {mensaje}"
assert total == 250.0, f"Total incorrecto: {total}"
# Verificar stock actualizado
producto = gestor.obtener_producto("TEST02")
assert producto.stock == 15, f"Stock incorrecto: {producto.stock}"
print("✅ Test gestor inventario: PASÓ")
if __name__ == "__main__":
test_crear_producto()
test_gestor_inventario()
print("🎉 Todas las pruebas pasaron!")