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