refactor: migrar a supabase, aplicar cia y poo a gastos
This commit is contained in:
+14
-2
@@ -10,6 +10,18 @@ ALLOWED_HOSTS=*
|
|||||||
# DATABASE_URL=
|
# DATABASE_URL=
|
||||||
# REDIS_URL=
|
# REDIS_URL=
|
||||||
|
|
||||||
# === CONFIGURACIÓN DOCKER (Descomentar para Producción/Docker) ===
|
# === CONFIGURACIÓN BASE DE DATOS (Supabase Postgres) ===
|
||||||
# DATABASE_URL=postgres://efintel_user:efintel_password@db:5432/efintel_db
|
# Formato: postgres://<user>:<password>@<host>:<port>/<dbname>
|
||||||
|
# DATABASE_URL=
|
||||||
|
|
||||||
|
# === CONFIGURACIÓN STORAGE (Supabase S3) ===
|
||||||
|
# Descomentar y completar para almacenar archivos en Supabase
|
||||||
|
# USE_S3=True
|
||||||
|
# AWS_ACCESS_KEY_ID=
|
||||||
|
# AWS_SECRET_ACCESS_KEY=
|
||||||
|
# AWS_STORAGE_BUCKET_NAME=
|
||||||
|
# AWS_S3_ENDPOINT_URL=
|
||||||
|
# AWS_S3_REGION_NAME=
|
||||||
|
|
||||||
|
# === CONFIGURACIÓN DOCKER (Redis) ===
|
||||||
# REDIS_URL=redis://redis:6379/0
|
# REDIS_URL=redis://redis:6379/0
|
||||||
|
|||||||
@@ -194,3 +194,29 @@ Usuarios: `admin` o `ginevra_ortega`
|
|||||||
- **Miguel Angel Contreras Orozco** — Jefe de Área I+D+i
|
- **Miguel Angel Contreras Orozco** — Jefe de Área I+D+i
|
||||||
- **Ginevra Ortega Acuña** — Encargada de Contratos y Auditoría Documental
|
- **Ginevra Ortega Acuña** — Encargada de Contratos y Auditoría Documental
|
||||||
- **Samuel Abdias Contreras Orozco** — Encargado de Desarrollo Comercial
|
- **Samuel Abdias Contreras Orozco** — Encargado de Desarrollo Comercial
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estándar de Control de Versiones
|
||||||
|
|
||||||
|
Este proyecto utiliza **GitHub Flow** y **Conventional Commits** para mantener un historial limpio y trazabilidad.
|
||||||
|
|
||||||
|
### 1. Flujo de Trabajo (GitHub Flow)
|
||||||
|
- La rama `main` contiene el código estable y de producción.
|
||||||
|
- Para cada nueva funcionalidad o corrección, se crea una rama nueva a partir de `main` (ej: `feature/agregar-modulo-nomina`).
|
||||||
|
- Una vez terminado el desarrollo en la rama, se realiza un Pull Request hacia `main`.
|
||||||
|
|
||||||
|
### 2. Formato de Commits (Conventional Commits)
|
||||||
|
Los mensajes de commit deben seguir esta estructura:
|
||||||
|
`<tipo>(<alcance opcional>): <descripción breve>`
|
||||||
|
|
||||||
|
**Tipos permitidos:**
|
||||||
|
- `feat:` Una nueva funcionalidad.
|
||||||
|
- `fix:` Corrección de un error (bug).
|
||||||
|
- `refactor:` Cambios en el código que no corrigen errores ni añaden funcionalidades (ej. aplicar POO).
|
||||||
|
- `docs:` Cambios en la documentación.
|
||||||
|
- `chore:` Tareas de mantenimiento, actualización de dependencias.
|
||||||
|
- `style:` Cambios de formato (espacios, comas, etc) que no afectan la lógica.
|
||||||
|
- `test:` Agregar o modificar pruebas.
|
||||||
|
|
||||||
|
*Ejemplo:* `feat(gastos): agregar soporte multidivisa para proveedores`
|
||||||
|
|||||||
+22
-1
@@ -101,7 +101,7 @@ CHANNEL_LAYERS = {
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': env.db('DATABASE_URL', default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}")
|
'default': env.db('DATABASE_URL', default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}")
|
||||||
}
|
}
|
||||||
|
DATABASES['default']['CONN_MAX_AGE'] = env.int('CONN_MAX_AGE', default=60)
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
|
||||||
@@ -143,9 +143,30 @@ STATIC_URL = '/static/'
|
|||||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
|
|
||||||
# Media files (Uploads)
|
# Media files (Uploads)
|
||||||
|
if env.bool('USE_S3', default=False):
|
||||||
|
# Supabase / AWS S3 Config
|
||||||
|
AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID')
|
||||||
|
AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY')
|
||||||
|
AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME')
|
||||||
|
AWS_S3_ENDPOINT_URL = env('AWS_S3_ENDPOINT_URL', default=None)
|
||||||
|
AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME', default=None)
|
||||||
|
AWS_DEFAULT_ACL = 'public-read'
|
||||||
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
|
||||||
|
MEDIA_URL = f'{AWS_S3_ENDPOINT_URL}/{AWS_STORAGE_BUCKET_NAME}/' if AWS_S3_ENDPOINT_URL else f'https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/'
|
||||||
|
else:
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = BASE_DIR / 'media'
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
# Security (Tríada CIA: Confidencialidad e Integridad)
|
||||||
|
if not DEBUG:
|
||||||
|
SECURE_SSL_REDIRECT = env.bool('SECURE_SSL_REDIRECT', default=True)
|
||||||
|
SESSION_COOKIE_SECURE = env.bool('SESSION_COOKIE_SECURE', default=True)
|
||||||
|
CSRF_COOKIE_SECURE = env.bool('CSRF_COOKIE_SECURE', default=True)
|
||||||
|
SECURE_HSTS_SECONDS = env.int('SECURE_HSTS_SECONDS', default=31536000)
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool('SECURE_HSTS_INCLUDE_SUBDOMAINS', default=True)
|
||||||
|
SECURE_HSTS_PRELOAD = env.bool('SECURE_HSTS_PRELOAD', default=True)
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
LOGIN_URL = '/login/'
|
LOGIN_URL = '/login/'
|
||||||
LOGIN_REDIRECT_URL = '/'
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
class RoleRequiredMixin(UserPassesTestMixin):
|
||||||
|
"""
|
||||||
|
Mixin para validar si el usuario autenticado pertenece a alguno de los grupos (roles) permitidos.
|
||||||
|
"""
|
||||||
|
allowed_roles = []
|
||||||
|
redirect_url = 'dashboard'
|
||||||
|
permission_denied_message = 'No tienes permisos para acceder a esta página.'
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
if self.request.user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
user_groups = self.request.user.groups.values_list('name', flat=True)
|
||||||
|
for role in self.allowed_roles:
|
||||||
|
if role in user_groups:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_no_permission(self):
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return redirect('login')
|
||||||
|
messages.error(self.request, self.permission_denied_message)
|
||||||
|
return redirect(self.redirect_url)
|
||||||
+2
-2
@@ -6,8 +6,8 @@ urlpatterns = [
|
|||||||
path('logout/', views.logout_view, name='logout'),
|
path('logout/', views.logout_view, name='logout'),
|
||||||
|
|
||||||
path('', views.dashboard, name='dashboard'),
|
path('', views.dashboard, name='dashboard'),
|
||||||
path('gastos/', views.expense_list, name='expense_list'),
|
path('gastos/', views.ExpenseListView.as_view(), name='expense_list'),
|
||||||
path('gastos/nuevo/', views.expense_create, name='expense_create'),
|
path('gastos/nuevo/', views.ExpenseCreateView.as_view(), name='expense_create'),
|
||||||
path('gastos/<int:pk>/editar/', views.expense_edit, name='expense_edit'),
|
path('gastos/<int:pk>/editar/', views.expense_edit, name='expense_edit'),
|
||||||
path('gastos/<int:pk>/eliminar/', views.expense_delete, name='expense_delete'),
|
path('gastos/<int:pk>/eliminar/', views.expense_delete, name='expense_delete'),
|
||||||
path('gastos/documento/<int:pk>/eliminar/', views.delete_document, name='delete_document'),
|
path('gastos/documento/<int:pk>/eliminar/', views.delete_document, name='delete_document'),
|
||||||
|
|||||||
+45
-38
@@ -1,6 +1,11 @@
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from .mixins import RoleRequiredMixin
|
||||||
|
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
@@ -96,39 +101,49 @@ def dashboard(request):
|
|||||||
return render(request, 'expenses/dashboard.html', context)
|
return render(request, 'expenses/dashboard.html', context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class ExpenseListView(LoginRequiredMixin, ListView):
|
||||||
def expense_list(request):
|
model = Expense
|
||||||
expenses = Expense.objects.select_related('project', 'category', 'user').order_by('-date')
|
template_name = 'expenses/expense_list.html'
|
||||||
|
context_object_name = 'expenses'
|
||||||
|
|
||||||
# Filtros
|
def get_queryset(self):
|
||||||
project_id = request.GET.get('project')
|
queryset = super().get_queryset().select_related('project', 'category', 'user').order_by('-date')
|
||||||
category_id = request.GET.get('category')
|
project_id = self.request.GET.get('project')
|
||||||
|
category_id = self.request.GET.get('category')
|
||||||
|
|
||||||
if project_id:
|
if project_id:
|
||||||
expenses = expenses.filter(project_id=project_id)
|
queryset = queryset.filter(project_id=project_id)
|
||||||
if category_id:
|
if category_id:
|
||||||
expenses = expenses.filter(category_id=category_id)
|
queryset = queryset.filter(category_id=category_id)
|
||||||
|
return queryset
|
||||||
|
|
||||||
context = {
|
def get_context_data(self, **kwargs):
|
||||||
'expenses': expenses,
|
context = super().get_context_data(**kwargs)
|
||||||
'projects': Project.objects.all(),
|
context['projects'] = Project.objects.all()
|
||||||
'categories': Category.objects.all(),
|
context['categories'] = Category.objects.all()
|
||||||
'selected_project': project_id,
|
context['selected_project'] = self.request.GET.get('project')
|
||||||
'selected_category': category_id,
|
context['selected_category'] = self.request.GET.get('category')
|
||||||
}
|
return context
|
||||||
return render(request, 'expenses/expense_list.html', context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
from django.utils.decorators import method_decorator
|
||||||
@transaction.atomic # Integridad de datos
|
|
||||||
def expense_create(request):
|
@method_decorator(transaction.atomic, name='dispatch')
|
||||||
# Only allow creation if user is admin or allowed to
|
class ExpenseCreateView(LoginRequiredMixin, CreateView):
|
||||||
if request.method == 'POST':
|
model = Expense
|
||||||
form = ExpenseForm(request.POST, request.FILES)
|
form_class = ExpenseForm
|
||||||
if form.is_valid():
|
template_name = 'expenses/expense_form.html'
|
||||||
|
success_url = reverse_lazy('expense_list')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['regions_communes_json'] = json.dumps(REGIONS_COMMUNES)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
expense = form.save(commit=False)
|
expense = form.save(commit=False)
|
||||||
expense.user = request.user
|
expense.user = self.request.user
|
||||||
# Auto convert to USD if CLP
|
|
||||||
setting, _ = SystemSetting.objects.get_or_create(id=1)
|
setting, _ = SystemSetting.objects.get_or_create(id=1)
|
||||||
if expense.currency == CurrencyEnum.CLP:
|
if expense.currency == CurrencyEnum.CLP:
|
||||||
expense.amount_usd = expense.amount_clp / setting.usd_exchange_rate
|
expense.amount_usd = expense.amount_clp / setting.usd_exchange_rate
|
||||||
@@ -138,7 +153,7 @@ def expense_create(request):
|
|||||||
|
|
||||||
expense.save()
|
expense.save()
|
||||||
|
|
||||||
receipts = request.FILES.getlist('receipt')
|
receipts = self.request.FILES.getlist('receipt')
|
||||||
if receipts:
|
if receipts:
|
||||||
from .models import Document
|
from .models import Document
|
||||||
import os
|
import os
|
||||||
@@ -151,26 +166,18 @@ def expense_create(request):
|
|||||||
expense=expense,
|
expense=expense,
|
||||||
file=f,
|
file=f,
|
||||||
title=semantic_title,
|
title=semantic_title,
|
||||||
uploaded_by=request.user
|
uploaded_by=self.request.user
|
||||||
)
|
)
|
||||||
|
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
user=request.user,
|
user=self.request.user,
|
||||||
action="Gasto Creado",
|
action="Gasto Creado",
|
||||||
entity_name="Expense",
|
entity_name="Expense",
|
||||||
entity_id=expense.expense_code,
|
entity_id=expense.expense_code,
|
||||||
description=f"Se creó el gasto por {expense.amount_clp} CLP"
|
description=f"Se creó el gasto por {expense.amount_clp} CLP"
|
||||||
)
|
)
|
||||||
messages.success(request, 'Gasto registrado exitosamente.')
|
messages.success(self.request, 'Gasto registrado exitosamente.')
|
||||||
return redirect('expense_list')
|
return super().form_valid(form)
|
||||||
else:
|
|
||||||
form = ExpenseForm()
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'form': form,
|
|
||||||
'regions_communes_json': json.dumps(REGIONS_COMMUNES)
|
|
||||||
}
|
|
||||||
return render(request, 'expenses/expense_form.html', context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
def refactor_views():
|
||||||
|
with open('expenses/views.py', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Imports to add
|
||||||
|
imports = """from django.views.generic import ListView, CreateView, UpdateView, DeleteView
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from .mixins import RoleRequiredMixin
|
||||||
|
"""
|
||||||
|
if 'ListView' not in content:
|
||||||
|
content = content.replace("from django.shortcuts import render, redirect, get_object_or_404", imports + "\nfrom django.shortcuts import render, redirect, get_object_or_404")
|
||||||
|
|
||||||
|
# Replace expense_list
|
||||||
|
old_list_view = """@login_required
|
||||||
|
def expense_list(request):
|
||||||
|
expenses = Expense.objects.select_related('project', 'category', 'user').order_by('-date')
|
||||||
|
|
||||||
|
# Filtros
|
||||||
|
project_id = request.GET.get('project')
|
||||||
|
category_id = request.GET.get('category')
|
||||||
|
|
||||||
|
if project_id:
|
||||||
|
expenses = expenses.filter(project_id=project_id)
|
||||||
|
if category_id:
|
||||||
|
expenses = expenses.filter(category_id=category_id)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'expenses': expenses,
|
||||||
|
'projects': Project.objects.all(),
|
||||||
|
'categories': Category.objects.all(),
|
||||||
|
'selected_project': project_id,
|
||||||
|
'selected_category': category_id,
|
||||||
|
}
|
||||||
|
return render(request, 'expenses/expense_list.html', context)"""
|
||||||
|
|
||||||
|
new_list_view = """class ExpenseListView(LoginRequiredMixin, ListView):
|
||||||
|
model = Expense
|
||||||
|
template_name = 'expenses/expense_list.html'
|
||||||
|
context_object_name = 'expenses'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset().select_related('project', 'category', 'user').order_by('-date')
|
||||||
|
project_id = self.request.GET.get('project')
|
||||||
|
category_id = self.request.GET.get('category')
|
||||||
|
|
||||||
|
if project_id:
|
||||||
|
queryset = queryset.filter(project_id=project_id)
|
||||||
|
if category_id:
|
||||||
|
queryset = queryset.filter(category_id=category_id)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['projects'] = Project.objects.all()
|
||||||
|
context['categories'] = Category.objects.all()
|
||||||
|
context['selected_project'] = self.request.GET.get('project')
|
||||||
|
context['selected_category'] = self.request.GET.get('category')
|
||||||
|
return context"""
|
||||||
|
|
||||||
|
content = content.replace(old_list_view, new_list_view)
|
||||||
|
|
||||||
|
with open('expenses/views.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
def refactor_urls():
|
||||||
|
with open('config/urls.py', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check if expense_list is imported
|
||||||
|
if 'expense_list' in content:
|
||||||
|
content = content.replace('expense_list', 'ExpenseListView')
|
||||||
|
content = content.replace("path('gastos/', views.expense_list, name='expense_list')", "path('gastos/', views.ExpenseListView.as_view(), name='expense_list')")
|
||||||
|
|
||||||
|
with open('config/urls.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
refactor_views()
|
||||||
|
refactor_urls()
|
||||||
|
print("Refactorización a POO (CBV) completada para expense_list.")
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
def refactor_create_view():
|
||||||
|
with open('expenses/views.py', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
old_create_view = """@login_required
|
||||||
|
@transaction.atomic # Integridad de datos
|
||||||
|
def expense_create(request):
|
||||||
|
# Only allow creation if user is admin or allowed to
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ExpenseForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
expense = form.save(commit=False)
|
||||||
|
expense.user = request.user
|
||||||
|
# Auto convert to USD if CLP
|
||||||
|
setting, _ = SystemSetting.objects.get_or_create(id=1)
|
||||||
|
if expense.currency == CurrencyEnum.CLP:
|
||||||
|
expense.amount_usd = expense.amount_clp / setting.usd_exchange_rate
|
||||||
|
else:
|
||||||
|
expense.amount_usd = expense.amount_clp
|
||||||
|
expense.amount_clp = expense.amount_usd * setting.usd_exchange_rate
|
||||||
|
|
||||||
|
expense.save()
|
||||||
|
|
||||||
|
receipts = request.FILES.getlist('receipt')
|
||||||
|
if receipts:
|
||||||
|
from .models import Document
|
||||||
|
import os
|
||||||
|
cat_name = expense.category.name if expense.category else 'Evidencia'
|
||||||
|
proj_code = expense.project.code if expense.project else 'PROY'
|
||||||
|
for idx, f in enumerate(receipts, start=1):
|
||||||
|
ext = os.path.splitext(f.name)[1].lower()
|
||||||
|
semantic_title = f"{proj_code} · {cat_name} · Doc {idx:02d}{ext}"
|
||||||
|
Document.objects.create(
|
||||||
|
expense=expense,
|
||||||
|
file=f,
|
||||||
|
title=semantic_title,
|
||||||
|
uploaded_by=request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
AuditLog.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
action="Gasto Creado",
|
||||||
|
entity_name="Expense",
|
||||||
|
entity_id=expense.expense_code,
|
||||||
|
description=f"Se creó el gasto por {expense.amount_clp} CLP"
|
||||||
|
)
|
||||||
|
messages.success(request, 'Gasto registrado exitosamente.')
|
||||||
|
return redirect('expense_list')
|
||||||
|
else:
|
||||||
|
form = ExpenseForm()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'regions_communes_json': json.dumps(REGIONS_COMMUNES)
|
||||||
|
}
|
||||||
|
return render(request, 'expenses/expense_form.html', context)"""
|
||||||
|
|
||||||
|
new_create_view = """from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
@method_decorator(transaction.atomic, name='dispatch')
|
||||||
|
class ExpenseCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
model = Expense
|
||||||
|
form_class = ExpenseForm
|
||||||
|
template_name = 'expenses/expense_form.html'
|
||||||
|
success_url = reverse_lazy('expense_list')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['regions_communes_json'] = json.dumps(REGIONS_COMMUNES)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
expense = form.save(commit=False)
|
||||||
|
expense.user = self.request.user
|
||||||
|
|
||||||
|
setting, _ = SystemSetting.objects.get_or_create(id=1)
|
||||||
|
if expense.currency == CurrencyEnum.CLP:
|
||||||
|
expense.amount_usd = expense.amount_clp / setting.usd_exchange_rate
|
||||||
|
else:
|
||||||
|
expense.amount_usd = expense.amount_clp
|
||||||
|
expense.amount_clp = expense.amount_usd * setting.usd_exchange_rate
|
||||||
|
|
||||||
|
expense.save()
|
||||||
|
|
||||||
|
receipts = self.request.FILES.getlist('receipt')
|
||||||
|
if receipts:
|
||||||
|
from .models import Document
|
||||||
|
import os
|
||||||
|
cat_name = expense.category.name if expense.category else 'Evidencia'
|
||||||
|
proj_code = expense.project.code if expense.project else 'PROY'
|
||||||
|
for idx, f in enumerate(receipts, start=1):
|
||||||
|
ext = os.path.splitext(f.name)[1].lower()
|
||||||
|
semantic_title = f"{proj_code} · {cat_name} · Doc {idx:02d}{ext}"
|
||||||
|
Document.objects.create(
|
||||||
|
expense=expense,
|
||||||
|
file=f,
|
||||||
|
title=semantic_title,
|
||||||
|
uploaded_by=self.request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
AuditLog.objects.create(
|
||||||
|
user=self.request.user,
|
||||||
|
action="Gasto Creado",
|
||||||
|
entity_name="Expense",
|
||||||
|
entity_id=expense.expense_code,
|
||||||
|
description=f"Se creó el gasto por {expense.amount_clp} CLP"
|
||||||
|
)
|
||||||
|
messages.success(self.request, 'Gasto registrado exitosamente.')
|
||||||
|
return super().form_valid(form)"""
|
||||||
|
|
||||||
|
content = content.replace(old_create_view, new_create_view)
|
||||||
|
|
||||||
|
with open('expenses/views.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
def refactor_urls_create():
|
||||||
|
with open('expenses/urls.py', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
content = content.replace("path('gastos/nuevo/', views.expense_create, name='expense_create')", "path('gastos/nuevo/', views.ExpenseCreateView.as_view(), name='expense_create')")
|
||||||
|
with open('expenses/urls.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
refactor_create_view()
|
||||||
|
refactor_urls_create()
|
||||||
|
print("Refactorización a POO completada para expense_create.")
|
||||||
@@ -52,3 +52,5 @@ wheel==0.42.0
|
|||||||
zope.interface==8.5
|
zope.interface==8.5
|
||||||
|
|
||||||
whitenoise==6.7.0
|
whitenoise==6.7.0
|
||||||
|
django-storages==2.14.2
|
||||||
|
boto3==1.34.69
|
||||||
|
|||||||
Reference in New Issue
Block a user