MVP final

This commit is contained in:
Nachou-u
2026-08-20 21:01:12 -04:00
parent 08a26cbf2f
commit 66655fb05c
10 changed files with 593 additions and 32 deletions
+98 -9
View File
@@ -4,25 +4,40 @@ from .models import Expense, Project, Category, SystemSetting, UserProfile, Docu
from .chile_regions import REGION_CHOICES, COMMUNE_CHOICES
from decimal import Decimal
import os
class MultipleFileInput(forms.FileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput(attrs={'multiple': True, 'accept': 'image/*,.pdf'}))
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = single_file_clean(data, initial)
return result
class ExpenseForm(forms.ModelForm):
date = forms.DateField(
widget=forms.DateInput(attrs={'type': 'date'}),
label='Fecha del Gasto'
)
amount_clp = forms.DecimalField(
amount_clp = forms.CharField(
label='Monto Neto (CLP)',
required=True,
widget=forms.NumberInput(attrs={'placeholder': 'Ej: 1450000'})
widget=forms.TextInput(attrs={'placeholder': 'Ej: 1.450.000'})
)
receipt = forms.FileField(
receipt = MultipleFileField(
label='Adjuntar Evidencia(s) (Boleta / Factura)',
required=False,
widget=MultipleFileInput(attrs={'multiple': True, 'accept': 'image/*,.pdf'})
required=False
)
region = forms.ChoiceField(
@@ -43,16 +58,28 @@ class ExpenseForm(forms.ModelForm):
required=False
)
tax_amount_clp = forms.DecimalField(
tax_amount_clp = forms.CharField(
label='Monto Impuesto (CLP)',
required=False,
widget=forms.NumberInput(attrs={'readonly': 'readonly'})
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
total_amount_clp = forms.DecimalField(
total_amount_clp = forms.CharField(
label='Monto Total (CLP)',
required=False,
widget=forms.NumberInput(attrs={'readonly': 'readonly'})
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
new_vendor_rut = forms.CharField(
label="RUT Nuevo Proveedor",
required=False,
widget=forms.TextInput(attrs={'placeholder': 'Ej: 76.543.210-K', 'id': 'id_new_vendor_rut'})
)
new_vendor_name = forms.CharField(
label="Nombre / Razón Social Nuevo Proveedor",
required=False,
widget=forms.TextInput(attrs={'placeholder': 'Ej: IMPORTADORA SPA', 'id': 'id_new_vendor_name'})
)
class Meta:
@@ -65,6 +92,68 @@ class ExpenseForm(forms.ModelForm):
'description': forms.Textarea(attrs={'rows': 3, 'placeholder': 'Ej: Carga combustible Copec / Pasajes bus terreno'}),
}
def clean_amount_clp(self):
data = self.cleaned_data.get('amount_clp', '')
if not data:
raise forms.ValidationError("Este campo es obligatorio.")
data_clean = str(data).replace('.', '').replace(',', '.')
try:
return Decimal(data_clean)
except Exception:
raise forms.ValidationError("Ingrese un monto válido (ej: 1.450.000).")
def clean_tax_amount_clp(self):
data = self.cleaned_data.get('tax_amount_clp', '')
if not data:
return Decimal('0.00')
data_clean = str(data).replace('.', '').replace(',', '.')
try:
return Decimal(data_clean)
except Exception:
return Decimal('0.00')
def clean_total_amount_clp(self):
data = self.cleaned_data.get('total_amount_clp', '')
if not data:
return Decimal('0.00')
data_clean = str(data).replace('.', '').replace(',', '.')
try:
return Decimal(data_clean)
except Exception:
return Decimal('0.00')
def clean_receipt(self):
files = self.files.getlist('receipt')
for f in files:
ext = os.path.splitext(f.name)[1].lower()
if ext not in ['.pdf', '.png', '.jpg', '.jpeg']:
raise forms.ValidationError(f"El archivo '{f.name}' no tiene una extensión permitida (PDF, PNG, JPG, JPEG).")
return self.cleaned_data.get('receipt')
def clean(self):
cleaned_data = super().clean()
new_name = cleaned_data.get('new_vendor_name')
new_rut = cleaned_data.get('new_vendor_rut')
if (new_name and not new_rut) or (new_rut and not new_name):
raise forms.ValidationError("Si desea registrar un nuevo proveedor, debe ingresar tanto el RUT como la Razón Social.")
return cleaned_data
def save(self, commit=True):
instance = super().save(commit=False)
new_name = self.cleaned_data.get('new_vendor_name')
new_rut = self.cleaned_data.get('new_vendor_rut')
if new_name and new_rut:
from .models import Vendor
vendor, _ = Vendor.objects.get_or_create(
rut=new_rut.strip(),
defaults={'name': new_name.strip()}
)
instance.vendor = vendor
if commit:
instance.save()
return instance
class ProjectForm(forms.ModelForm):
class Meta:
+1 -1
View File
@@ -11,7 +11,7 @@ def expense_saved(sender, instance, created, **kwargs):
expense_data = {
'id': instance.id,
'amount': str(instance.amount),
'amount': str(instance.amount_clp),
'category': instance.category.name if instance.category else None,
'project': instance.project.name if instance.project else None,
'user': instance.user.username if instance.user else None,
+1
View File
@@ -18,6 +18,7 @@ urlpatterns = [
# Export / Import
path('gastos/exportar/csv/', views.export_expenses_csv, name='export_csv'),
path('gastos/exportar/transferencias/', views.export_bank_transfer_csv, name='export_bank_transfer_csv'),
path('proyectos/', views.project_list, name='project_list'),
+53
View File
@@ -343,6 +343,59 @@ def export_expenses_csv(request):
return response
@login_required
@role_required('Administrador', 'Administrador Finanzas')
def export_bank_transfer_csv(request):
# Get all expenses requested for payment that are linked to a vendor
pending_expenses = Expense.objects.filter(
approval_status=ApprovalStatusEnum.SOLICITADO_FINANZAS,
vendor__isnull=False
)
# Group debts by vendor to perform single bank transfer per supplier
debts = pending_expenses.values(
'vendor__id', 'vendor__rut', 'vendor__name', 'vendor__bank', 'vendor__account_type', 'vendor__account_number'
).annotate(
total_clp=Sum('amount_clp'),
total_usd=Sum('amount_usd'),
expense_count=Count('id')
).order_by('vendor__name')
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="Nomina_Transferencias_{date.today()}.csv"'
# Write BOM for Excel Spanish compatibility
response.write('\ufeff'.encode('utf-8'))
writer = csv.writer(response, delimiter=';')
writer.writerow([
'RUT Proveedor', 'Nombre / Razón Social', 'Banco', 'Tipo Cuenta',
'Número Cuenta', 'Monto Transferir (CLP)', 'Monto Transferir (USD)',
'Comprobantes Pendientes', 'Glosa / Comentario'
])
for debt in debts:
writer.writerow([
debt['vendor__rut'],
debt['vendor__name'],
debt['vendor__bank'] or 'No Registrado',
debt['vendor__account_type'] or 'No Registrado',
debt['vendor__account_number'] or 'No Registrado',
int(debt['total_clp']),
round(debt['total_usd'], 2),
debt['expense_count'],
'Pago Facturas Pendientes EFINTEL ERP'
])
AuditLog.objects.create(
user=request.user,
action="EXPORT_BANK_TRANSFER",
description="Se exportó la nómina bancaria de transferencias a proveedores."
)
return response
@login_required
def project_list(request):
projects = Project.objects.annotate(