Files
erp-efintel-2026-portainer/payroll/views.py
T
2026-09-07 18:44:32 -03:00

248 lines
12 KiB
Python

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import HttpResponse, JsonResponse
from django.utils import timezone
from django.db.models import Sum, Count
from decimal import Decimal
from datetime import date, datetime
from .models import EmployeeProfile, SalarySlip, PayrollCentralizationEntry, SalarySlipStatusEnum
from .forms import EmployeeProfileForm, SalarySlipGenerateForm
from .services.engine import PayrollEngine, BankExportService, PreviredExportService, LREExportService
@login_required
def employee_list(request):
employees = EmployeeProfile.objects.all().order_by('last_name', 'first_name')
active_count = employees.filter(is_active=True).count()
total_base_clp = employees.filter(is_active=True).aggregate(Sum('base_salary_clp'))['base_salary_clp__sum'] or 0
return render(request, 'payroll/employee_list.html', {
'employees': employees,
'active_count': active_count,
'total_base_clp': total_base_clp,
})
@login_required
def employee_create(request):
if request.method == 'POST':
form = EmployeeProfileForm(request.POST)
if form.is_valid():
emp = form.save()
messages.success(request, f"Colaborador {emp.full_name} registrado exitosamente.")
return redirect('payroll:employee_list')
else:
form = EmployeeProfileForm()
return render(request, 'payroll/employee_form.html', {'form': form, 'title': 'Registrar Nuevo Colaborador'})
@login_required
def employee_edit(request, pk):
emp = get_object_or_404(EmployeeProfile, pk=pk)
if request.method == 'POST':
form = EmployeeProfileForm(request.POST, instance=emp)
if form.is_valid():
emp = form.save()
messages.success(request, f"Ficha de {emp.full_name} actualizada.")
return redirect('payroll:employee_list')
else:
form = EmployeeProfileForm(instance=emp)
return render(request, 'payroll/employee_form.html', {'form': form, 'title': f"Editar Colaborador: {emp.full_name}"})
@login_required
def slip_list(request):
month = request.GET.get('month')
year = request.GET.get('year', 2026)
slips = SalarySlip.objects.select_related('employee').all()
if month:
slips = slips.filter(month=int(month))
if year:
slips = slips.filter(year=int(year))
totals = slips.aggregate(
total_liquid=Sum('net_salary'),
total_gross=Sum('total_gross_income'),
total_company=Sum('total_company_cost')
)
return render(request, 'payroll/slip_list.html', {
'slips': slips,
'selected_month': month,
'selected_year': int(year),
'total_liquid': totals['total_liquid'] or 0,
'total_gross': totals['total_gross'] or 0,
'total_company': totals['total_company'] or 0,
})
@login_required
def slip_generate(request):
if request.method == 'POST':
form = SalarySlipGenerateForm(request.POST)
if form.is_valid():
month = int(form.cleaned_data['month'])
year = int(form.cleaned_data['year'])
specific_employee = form.cleaned_data.get('employee')
overtime_hours = form.cleaned_data.get('overtime_hours') or Decimal('0')
rd_bonus = form.cleaned_data.get('rd_bonus') or Decimal('0')
other_deductions = form.cleaned_data.get('other_deductions') or Decimal('0')
employees_to_calc = [specific_employee] if specific_employee else list(EmployeeProfile.objects.filter(is_active=True))
created_count = 0
for emp in employees_to_calc:
# Calculate
calc = PayrollEngine.compute_salary_slip(
employee=emp,
month=month,
year=year,
days_worked=30,
overtime_hours=overtime_hours if specific_employee else Decimal('0'),
rd_bonus=rd_bonus if specific_employee else Decimal('0'),
other_deductions=other_deductions if specific_employee else Decimal('0')
)
slip, created = SalarySlip.objects.update_or_create(
employee=emp,
month=month,
year=year,
defaults={
'issue_date': date(year, month, 28),
'days_worked': calc['days_worked'],
'base_salary': calc['base_salary'],
'overtime_hours_50': calc['overtime_hours_50'],
'overtime_amount_50': calc['overtime_amount_50'],
'legal_gratification': calc['legal_gratification'],
'rd_bonus': calc['rd_bonus'],
'other_taxable': calc['other_taxable'],
'total_taxable_income': calc['total_taxable_income'],
'allowance_lunch': calc['allowance_lunch'],
'allowance_commute': calc['allowance_commute'],
'total_non_taxable_income': calc['total_non_taxable_income'],
'total_gross_income': calc['total_gross_income'],
'afp_deduction': calc['afp_deduction'],
'health_deduction': calc['health_deduction'],
'afc_deduction': calc['afc_deduction'],
'second_category_tax': calc['second_category_tax'],
'other_deductions': calc['other_deductions'],
'total_deductions': calc['total_deductions'],
'net_salary': calc['net_salary'],
'employer_sis': calc['employer_sis'],
'employer_mutual': calc['employer_mutual'],
'employer_afc': calc['employer_afc'],
'total_company_cost': calc['total_company_cost'],
'status': SalarySlipStatusEnum.EMITIDA
}
)
created_count += 1
# Auto-update centralization entry for this month/year
month_slips = SalarySlip.objects.filter(month=month, year=year)
m_totals = month_slips.aggregate(
sum_taxable=Sum('total_taxable_income'),
sum_non_taxable=Sum('total_non_taxable_income'),
sum_employer=Sum('employer_sis') + Sum('employer_mutual') + Sum('employer_afc'),
sum_net=Sum('net_salary'),
sum_previred=Sum('afp_deduction') + Sum('health_deduction') + Sum('afc_deduction') + Sum('employer_sis') + Sum('employer_mutual') + Sum('employer_afc'),
sum_sii=Sum('second_category_tax'),
sum_cost=Sum('total_company_cost')
)
debit_total = (m_totals['sum_cost'] or Decimal('0'))
credit_total = (m_totals['sum_net'] or Decimal('0')) + (m_totals['sum_previred'] or Decimal('0')) + (m_totals['sum_sii'] or Decimal('0'))
PayrollCentralizationEntry.objects.update_or_create(
month=month,
year=year,
defaults={
'entry_date': date(year, month, 28),
'glosa': f"Centralización Sueldos y Leyes Sociales {month:02d}/{year} EFINTEL I+D+i",
'account_code': '5101',
'account_name': 'Remuneraciones y Cargas Sociales I+D+i',
'total_taxable': m_totals['sum_taxable'] or 0,
'total_non_taxable': m_totals['sum_non_taxable'] or 0,
'total_employer_charges': m_totals['sum_employer'] or 0,
'total_net_payable': m_totals['sum_net'] or 0,
'total_previred_payable': m_totals['sum_previred'] or 0,
'total_sii_payable': m_totals['sum_sii'] or 0,
'total_debit': debit_total,
'total_credit': credit_total,
'is_balanced': abs(debit_total - credit_total) < Decimal('1.00'),
'status': 'CONTABILIZADO'
}
)
messages.success(request, f"Se generaron/actualizaron con éxito {created_count} liquidaciones para el periodo {month:02d}/{year}.")
return redirect('payroll:slip_list')
else:
form = SalarySlipGenerateForm()
return render(request, 'payroll/slip_generate.html', {'form': form})
@login_required
def slip_detail(request, pk):
slip = get_object_or_404(SalarySlip.objects.select_related('employee'), pk=pk)
return render(request, 'payroll/slip_detail.html', {'slip': slip})
@login_required
def slip_sign(request, pk):
slip = get_object_or_404(SalarySlip, pk=pk)
if request.method == 'POST':
now = timezone.now()
sig_hash = PayrollEngine.generate_signature_hash(slip.id, slip.employee.rut, slip.net_salary, now)
slip.digital_signature_hash = sig_hash
slip.signed_at = now
slip.status = SalarySlipStatusEnum.FIRMADA
slip.save()
messages.success(request, f"Liquidación firmada digitalmente con éxito. Hash: {sig_hash[:16]}...")
return redirect('payroll:slip_detail', pk=pk)
@login_required
def centralization_view(request):
entries = PayrollCentralizationEntry.objects.all().order_by('-year', '-month')
return render(request, 'payroll/centralization.html', {'entries': entries})
# Export Views
@login_required
def export_previred(request):
month = request.GET.get('month', date.today().month)
year = request.GET.get('year', date.today().year)
slips = SalarySlip.objects.filter(month=int(month), year=int(year)).select_related('employee')
if not slips.exists():
messages.warning(request, f"No hay liquidaciones emitidas para {month}/{year}.")
return redirect('payroll:slip_list')
content = PreviredExportService.generate_previred_105(list(slips))
response = HttpResponse(content, content_type='text/plain; charset=iso-8859-1')
response['Content-Disposition'] = f'attachment; filename="PREVIRED_105_{year}_{int(month):02d}.txt"'
return response
@login_required
def export_lre(request):
month = request.GET.get('month', date.today().month)
year = request.GET.get('year', date.today().year)
slips = SalarySlip.objects.filter(month=int(month), year=int(year)).select_related('employee')
if not slips.exists():
messages.warning(request, f"No hay liquidaciones emitidas para {month}/{year}.")
return redirect('payroll:slip_list')
content = LREExportService.generate_dt_lre_csv(list(slips))
response = HttpResponse(content, content_type='text/csv; charset=utf-8-sig')
response['Content-Disposition'] = f'attachment; filename="LRE_DT_{year}_{int(month):02d}.csv"'
return response
@login_required
def export_bank_txt(request):
month = request.GET.get('month', date.today().month)
year = request.GET.get('year', date.today().year)
slips = SalarySlip.objects.filter(month=int(month), year=int(year)).select_related('employee')
if not slips.exists():
messages.warning(request, f"No hay liquidaciones emitidas para {month}/{year}.")
return redirect('payroll:slip_list')
content = BankExportService.export_santander_format(list(slips))
response = HttpResponse(content, content_type='text/csv; charset=utf-8-sig')
response['Content-Disposition'] = f'attachment; filename="NOMINA_BANCO_{year}_{int(month):02d}.csv"'
return response