Compare commits
	
		
			6 commits
		
	
	
		
			0efd6cf2b8
			...
			e3dc9c90b7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e3dc9c90b7 | |||
| a656592601 | |||
| 08dfaef620 | |||
| 20c645b06e | |||
| 41c40265cf | |||
| 2ba3bb68ec | 
					 2 changed files with 499 additions and 0 deletions
				
			
		
							
								
								
									
										46
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								README.md
									
									
									
									
									
								
							|  | @ -80,6 +80,8 @@ bin/update.sh | ||||||
| | Comando | Descripción | | | Comando | Descripción | | ||||||
| |---------|-------------| | |---------|-------------| | ||||||
| | `bin/odoo_set.sh` | Crea un nuevo proyecto Odoo con estructura completa | | | `bin/odoo_set.sh` | Crea un nuevo proyecto Odoo con estructura completa | | ||||||
|  | | `bin/rate_update.py` | Actualiza tarifas por hora de diferentes tipos de programadores | | ||||||
|  | | `bin/cocomo.py` | Calcula costos de proyecto usando el modelo COCOMO | | ||||||
| 
 | 
 | ||||||
| ## 📚 Guía de Usuario | ## 📚 Guía de Usuario | ||||||
| 
 | 
 | ||||||
|  | @ -166,6 +168,50 @@ cd [ruta-al-proyecto] | ||||||
| ./scripts/start.sh | ./scripts/start.sh | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ### Gestión de Tarifas y Costos | ||||||
|  | 
 | ||||||
|  | MRDevs Tools incluye utilidades para la gestión de tarifas y estimación de costos: | ||||||
|  | 
 | ||||||
|  | #### Actualización de Tarifas | ||||||
|  | 
 | ||||||
|  | El script `rate_update.py` permite mantener actualizadas las tarifas por hora de diferentes tipos de programadores: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | # Actualizar tarifas faltantes | ||||||
|  | bin/rate_update.py | ||||||
|  | 
 | ||||||
|  | # Listar todas las tarifas disponibles | ||||||
|  | bin/rate_update.py --list | ||||||
|  | 
 | ||||||
|  | # Actualizar la tarifa de un tipo específico de programador | ||||||
|  | bin/rate_update.py --type python | ||||||
|  | 
 | ||||||
|  | # Actualizar todas las tarifas, incluso las existentes | ||||||
|  | bin/rate_update.py --init | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Las tarifas se almacenan en archivos individuales dentro de `bin/config/rates/` y pueden ser utilizadas por otras herramientas. | ||||||
|  | 
 | ||||||
|  | #### Estimación de Costos con COCOMO | ||||||
|  | 
 | ||||||
|  | El script `cocomo.py` implementa el modelo COCOMO para estimar costos de proyectos de software basados en las líneas de código: | ||||||
|  | 
 | ||||||
