CORRECCIÓN: arreglar excepciones silenciosas y limpiar scripts

This commit is contained in:
Nachou-u
2026-09-09 08:28:09 -03:00
parent 5d9e0f4589
commit 29c447c23e
11 changed files with 7 additions and 4 deletions
+16
View File
@@ -0,0 +1,16 @@
import re
with open('docker-compose.yml', 'r') as f:
content = f.read()
new_service = """ tax-api:
build: ./tax_api
ports:
- "8001:8000"
restart: unless-stopped
celery:"""
content = content.replace(' celery:', new_service)
with open('docker-compose.yml', 'w') as f:
f.write(content)
+98
View File
@@ -0,0 +1,98 @@
import re
with open('templates/expenses/expense_form.html', 'r', encoding='utf-8') as f:
content = f.read()
# 1. Add x-data
content = content.replace('<form method="post" enctype="multipart/form-data">', '<form method="post" enctype="multipart/form-data" x-data="expenseForm()">')
# 2. Add x-show to responsible person container
content = content.replace('<div id="responsible-person-container"', '<div id="responsible-person-container" x-show="isReimbursement"')
# 3. Add x-show to new vendor fields
# We need to bind the toggle-new-vendor checkbox
content = content.replace('<input type="checkbox" id="toggle-new-vendor"', '<input type="checkbox" id="toggle-new-vendor" x-model="isNewVendor"')
content = content.replace('<div id="new-vendor-fields"', '<div id="new-vendor-fields" x-show="isNewVendor"')
content = content.replace('style="display: none;', 'style="display: none;" x-cloak ')
# Add x-cloak style to head or just use x-show directly
# Actually replacing the display: none might be tricky, let's just do x-show.
content = content.replace('<div id="new-vendor-fields" style="display: none;', '<div id="new-vendor-fields" x-cloak x-show="isNewVendor" style="')
content = content.replace('<div id="responsible-person-container" x-show="isReimbursement" style="margin-top:0.5rem;">', '<div id="responsible-person-container" x-cloak x-show="isReimbursement" style="margin-top:0.5rem;">')
# Replace the giant vanilla JS <script> with Alpine
# The original script starts at <script> around line 242 and ends at </script> around line 875
script_pattern = re.compile(r'<script>\s*document\.querySelectorAll.*?</script>', re.DOTALL)
alpine_script = """
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('expenseForm', () => ({
isReimbursement: document.getElementById('id_is_reimbursement')?.checked || false,
isNewVendor: false,
amountClp: document.getElementById('id_amount_clp')?.value || '',
taxType: document.getElementById('id_tax_type')?.value || 'EXENTO',
taxAmountClp: document.getElementById('id_tax_amount_clp')?.value || '0',
totalAmountClp: document.getElementById('id_total_amount_clp')?.value || '0',
newVendorRut: '',
rutError: false,
formatAmount() {
let cleanVal = this.amountClp.replace(/\./g, '').replace(/\D/g, '');
if (cleanVal) {
this.amountClp = cleanVal.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ".");
} else {
this.amountClp = '';
}
},
async calculateTax() {
let cleanVal = this.amountClp.replace(/\./g, '');
let amount = parseFloat(cleanVal) || 0;
if (amount > 0 && this.taxType) {
try {
const response = await fetch(`/api/v1/sii/calculate-tax?amount=${amount}&tax_type=${this.taxType}`);
const data = await response.json();
this.taxAmountClp = data.tax_amount.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ".");
this.totalAmountClp = data.total_amount.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ".");
} catch (e) {
console.error('Error calculating tax', e);
}
} else {
this.taxAmountClp = '0';
this.totalAmountClp = '0';
}
},
async validateRut() {
if (!this.newVendorRut) {
this.rutError = false;
return;
}
try {
const response = await fetch(`/api/v1/sii/validate-rut?rut=${this.newVendorRut}`);
const data = await response.json();
if (data.is_valid) {
this.newVendorRut = data.formatted_rut;
this.rutError = false;
} else {
this.rutError = true;
alert(data.error_message || "RUT inválido");
}
} catch (e) {
console.error('Error validating RUT', e);
}
}
}));
});
</script>
<style>
[x-cloak] { display: none !important; }
</style>
"""
content = script_pattern.sub(alpine_script, content)
with open('templates/expenses/expense_form.html', 'w', encoding='utf-8') as f:
f.write(content)
+88
View File
@@ -0,0 +1,88 @@
import re
with open('expenses/models.py', 'r', encoding='utf-8') as f:
content = f.read()
# Add TimeStampedModel and CatalogModel right before Project
abstracts = """
from django.core.exceptions import ValidationError
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Creación")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Última Modificación")
class Meta:
abstract = True
class CatalogModel(TimeStampedModel):
code = models.CharField(max_length=50, unique=True, verbose_name="Código")
name = models.CharField(max_length=200, verbose_name="Nombre")
description = models.TextField(blank=True, verbose_name="Descripción")
class Meta:
abstract = True
"""
if 'class TimeStampedModel' not in content:
content = content.replace("class Project(models.Model):", abstracts + "class Project(CatalogModel):")
# Clean Project
content = content.replace(" code = models.CharField(max_length=50, unique=True, verbose_name=\"Código\", default='PROY-000')\n", "")
content = content.replace(" name = models.CharField(max_length=200, verbose_name=\"Nombre del Proyecto/Centro\")\n", "")
content = content.replace(" description = models.TextField(blank=True, verbose_name=\"Descripción\")\n", "")
content = content.replace(" created_at = models.DateTimeField(auto_now_add=True, verbose_name=\"Fecha de Creación\")\n", "")
content = content.replace(" updated_at = models.DateTimeField(auto_now=True, verbose_name=\"Última Modificación\")\n", "")
# Clean Category
content = content.replace("class Category(models.Model):", "class Category(CatalogModel):")
content = content.replace(" code = models.CharField(max_length=10, unique=True, verbose_name=\"Código de Categoría\")\n", "")
content = content.replace(" name = models.CharField(max_length=100, verbose_name=\"Nombre de Categoría\")\n", "")
content = content.replace(" description = models.TextField(blank=True, verbose_name=\"Descripción\")\n", "")
# Clean Vendor
content = content.replace("class Vendor(models.Model):", "class Vendor(TimeStampedModel):")
content = content.replace(" created_at = models.DateTimeField(auto_now_add=True)\n updated_at = models.DateTimeField(auto_now=True)\n", "")
# Clean Expense
content = content.replace("class Expense(models.Model):", "class Expense(TimeStampedModel):")
# We must only replace the first occurrence of these dates inside Expense, but replace is global.
# So we do a regex to target Expense block.
# Actually Expense is the only one with exactly these lines together at the end.
content = re.sub(r' created_at = models.DateTimeField\(auto_now_add=True\)\n updated_at = models.DateTimeField\(auto_now=True\)\n\n def save', ' def save', content)
encap = """
def clean(self):
super().clean()
if self.amount_clp and self.amount_clp < 0:
raise ValidationError({'amount_clp': 'El monto neto no puede ser negativo.'})
amount = Decimal(str(self.amount_clp or '0'))
tax = Decimal(str(self.tax_amount_clp or '0'))
expected = amount + tax
if self.total_amount_clp and abs(self.total_amount_clp - expected) > Decimal('1.0'):
raise ValidationError({'total_amount_clp': f'Discrepancia. Total esperado: {expected}'})
def approve_expense(self, user):
if self.approval_status == ApprovalStatusEnum.EN_REVISION:
self.approval_status = ApprovalStatusEnum.SOLICITADO_FINANZAS
self.elevated_by = user
self.save()
def save(self, *args, **kwargs):"""
content = content.replace(" def save(self, *args, **kwargs):", encap)
proxy = """
class Reimbursement(Expense):
class Meta:
proxy = True
verbose_name = "Rendición Especializada"
verbose_name_plural = "Rendiciones Especializadas"
def get_approval_flow(self):
return "Flujo Acelerado de Reembolso"
"""
if 'class Reimbursement' not in content:
content += proxy
with open('expenses/models.py', 'w', encoding='utf-8') as f:
f.write(content)
+42
View File
@@ -0,0 +1,42 @@
import re
with open('config/settings.py', 'r') as f:
content = f.read()
# Replace CHANNEL_LAYERS
old_channels = """CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [env('REDIS_URL', default='redis://localhost:6379/0')],
},
},
}"""
new_channels = """REDIS_URL = env('REDIS_URL', default=None)
if REDIS_URL:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [REDIS_URL],
},
},
}
else:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}"""
content = content.replace(old_channels, new_channels)
# Replace Celery defaults so they don't crash if REDIS_URL is None
old_celery1 = "CELERY_BROKER_URL = env('REDIS_URL', default='redis://localhost:6379/0')"
new_celery1 = "CELERY_BROKER_URL = env('REDIS_URL', default='memory://')"
old_celery2 = "CELERY_RESULT_BACKEND = env('REDIS_URL', default='redis://localhost:6379/0')"
new_celery2 = "CELERY_RESULT_BACKEND = env('REDIS_URL', default='cache+memory://')"
content = content.replace(old_celery1, new_celery1)
content = content.replace(old_celery2, new_celery2)
with open('config/settings.py', 'w') as f:
f.write(content)
+17
View File
@@ -0,0 +1,17 @@
with open('expenses/views.py', 'r', encoding='utf-8') as f:
content = f.read()
# Add transaction and cache imports
if 'from django.db import transaction' not in content:
content = content.replace("from django.db.models import Sum, Count, F", "from django.db.models import Sum, Count, F\nfrom django.db import transaction")
if 'from django.views.decorators.cache import cache_page' not in content:
content = content.replace("from django.contrib.auth.decorators import login_required", "from django.contrib.auth.decorators import login_required\nfrom django.views.decorators.cache import cache_page")
# Decorate dashboard with cache_page
content = content.replace("@login_required\ndef dashboard(request):", "@login_required\n@cache_page(60 * 5) # Cache 5 minutos (Disponibilidad)\ndef dashboard(request):")
# Add @transaction.atomic to expense_create
content = content.replace("@login_required\ndef expense_create(request):", "@login_required\n@transaction.atomic # Integridad de datos\ndef expense_create(request):")
with open('expenses/views.py', 'w', encoding='utf-8') as f:
f.write(content)
+22
View File
@@ -0,0 +1,22 @@
import re
with open('config/settings.py', 'r') as f:
content = f.read()
middleware = """MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
"""
content = re.sub(r"MIDDLEWARE = \[\n\s+'django\.middleware\.security\.SecurityMiddleware',\n", middleware, content)
static_root = """STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
"""
content = re.sub(r"STATIC_URL = 'static/'\n", static_root, content)
with open('config/settings.py', 'w') as f:
f.write(content)
with open('requirements.txt', 'a') as f:
f.write('\nwhitenoise==6.7.0\n')
+38
View File
@@ -0,0 +1,38 @@
import subprocess
import sys
import os
scripts = [
'setup_roles.py',
'seed_categories.py',
'seed_projects.py',
'seed_specialties.py'
]
def run_all():
print("🚀 Iniciando Sembrado Maestro (Master Seed) de EFINTEL ERP...")
# Get the directory of this script
base_dir = os.path.dirname(os.path.abspath(__file__))
for script in scripts:
script_path = os.path.join(base_dir, script)
print(f"\n--------------------------------------------------")
print(f"👉 Ejecutando: {script}...")
print(f"--------------------------------------------------")
# Run script with current python interpreter
result = subprocess.run([sys.executable, script_path], check=False)
if result.returncode != 0:
print(f"\n❌ Error al ejecutar {script}. Sembrado interrumpido.")
sys.exit(1)
print("\n==================================================")
print("✅ ¡Sembrado Maestro Completado Exitosamente!")
print("Las categorías, portafolios, proyectos, roles y")
print("especialistas coinciden al 100% con tu entorno actual.")
print("==================================================")
if __name__ == "__main__":
run_all()
+99
View File
@@ -0,0 +1,99 @@
import os
import django
import sys
# Set up Django environment
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from expenses.models import Category
def run_seed():
print("Seeding professional CBS categories and Chart of Accounts...")
# Define the complete EFINTEL CBS structure
cbs_structure = {
"1000": {
"name": "Recursos Humanos y Especialidades I+D+i",
"children": {
"1100": "Personal Staff Core (Fijo / Sueldos Base)",
"1201": "Electricidad de Potencia e Infraestructura",
"1202": "Automatización y Control Industrial",
"1203": "Domótica e Inmótica (BMS & Climatización)",
"1204": "Electrónica y Sistemas de Hardware (PCB / Firmware)",
"1205": "Redes, Conectividad e Informática (TI & Ciberseguridad)",
"1206": "Tecnologías Inmersivas y Gemelos Digitales (VR/AR/LiDAR)",
"1207": "Gestión Administrativa, Dir. de Proyectos (PMP/Agile)"
}
},
"2000": {
"name": "Materiales, Insumos e Infraestructura de Trabajo",
"children": {
"2100": "Componentes Electrónicos Prototipado",
"2200": "Equipamiento Industrial de Campo",
"2300": "Materiales Eléctricos y de Montaje",
"2301": "Arriendo de Oficinas y Espacios de Trabajo",
"2400": "Insumos de Oficina, Aseo, Cafetería y Papelería"
}
},
"3000": {
"name": "Logística y Desplazamiento Terreno",
"children": {
"3101": "Combustibles / Bencina",
"3102": "Arriendo Vehículos 4x4",
"3103": "Pasajes de Bus Interurbano",
"3104": "Pasajes de Micro / Urbano",
"3105": "Pasajes de Colectivos / Taxis",
"3106": "Uber / Cabify / Didi",
"3107": "Peajes & Tag Nacionales",
"3108": "Reembolso Kilometraje Particular",
"3200": "Viáticos Alimentación y Alojamiento Terreno"
}
},
"4000": {
"name": "Software, Herramientas Digitales y Formación",
"children": {
"4100": "Licencias de Ingeniería & Diseño (Altium/Unity/Unreal)",
"4200": "Infraestructura y Servidores Cloud Tradicionales",
"4201": "Cursos, Capacitación y Certificaciones Técnicas para Especialistas",
"4300": "Herramientas de Gestión de Proyectos / Software General"
}
},
"7000": {
"name": "Software, Datos, Inteligencia Artificial y Cloud",
"children": {
"7100": "Licencias IA y Modelos Propietarios",
"7200": "Cloud & Computación GPU de Alto Rendimiento",
"7300": "Consumo de APIs LLM (OpenAI, Anthropic, Gemini)",
"7400": "MLOps, Pipelines de Datos y Despliegue de Modelos"
}
}
}
# Create Parents and Children
for p_code, p_data in cbs_structure.items():
parent_cat, _ = Category.objects.get_or_create(
code=p_code,
defaults={'name': p_data['name'], 'description': 'Partida Principal (Macro-categoría)'}
)
if parent_cat.name != p_data['name']:
parent_cat.name = p_data['name']
parent_cat.save()
print(f"Parent: {parent_cat.code} - {parent_cat.name}")
for c_code, c_name in p_data['children'].items():
child_cat, _ = Category.objects.get_or_create(
code=c_code,
defaults={'name': c_name, 'parent': parent_cat}
)
if child_cat.name != c_name or child_cat.parent != parent_cat:
child_cat.name = c_name
child_cat.parent = parent_cat
child_cat.save()
print(f" Child: {child_cat.code} - {child_cat.name}")
print("✅ CBS Seeding completed successfully.")
if __name__ == "__main__":
run_seed()
+103
View File
@@ -0,0 +1,103 @@
import os
import django
import sys
# Set up Django environment
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from expenses.models import Project
def run_seed():
print("Clearing existing projects to ensure clean portfolio seeding...")
# This might cascade to expenses, which is expected for seeding a clean MVP/dev environment
Project.objects.all().delete()
print("Seeding Root Cost Centers, Portfolios and Projects...")
# 1. Create Root Cost Centers (Level 1)
cc_idi = Project.objects.create(
code="CC-IDI",
name="Centro de Costo I+D+i",
description="Centro de costo principal para Investigación, Desarrollo e Innovación tecnológica.",
assigned_budget_clp=350000000.00,
parent=None
)
cc_adf = Project.objects.create(
code="CC-ADF",
name="Centro de Costo Administración y Finanzas",
description="Centro de costo corporativo para Administración, Finanzas y Overhead general.",
assigned_budget_clp=150000000.00,
parent=None
)
print(f"Created Root Cost Center: {cc_idi.code} - {cc_idi.name}")
print(f"Created Root Cost Center: {cc_adf.code} - {cc_adf.name}")
# 2. Portfolio definitions (Level 2) - under CC-IDI
portfolio_def = {
"Portafolio 1": 5,
"Portafolio 2": 4,
"Portafolio 3": 3,
"Portafolio 4": 1,
"Portafolio 5": 1,
"Portafolio 6": 1,
"Portafolio 7": 2,
}
for index, (port_name, num_projects) in enumerate(portfolio_def.items(), 1):
port_code = f"PORT-{index:03d}"
# Create portfolio under CC-IDI
portfolio = Project.objects.create(
code=port_code,
name=port_name,
description=f"Portafolio tecnológico para agrupar {num_projects} proyectos.",
assigned_budget_clp=50000000.00,
parent=cc_idi
)
print(f" Created Portfolio: {portfolio.code} - {portfolio.name} (under CC-IDI)")
# Create subprojects (Level 3)
for sub_idx in range(1, num_projects + 1):
sub_code = f"PRY-{index}{sub_idx:02d}"
subproject = Project.objects.create(
code=sub_code,
name=f"Proyecto {sub_idx} ({port_name})",
description=f"Subproyecto contable imputable de {port_name}.",
assigned_budget_clp=10000000.00,
parent=portfolio
)
print(f" Created Project: {subproject.code} - {subproject.name}")
# 3. Create Portfolios and Projects under CC-ADF
port_adm = Project.objects.create(
code="PORT-008",
name="Operaciones y Finanzas",
description="Portafolio administrativo para gastos fijos, soporte y legal.",
assigned_budget_clp=80000000.00,
parent=cc_adf
)
print(f" Created Portfolio: {port_adm.code} - {port_adm.name} (under CC-ADF)")
pry_adm = Project.objects.create(
code="PRY-801",
name="Sueldos y Overhead Administrativo",
description="Gasto operacional fijo de oficina, contabilidad y sueldos staff core.",
assigned_budget_clp=40000000.00,
parent=port_adm
)
pry_fin = Project.objects.create(
code="PRY-802",
name="Servicios Financieros y Legal",
description="Gastos de auditoría externa, patentes y asesoría legal B2B.",
assigned_budget_clp=40000000.00,
parent=port_adm
)
print(f" Created Project: {pry_adm.code} - {pry_adm.name}")
print(f" Created Project: {pry_fin.code} - {pry_fin.name}")
print("✅ Root Cost Centers, Portfolios and Projects seeding completed successfully.")
if __name__ == "__main__":
run_seed()
+63
View File
@@ -0,0 +1,63 @@
import os
import django
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.contrib.auth.models import User
from expenses.models import Project, Category, UserProfile
def seed():
print("Seeding project categories and user specialties...")
# Load categories
cat_1201 = Category.objects.filter(code="1201").first()
cat_1202 = Category.objects.filter(code="1202").first()
cat_1203 = Category.objects.filter(code="1203").first()
cat_1204 = Category.objects.filter(code="1204").first()
cat_1205 = Category.objects.filter(code="1205").first()
# Link categories to some projects
projs = Project.objects.filter(parent__isnull=False)
if projs.exists():
# Set category on a few subprojects
mapping = {
"PRY-101": (cat_1201, "ACTIVO"),
"PRY-102": (cat_1202, "PAUSADA"),
"PRY-103": (cat_1203, "ACTIVO"),
"PRY-104": (cat_1204, "TERMINADO"),
"PRY-201": (cat_1201, "ACTIVO"),
"PRY-202": (cat_1202, "ACTIVO"),
"PRY-301": (cat_1205, "PAUSADA"),
}
for code, (cat, status) in mapping.items():
proj = Project.objects.filter(code=code).first()
if proj and cat:
proj.category = cat
proj.status = status
proj.save()
print(f"Set Project {proj.code} Category to {cat.code} and Status to {status}")
# Set specialties on users
miguel = User.objects.filter(username="miguel_contreras").first()
ginevra = User.objects.filter(username="ginevra_ortega").first()
admin = User.objects.filter(username="admin").first()
if miguel and hasattr(miguel, 'profile'):
miguel.profile.specialties.set([cat_1201, cat_1202])
print(f"Set miguel_contreras specialties to 1201, 1202")
if ginevra and hasattr(ginevra, 'profile'):
ginevra.profile.specialties.set([cat_1202, cat_1203, cat_1204])
print(f"Set ginevra_ortega specialties to 1202, 1203, 1204")
if admin and hasattr(admin, 'profile'):
admin.profile.specialties.set([cat_1205])
print(f"Set admin specialties to 1205")
print("✅ Project categories and user specialties seeding completed.")
if __name__ == "__main__":
seed()