CORRECCIÓN: arreglar excepciones silenciosas y limpiar scripts
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user