|  | ```bash | ||||||
|  | # Calcular costo de un proyecto | ||||||
|  | bin/cocomo.py --project /ruta/al/proyecto | ||||||
|  | 
 | ||||||
|  | # Usar un tipo específico de programador para los costos | ||||||
|  | bin/cocomo.py --project /ruta/al/proyecto --type devops | ||||||
|  | 
 | ||||||
|  | # Especificar un modelo COCOMO específico | ||||||
|  | bin/cocomo.py --project /ruta/al/proyecto --model embedded | ||||||
|  | 
 | ||||||
|  | # Ignorar patrones adicionales de archivos | ||||||
|  | bin/cocomo.py --project /ruta/al/proyecto --ignore "*.generated.js" --ignore "vendor/**" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | El script analizará el código fuente, contará las líneas efectivas, y calculará estimaciones de esfuerzo, tiempo y costos utilizando las tarifas por hora configuradas. | ||||||
|  | 
 | ||||||
| ## 🔌 Arquitectura del Sistema | ## 🔌 Arquitectura del Sistema | ||||||
| 
 | 
 | ||||||
| ### Estructura de Directorios | ### Estructura de Directorios | ||||||
|  |  | ||||||
							
								
								
									
										453
									
								
								bin/cocomo.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										453
									
								
								bin/cocomo.py
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,453 @@ | ||||||
|  | #!/usr/bin/env python3 | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | """ | ||||||
|  | COCOMO Calculator | ||||||
|  | 
 | ||||||
|  | Este script calcula el costo de un proyecto de software utilizando el modelo COCOMO, | ||||||
|  | basado en el conteo de líneas de código en un directorio dado. | ||||||
|  | 
 | ||||||
|  | Licencia: AGPL-3.0 | ||||||
|  | Modified: 2025-03-12 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import argparse | ||||||
|  | import logging | ||||||
|  | import fnmatch | ||||||
|  | from pathlib import Path | ||||||
|  | from math import ceil | ||||||
|  | 
 | ||||||
|  | # Configuración de logging | ||||||
|  | console_handler = logging.StreamHandler() | ||||||
|  | console_handler.setLevel(logging.WARNING) | ||||||
|  | console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) | ||||||
|  | 
 | ||||||
|  | # Configuración global del logger | ||||||
|  | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[]) | ||||||
|  | logger = logging.getLogger('cocomo') | ||||||
|  | logger.addHandler(console_handler) | ||||||
|  | 
 | ||||||
|  | # Variable para controlar si se muestra el resultado en la consola | ||||||
|  | SHOW_RESULTS = True | ||||||
|  | 
 | ||||||
|  | # Directorio base del proyecto | ||||||
|  | BASE_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||||||
|  | CONFIG_DIR = BASE_DIR / 'bin' / 'config' | ||||||
|  | RATES_DIR = CONFIG_DIR / 'rates'  # Ubicación para archivos de tarifas | ||||||
|  | 
 | ||||||
|  | # Constantes para el modelo COCOMO básico | ||||||
|  | COCOMO_MODELS = { | ||||||
|  |     # [a, b, c, d] - Coeficientes para Esfuerzo = a * (KLOC^b) y Tiempo = c * (Esfuerzo^d) | ||||||
|  |     'organic': [2.4, 1.05, 2.5, 0.38],     # Proyectos simples, pequeños equipos con experiencia | ||||||
|  |     'semi-detached': [3.0, 1.12, 2.5, 0.35],  # Proyectos intermedios | ||||||
|  |     'embedded': [3.6, 1.20, 2.5, 0.32]     # Proyectos complejos | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | # Número de horas laborables en un mes (22 días x 8 horas) | ||||||
|  | HOURS_PER_MONTH = 176 | ||||||
|  | 
 | ||||||
|  | # Extensiones de archivo a considerar por defecto | ||||||
|  | DEFAULT_EXTENSIONS = [ | ||||||
|  |     '.py', '.java', '.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.js', '.ts', | ||||||
|  |     '.html', '.css', '.php', '.rb', '.go', '.rs', '.swift', '.kt', '.scala', | ||||||
|  |     '.sh', '.bash', '.pl', '.pm', '.lua', '.sql' | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | # Archivos a ignorar por defecto | ||||||
|  | DEFAULT_IGNORE_PATTERNS = [ | ||||||
|  |     '*.min.js', '*.min.css', '**/node_modules/**', '**/venv/**', '**/.git/**', | ||||||
|  |     '**/__pycache__/**', '**/build/**', '**/dist/**', '**/bin/config/rates/**', | ||||||
|  |     '**/bin/msg/**' | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | def count_lines_of_code(project_path, extensions=None, ignore_patterns=None): | ||||||
|  |     """ | ||||||
|  |     Cuenta las líneas de código no vacías en los archivos con las extensiones especificadas. | ||||||
|  |     Excluye comentarios en archivos Python. | ||||||
|  |      | ||||||
|  |     Args: | ||||||
|  |         project_path: Ruta del directorio del proyecto | ||||||
|  |         extensions: Lista de extensiones de archivo a considerar | ||||||
|  |         ignore_patterns: Lista de patrones de archivo a ignorar | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |         Número total de líneas de código | ||||||
|  |     """ | ||||||
|  |     if extensions is None: | ||||||
|  |         extensions = DEFAULT_EXTENSIONS | ||||||
|  |      | ||||||
|  |     if ignore_patterns is None: | ||||||
|  |         ignore_patterns = DEFAULT_IGNORE_PATTERNS | ||||||
|  |      | ||||||
|  |     total_lines = 0 | ||||||
|  |     files_counted = 0 | ||||||
|  |     skipped_files = 0 | ||||||
|  |      | ||||||
|  |     logger.info(f"Contando líneas de código en: {project_path}") | ||||||
|  |     logger.info(f"Extensiones: {extensions}") | ||||||
|  |      | ||||||
|  |     for root, _, files in os.walk(project_path): | ||||||
|  |         for file in files: | ||||||
|  |             file_path = os.path.join(root, file) | ||||||
|  |             rel_path = os.path.relpath(file_path, project_path) | ||||||
|  |              | ||||||
|  |             # Verificar si el archivo debe ser ignorado | ||||||
|  |             should_ignore = False | ||||||
|  |             for pattern in ignore_patterns: | ||||||
|  |                 if fnmatch.fnmatch(rel_path, pattern): | ||||||
|  |                     should_ignore = True | ||||||
|  |                     break | ||||||
|  |              | ||||||
|  |             if should_ignore: | ||||||
|  |                 logger.debug(f"Ignorando archivo: {rel_path}") | ||||||
|  |                 skipped_files += 1 | ||||||
|  |                 continue | ||||||
|  |              | ||||||
|  |             # Verificar si la extensión está en la lista | ||||||
|  |             file_extension = os.path.splitext(file)[1].lower() | ||||||
|  |             if file_extension not in extensions: | ||||||
|  |                 logger.debug(f"Extensión no considerada: {rel_path}") | ||||||
|  |                 skipped_files += 1 | ||||||
|  |                 continue | ||||||
|  |              | ||||||
|  |             try: | ||||||
|  |                 with open(file_path, 'r', encoding='utf-8') as f: | ||||||
|  |                     lines = f.readlines() | ||||||
|  |                      | ||||||
|  |                     # Contar líneas no vacías (excluyendo comentarios en Python) | ||||||
|  |                     if file_extension == '.py': | ||||||
|  |                         non_empty_lines = 0 | ||||||
|  |                         in_multiline_comment = False | ||||||
|  |                          | ||||||
|  |                         for line in lines: | ||||||
|  |                             line = line.strip() | ||||||
|  |                              | ||||||
|  |                             # Saltar líneas vacías | ||||||
|  |                             if not line: | ||||||
|  |                                 continue | ||||||
|  |                              | ||||||
|  |                             # Manejar comentarios multilinea | ||||||
|  |                             if '"""' in line or "'''" in line: | ||||||
|  |                                 # Si ya estamos en un comentario multilinea, verificamos si termina | ||||||
|  |                                 if in_multiline_comment: | ||||||
|  |                                     if line.endswith('"""') or line.endswith("'''"): | ||||||
|  |                                         in_multiline_comment = False | ||||||
|  |                                     continue | ||||||
|  |                                 # Si encontramos el inicio de un comentario multilinea | ||||||
|  |                                 elif line.startswith('"""') or line.startswith("'''"): | ||||||
|  |                                     # Verificar si el comentario comienza y termina en la misma línea | ||||||
|  |                                     if line.endswith('"""') or line.endswith("'''"): | ||||||
|  |                                         if line.count('"""') == 2 or line.count("'''") == 2: | ||||||
|  |                                             continue | ||||||
|  |                                     else: | ||||||
|  |                                         in_multiline_comment = True | ||||||
|  |                                         continue | ||||||
|  |                              | ||||||
|  |                             # Saltar si estamos en un comentario multilinea | ||||||
|  |                             if in_multiline_comment: | ||||||
|  |                                 continue | ||||||
|  |                              | ||||||
|  |                             # Saltar comentarios de una línea | ||||||
|  |                             if line.startswith('#'): | ||||||
|  |                                 continue | ||||||
|  |                              | ||||||
|  |                             # Contar línea de código válida | ||||||
|  |                             non_empty_lines += 1 | ||||||
|  |                          | ||||||
|  |                         total_lines += non_empty_lines | ||||||
|  |                     else: | ||||||
|  |                         # Para otros lenguajes, simplemente contamos líneas no vacías | ||||||
|  |                         non_empty_lines = sum(1 for line in lines if line.strip()) | ||||||
|  |                         total_lines += non_empty_lines | ||||||
|  |                      | ||||||
|  |                     files_counted += 1 | ||||||
|  |                      | ||||||
|  |                     if files_counted % 100 == 0: | ||||||
|  |                         logger.info(f"Procesados {files_counted} archivos, {total_lines} líneas contadas hasta ahora.") | ||||||
|  |                      | ||||||
|  |             except Exception as e: | ||||||
|  |                 logger.warning(f"Error al procesar archivo {file_path}: {e}") | ||||||
|  |                 skipped_files += 1 | ||||||
|  |      | ||||||
|  |     logger.info(f"Conteo completo. Procesados {files_counted} archivos, ignorados {skipped_files}.") | ||||||
|  |     return total_lines | ||||||
|  | 
 | ||||||
|  | def estimate_cocomo(loc, model_type='organic', cost_per_pm=5000, programmer_type='fullstack'): | ||||||
|  |     """ | ||||||
|  |     Calcula las estimaciones COCOMO basadas en las líneas de código. | ||||||
|  |      | ||||||
|  |     Args: | ||||||
|  |         loc: Líneas de código | ||||||
|  |         model_type: Tipo de modelo COCOMO ('organic', 'semi-detached', 'embedded') | ||||||
|  |         cost_per_pm: Costo por persona-mes en USD | ||||||
|  |         programmer_type: Tipo de programador usado para el cálculo de costo | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |         Dictionary con los resultados del cálculo | ||||||
|  |     """ | ||||||
|  |     # Convertir LOC a KLOC (miles de líneas de código) | ||||||
|  |     kloc = loc / 1000 | ||||||
|  |      | ||||||
|  |     # Obtener coeficientes del modelo | ||||||
|  |     a, b, c, d = COCOMO_MODELS[model_type] | ||||||
|  |      | ||||||
|  |     # Calcular esfuerzo en persona-mes | ||||||
|  |     effort_pm = a * (kloc ** b) | ||||||
|  |      | ||||||
|  |     # Calcular tiempo de desarrollo en meses | ||||||
|  |     dev_time = c * (effort_pm ** d) | ||||||
|  |      | ||||||
|  |     # Calcular personas requeridas (promedio) | ||||||
|  |     avg_staff = effort_pm / dev_time | ||||||
|  |      | ||||||
|  |     # Calcular costo total | ||||||
|  |     total_cost = effort_pm * cost_per_pm | ||||||
|  |      | ||||||
|  |     # Calcular costo por hora (dividir el costo por persona-mes entre las horas laborables) | ||||||
|  |     hourly_rate = cost_per_pm / HOURS_PER_MONTH | ||||||
|  |      | ||||||
|  |     # Redondear valores para presentación | ||||||
|  |     effort_pm = round(effort_pm, 2) | ||||||
|  |     dev_time = round(dev_time, 2) | ||||||
|  |     avg_staff = round(avg_staff, 2) | ||||||
|  |     hourly_rate = round(hourly_rate, 2) | ||||||
|  |     total_cost = round(total_cost, 2) | ||||||
|  |      | ||||||
|  |     # Preparar resultados | ||||||
|  |     results = { | ||||||
|  |         'loc': loc, | ||||||
|  |         'kloc': round(kloc, 2), | ||||||
|  |         'model': model_type, | ||||||
|  |         'effort_pm': effort_pm, | ||||||
|  |         'dev_time': dev_time, | ||||||
|  |         'avg_staff': avg_staff, | ||||||
|  |         'cost_per_pm': cost_per_pm, | ||||||
|  |         'hourly_rate': hourly_rate, | ||||||
|  |         'programmer_type': programmer_type, | ||||||
|  |         'total_cost': total_cost | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return results | ||||||
|  | 
 | ||||||
|  | def show_result(message): | ||||||
|  |     """ | ||||||
|  |     Función para mostrar resultados en la consola. | ||||||
|  |     Solo muestra mensajes si SHOW_RESULTS es True. | ||||||
|  |     """ | ||||||
|  |     if SHOW_RESULTS: | ||||||
|  |         print(message) | ||||||
|  | 
 | ||||||
|  | def determine_cocomo_model(loc): | ||||||
|  |     """ | ||||||
|  |     Determina automáticamente el modelo COCOMO basado en el tamaño del proyecto. | ||||||
|  |      | ||||||
|  |     Args: | ||||||
|  |         loc: Líneas de código | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |         Tipo de modelo COCOMO ('organic', 'semi-detached', 'embedded') | ||||||
|  |     """ | ||||||
|  |     kloc = loc / 1000 | ||||||
|  |      | ||||||
|  |     if kloc < 50: | ||||||
|  |         return 'organic'  # Proyectos pequeños (menos de 50K líneas) | ||||||
|  |     elif kloc < 300: | ||||||
|  |         return 'semi-detached'  # Proyectos medianos (entre 50K y 300K líneas) | ||||||
|  |     else: | ||||||
|  |         return 'embedded'  # Proyectos grandes (más de 300K líneas) | ||||||
|  | 
 | ||||||
|  | def get_available_programmer_types(): | ||||||
|  |     """ | ||||||
|  |     Obtiene una lista de los tipos de programadores disponibles | ||||||
|  |     basados en los archivos .rate existentes en el directorio de tarifas. | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |         List: Lista de tipos de programadores disponibles | ||||||
|  |     """ | ||||||
|  |     if not RATES_DIR.exists(): | ||||||
|  |         logger.warning(f"El directorio de tarifas no existe: {RATES_DIR}") | ||||||
|  |         return ['fullstack']  # Por defecto solo fullstack si no hay directorio | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         # Buscar archivos .rate en el directorio | ||||||
|  |         rate_files = list(RATES_DIR.glob('*.rate')) | ||||||
|  |          | ||||||
|  |         # Extraer nombres de programadores de los nombres de archivo | ||||||
|  |         programmer_types = [file.stem for file in rate_files] | ||||||
|  |          | ||||||
|  |         if not programmer_types: | ||||||
|  |             logger.warning("No se encontraron archivos de tarifas. Usando solo 'fullstack' por defecto.") | ||||||
|  |             return ['fullstack'] | ||||||
|  |          | ||||||
|  |         return sorted(programmer_types) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"Error al buscar tipos de programadores disponibles: {e}") | ||||||
|  |         return ['fullstack']  # En caso de error, devolver tipo por defecto | ||||||
|  | 
 | ||||||
|  | def get_programmer_rate(programmer_type='fullstack'): | ||||||
|  |     """ | ||||||
|  |     Obtiene la tarifa por hora de un tipo específico de programador desde el archivo de configuración. | ||||||
|  |     Si el archivo no existe, devuelve un valor predeterminado según el tipo. | ||||||
|  |      | ||||||
|  |     Args: | ||||||
|  |         programmer_type: Tipo de programador (por defecto: 'fullstack') | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |         Float: Tarifa por hora en USD | ||||||
|  |     """ | ||||||
|  |     rate_file = RATES_DIR / f"{programmer_type}.rate" | ||||||
|  |      | ||||||
|  |     # Valores predeterminados por tipo de programador | ||||||
|  |     default_rates = { | ||||||
|  |         'fullstack': 45.00, | ||||||
|  |         'frontend': 40.00, | ||||||
|  |         'backend': 42.00, | ||||||
|  |         'devops': 50.00, | ||||||
|  |         'python': 35.00, | ||||||
|  |         'java': 40.00, | ||||||
|  |         'php': 30.00, | ||||||
|  |         'mobile': 45.00, | ||||||
|  |         'data': 55.00, | ||||||
|  |         'ml': 65.00, | ||||||
|  |         'cloud': 60.00, | ||||||
|  |         'bash': 20.00 | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     default_rate = default_rates.get(programmer_type, 40.00) | ||||||
|  |      | ||||||
|  |     # Comprobar si existe el archivo | ||||||
|  |     if not rate_file.exists(): | ||||||
|  |         logger.warning(f"No se encontró el archivo de tarifa: {rate_file}") | ||||||
|  |         logger.warning(f"Usando tarifa predeterminada de {default_rate:.2f} USD/hora para {programmer_type}") | ||||||
|  |         return default_rate | ||||||
|  |      | ||||||
|  |     # Leer la tarifa del archivo | ||||||
|  |     try: | ||||||
|  |         with open(rate_file, 'r', encoding='utf-8') as f: | ||||||
|  |             rate = float(f.read().strip()) | ||||||
|  |             logger.info(f"Tarifa leída desde {rate_file}: {rate:.2f} USD/hora") | ||||||
|  |             return rate | ||||||
|  |     except (FileNotFoundError, ValueError, IOError) as e: | ||||||
|  |         logger.warning(f"Error al leer el archivo de tarifa {rate_file}: {e}") | ||||||
|  |         logger.warning(f"Usando tarifa predeterminada de {default_rate:.2f} USD/hora para {programmer_type}") | ||||||
|  |         return default_rate | ||||||
|  | 
 | ||||||
|  | def print_results(results): | ||||||
|  |     """ | ||||||
|  |     Imprime los resultados del cálculo COCOMO de manera formateada. | ||||||
|  |      | ||||||
|  |     Args: | ||||||
|  |         results: Dictionary con los resultados del cálculo COCOMO | ||||||
|  |     """ | ||||||
|  |     print("\n========== RESULTADOS DEL ANÁLISIS COCOMO ==========") | ||||||
|  |     print(f"Proyecto: {results['project_path']}") | ||||||
|  |     print(f"Líneas de código: {results['loc']} ({results['kloc']} KLOC)") | ||||||
|  |     print(f"Modelo COCOMO: {results['model'].upper()}") | ||||||
|  |     print("\n--- Estimaciones ---") | ||||||
|  |     print(f"Esfuerzo: {results['effort_pm']} persona-meses") | ||||||
|  |     print(f"Tiempo de desarrollo: {results['dev_time']} meses") | ||||||
|  |     print(f"Personal promedio: {results['avg_staff']} personas") | ||||||
|  |     print("\n--- Costos ---") | ||||||
|  |     print(f"Tipo de programador: {results['programmer_type']}") | ||||||
|  |     print(f"Costo por hora: ${results['hourly_rate']}") | ||||||
|  |     print(f"Costo por persona-mes: ${results['cost_per_pm']} ({HOURS_PER_MONTH} horas)") | ||||||
|  |     print(f"Costo total estimado: ${results['total_cost']:,.2f}") | ||||||
|  |     print("=====================================================\n") | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     # Obtener los tipos de programadores disponibles para el parámetro --type | ||||||
|  |     available_types = get_available_programmer_types() | ||||||
|  |     available_types_str = ', '.join(available_types) | ||||||
|  |      | ||||||
|  |     # Parámetros de línea de comandos | ||||||
|  |     parser = argparse.ArgumentParser(description='Calculadora COCOMO para estimar costos de proyectos de software') | ||||||
|  |     parser.add_argument('--project', required=True, help='Ruta del directorio del proyecto a analizar') | ||||||
|  |     parser.add_argument('--model', choices=['organic', 'semi-detached', 'embedded', 'auto'],  | ||||||
|  |                       default='auto', help='Tipo de modelo COCOMO a utilizar (por defecto: auto)') | ||||||
|  |     parser.add_argument('--type', choices=available_types, default='fullstack', | ||||||
|  |                       help=f'Tipo de programador para calcular costos (por defecto: fullstack). Disponibles: {available_types_str}') | ||||||
|  |     parser.add_argument('--cost', type=float,  | ||||||
|  |                       help='Costo por persona-mes en USD (por defecto: tarifa del tipo de programador × 176)') | ||||||
|  |     parser.add_argument('--ignore', action='append',  | ||||||
|  |                       help='Patrones adicionales de archivos a ignorar') | ||||||
|  |     parser.add_argument('--ext', action='append',  | ||||||
|  |                       help='Extensiones adicionales de archivo a considerar') | ||||||
|  |     parser.add_argument('-q', '--quiet', action='store_true',  | ||||||
|  |                       help='No mostrar resultados detallados') | ||||||
|  |     parser.add_argument('-v', '--verbose', action='store_true',  | ||||||
|  |                       help='Mostrar información detallada del proceso') | ||||||
|  |      | ||||||
|  |     args = parser.parse_args() | ||||||
|  |      | ||||||
|  |     # Configurar nivel de log según parámetros | ||||||
|  |     if args.verbose: | ||||||
|  |         console_handler.setLevel(logging.INFO) | ||||||
|  |      | ||||||
|  |     # Configurar si se muestran resultados | ||||||
|  |     SHOW_RESULTS = not args.quiet | ||||||
|  |      | ||||||
|  |     # Verificar que el directorio del proyecto existe | ||||||
|  |     project_path = Path(args.project) | ||||||
|  |     if not project_path.exists() or not project_path.is_dir(): | ||||||
|  |         logger.error(f"El directorio del proyecto no existe: {args.project}") | ||||||
|  |         print(f"ERROR: El directorio del proyecto no existe: {args.project}") | ||||||
|  |         sys.exit(1) | ||||||
|  |      | ||||||
|  |     # Verificar que el directorio de tarifas existe | ||||||
|  |     if not RATES_DIR.exists(): | ||||||
|  |         logger.warning(f"El directorio de tarifas no existe: {RATES_DIR}") | ||||||
|  |         logger.warning("Se creará el directorio y se usarán valores predeterminados.") | ||||||
|  |         try: | ||||||
|  |             os.makedirs(RATES_DIR, exist_ok=True) | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"Error al crear el directorio de tarifas: {e}") | ||||||
|  |      | ||||||
|  |     # Obtener tipo de programador y su tarifa | ||||||
|  |     programmer_type = args.type  # Usar el tipo especificado por el usuario | ||||||
|  |      | ||||||
|  |     # Obtener costo por hora desde el archivo .rate y convertir a costo por persona-mes | ||||||
|  |     if args.cost is None: | ||||||
|  |         # Obtener la tarifa por hora y multiplicar por las horas mensuales | ||||||
|  |         hourly_rate = get_programmer_rate(programmer_type) | ||||||
|  |         cost_per_pm = hourly_rate * HOURS_PER_MONTH | ||||||
|  |         logger.info(f"Costo por hora ({programmer_type}): ${hourly_rate:.2f} × {HOURS_PER_MONTH} horas = ${cost_per_pm:.2f} por persona-mes") | ||||||
|  |     else: | ||||||
|  |         cost_per_pm = args.cost | ||||||
|  |         programmer_type = f"{programmer_type} (personalizado)"  # Indicar que es un valor personalizado | ||||||
|  |         hourly_rate = cost_per_pm / HOURS_PER_MONTH | ||||||
|  |         logger.info(f"Usando costo por persona-mes especificado: ${cost_per_pm:.2f} (${hourly_rate:.2f}/hora)") | ||||||
|  |      | ||||||
|  |     # Preparar extensiones y patrones de ignorar | ||||||
|  |     extensions = DEFAULT_EXTENSIONS.copy() | ||||||
|  |     if args.ext: | ||||||
|  |         extensions.extend([ext if ext.startswith('.') else f'.{ext}' for ext in args.ext]) | ||||||
|  |      | ||||||
|  |     ignore_patterns = DEFAULT_IGNORE_PATTERNS.copy() | ||||||
|  |     if args.ignore: | ||||||
|  |         ignore_patterns.extend(args.ignore) | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         # Contar líneas de código | ||||||
|  |         loc = count_lines_of_code(project_path, extensions, ignore_patterns) | ||||||
|  |          | ||||||
|  |         # Determinar el modelo COCOMO a utilizar | ||||||
|  |         if args.model == 'auto': | ||||||
|  |             model_type = determine_cocomo_model(loc) | ||||||
|  |             logger.info(f"Modelo seleccionado automáticamente: {model_type}") | ||||||
|  |         else: | ||||||
|  |             model_type = args.model | ||||||
|  |          | ||||||
|  |         # Calcular estimaciones COCOMO | ||||||
|  |         results = estimate_cocomo(loc, model_type, cost_per_pm, programmer_type) | ||||||
|  |          | ||||||
|  |         # Añadir ruta del proyecto para la presentación | ||||||
|  |         results['project_path'] = str(project_path) | ||||||
|  |          | ||||||
|  |         # Mostrar resultados | ||||||
|  |         print_results(results) | ||||||
|  |          | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"Error durante el cálculo COCOMO: {e}") | ||||||
|  |         print(f"ERROR: {e}") | ||||||
|  |         sys.exit(1) | ||||||
		Loading…
	
		Reference in a new issue