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