248 lines
12 KiB
Python
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
|