214 lines
13 KiB
Python
214 lines
13 KiB
Python
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
from decimal import Decimal
|
|
import uuid
|
|
|
|
class ContractTypeEnum(models.TextChoices):
|
|
INDEFINIDO = 'INDEFINIDO', 'Contrato Indefinido'
|
|
PLAZO_FIJO = 'PLAZO_FIJO', 'Contrato a Plazo Fijo'
|
|
HONORARIOS = 'HONORARIOS', 'Prestación de Servicios (Honorarios)'
|
|
PRACTICA = 'PRACTICA', 'Práctica Profesional'
|
|
|
|
class WorkScheduleEnum(models.TextChoices):
|
|
LEY_21561_40H = '40H_LEY_21561', '40 Horas Semanales (Ley 21.561)'
|
|
REGULAR_44H = '44H', '44 Horas Semanales'
|
|
REGULAR_45H = '45H', '45 Horas Semanales (Antiguo)'
|
|
PART_TIME_30H = '30H_PART_TIME', 'Part-time 30 Horas'
|
|
PART_TIME_20H = '20H_PART_TIME', 'Part-time 20 Horas'
|
|
ART_22 = 'ART_22_INC_2', 'Sin Fiscalización Superior (Art. 22 Inc. 2)'
|
|
|
|
class AFPEnum(models.TextChoices):
|
|
HABITAT = 'HABITAT', 'AFP Habitat (11.27%)'
|
|
CUPRUM = 'CUPRUM', 'AFP Cuprum (11.44%)'
|
|
MODELO = 'MODELO', 'AFP Modelo (10.58%)'
|
|
PLANVITAL = 'PLANVITAL', 'AFP PlanVital (11.16%)'
|
|
PROVIDA = 'PROVIDA', 'AFP ProVida (11.45%)'
|
|
CAPITAL = 'CAPITAL', 'AFP Capital (11.44%)'
|
|
UNO = 'UNO', 'AFP Uno (10.49%)'
|
|
|
|
class HealthSystemEnum(models.TextChoices):
|
|
FONASA = 'FONASA', 'FONASA (7.00%)'
|
|
ISAPRE = 'ISAPRE', 'ISAPRE (Pactado UF / %)'
|
|
|
|
class BankAccountTypeEnum(models.TextChoices):
|
|
CORRIENTE = 'CORRIENTE', 'Cuenta Corriente'
|
|
VISTA_RUT = 'VISTA_RUT', 'Cuenta Vista / Cuenta RUT'
|
|
AHORRO = 'AHORRO', 'Cuenta de Ahorro'
|
|
|
|
class SalarySlipStatusEnum(models.TextChoices):
|
|
BORRADOR = 'BORRADOR', 'Borrador'
|
|
EMITIDA = 'EMITIDA', 'Emitida (Pendiente de Firma)'
|
|
FIRMADA = 'FIRMADA', 'Firmada Digitalmente'
|
|
PAGADA = 'PAGADA', 'Pagada / Transferida'
|
|
ANULADA = 'ANULADA', 'Anulada'
|
|
|
|
|
|
class EmployeeProfile(models.Model):
|
|
"""
|
|
Ficha de Empleado / Colaborador conforme a la legislación laboral chilena (Ley 21.561 40h).
|
|
"""
|
|
user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='employee_profile', verbose_name="Usuario del Sistema")
|
|
rut = models.CharField(max_length=20, unique=True, verbose_name="RUT Trabajador")
|
|
first_name = models.CharField(max_length=100, verbose_name="Nombres")
|
|
last_name = models.CharField(max_length=100, verbose_name="Apellidos")
|
|
email = models.EmailField(verbose_name="Correo Electrónico")
|
|
phone = models.CharField(max_length=30, blank=True, verbose_name="Teléfono Móvil")
|
|
address = models.CharField(max_length=255, blank=True, verbose_name="Dirección Particular")
|
|
|
|
position = models.CharField(max_length=150, verbose_name="Cargo / Especialidad I+D+i", default="Ingeniero de Desarrollo")
|
|
department = models.CharField(max_length=100, verbose_name="Departamento / Área", default="I+D+i y Operaciones")
|
|
|
|
contract_type = models.CharField(max_length=50, choices=ContractTypeEnum.choices, default=ContractTypeEnum.INDEFINIDO, verbose_name="Tipo de Contrato")
|
|
work_schedule = models.CharField(max_length=50, choices=WorkScheduleEnum.choices, default=WorkScheduleEnum.LEY_21561_40H, verbose_name="Jornada Laboral")
|
|
hire_date = models.DateField(verbose_name="Fecha de Contratación")
|
|
|
|
# Previsional
|
|
afp = models.CharField(max_length=50, choices=AFPEnum.choices, default=AFPEnum.MODELO, verbose_name="AFP Previsional")
|
|
afp_rate = models.DecimalField(max_digits=5, decimal_places=2, default=10.58, verbose_name="Tasa Cotización AFP (%)")
|
|
|
|
health_system = models.CharField(max_length=50, choices=HealthSystemEnum.choices, default=HealthSystemEnum.FONASA, verbose_name="Sistema de Salud")
|
|
health_institution = models.CharField(max_length=100, blank=True, default="FONASA", verbose_name="Institución Salud (FONASA / Isapre)")
|
|
health_plan_uf = models.DecimalField(max_digits=6, decimal_places=4, default=0, verbose_name="Plan Salud Pactado (UF - solo Isapre)")
|
|
health_rate = models.DecimalField(max_digits=5, decimal_places=2, default=7.00, verbose_name="Tasa Legal Salud (%)")
|
|
|
|
has_afc = models.BooleanField(default=True, verbose_name="Afecto a Seguro de Cesantía (AFC)")
|
|
|
|
# Remuneración Pactada
|
|
base_salary_clp = models.DecimalField(max_digits=12, decimal_places=2, default=1500000, verbose_name="Sueldo Base Mensual (CLP)")
|
|
colacion_clp = models.DecimalField(max_digits=10, decimal_places=2, default=65000, verbose_name="Asignación Colación (No Imponible)")
|
|
movilizacion_clp = models.DecimalField(max_digits=10, decimal_places=2, default=65000, verbose_name="Asignación Movilización (No Imponible)")
|
|
|
|
# Datos Bancarios para Pago de Nómina
|
|
bank_name = models.CharField(max_length=100, default="Banco Santander-Chile", verbose_name="Banco Receptor")
|
|
account_type = models.CharField(max_length=50, choices=BankAccountTypeEnum.choices, default=BankAccountTypeEnum.CORRIENTE, verbose_name="Tipo de Cuenta")
|
|
account_number = models.CharField(max_length=50, default="00000000", verbose_name="Número de Cuenta Bancaria")
|
|
|
|
is_active = models.BooleanField(default=True, verbose_name="Trabajador Activo")
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.rut} - {self.first_name} {self.last_name} ({self.position})"
|
|
|
|
@property
|
|
def full_name(self):
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
class Meta:
|
|
verbose_name = "Perfil de Colaborador"
|
|
verbose_name_plural = "Nómina de Colaboradores"
|
|
ordering = ['last_name', 'first_name']
|
|
|
|
|
|
class SalarySlip(models.Model):
|
|
"""
|
|
Liquidación de Sueldo Digital (Estilo Buk).
|
|
Calcula desglose analítico de Haberes Imponibles/No Imponibles, Descuentos Legales,
|
|
Impuesto Único de Segunda Categoría SII y Aportes Patronales (SIS, Mutual, AFC).
|
|
"""
|
|
slip_uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
|
employee = models.ForeignKey(EmployeeProfile, on_delete=models.CASCADE, related_name='salary_slips', verbose_name="Colaborador")
|
|
|
|
month = models.IntegerField(verbose_name="Mes (1-12)")
|
|
year = models.IntegerField(verbose_name="Año (ej. 2026)")
|
|
issue_date = models.DateField(verbose_name="Fecha de Emisión")
|
|
days_worked = models.IntegerField(default=30, verbose_name="Días Trabajados")
|
|
|
|
# 1. Haberes Imponibles
|
|
base_salary = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Sueldo Base")
|
|
overtime_hours_50 = models.DecimalField(max_digits=5, decimal_places=2, default=0, verbose_name="Horas Extras (50%)")
|
|
overtime_amount_50 = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Monto Horas Extras 50%")
|
|
legal_gratification = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Gratificación Legal (Art. 50)")
|
|
rd_bonus = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name="Bonos de Rendimiento I+D+i")
|
|
other_taxable = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Otros Haberes Imponibles")
|
|
total_taxable_income = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Total Haberes Imponibles")
|
|
|
|
# 2. Haberes No Imponibles
|
|
allowance_lunch = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Asignación Colación")
|
|
allowance_commute = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Asignación Movilización")
|
|
expense_reimbursements = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Reembolsos de Gastos en Terreno")
|
|
total_non_taxable_income = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Total Haberes No Imponibles")
|
|
|
|
total_gross_income = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Total Haberes (Bruto)")
|
|
|
|
# 3. Descuentos Previsionales e Impositivos
|
|
afp_deduction = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Cotización Previsional AFP")
|
|
health_deduction = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Cotización Legal Salud (7% o Isapre)")
|
|
afc_deduction = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Seguro de Cesantía Trabajador (AFC 0.6%)")
|
|
second_category_tax = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Impuesto Único 2da Categoría SII")
|
|
other_deductions = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Otros Descuentos / Anticipos")
|
|
total_deductions = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Total Descuentos")
|
|
|
|
# 4. Líquido a Pagar
|
|
net_salary = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Sueldo Líquido a Pagar")
|
|
|
|
# 5. Cargas Sociales del Empleador (Costo Empresa)
|
|
employer_sis = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Seguro Invalidez y Sobrevivencia SIS (1.49%)")
|
|
employer_mutual = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Aporte Mutualidad / ACHS (0.93%)")
|
|
employer_afc = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Seguro Cesantía Empleador AFC (2.4%)")
|
|
total_company_cost = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Costo Total Empresa")
|
|
|
|
# 6. Workflow y Firma Digital
|
|
status = models.CharField(max_length=30, choices=SalarySlipStatusEnum.choices, default=SalarySlipStatusEnum.BORRADOR, verbose_name="Estado de Liquidación")
|
|
digital_signature_hash = models.CharField(max_length=128, blank=True, verbose_name="Hash de Firma Digital (SHA-256)")
|
|
signed_at = models.DateTimeField(null=True, blank=True, verbose_name="Fecha y Hora de Firma")
|
|
notes = models.TextField(blank=True, verbose_name="Observaciones")
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self):
|
|
return f"Liquidación {self.month:02d}/{self.year} - {self.employee.full_name} (${self.net_salary:,.0f} CLP)"
|
|
|
|
@property
|
|
def period_display(self):
|
|
months = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
|
|
m_name = months[self.month - 1] if 1 <= self.month <= 12 else str(self.month)
|
|
return f"{m_name} {self.year}"
|
|
|
|
class Meta:
|
|
verbose_name = "Liquidación de Sueldo"
|
|
verbose_name_plural = "Liquidaciones de Sueldo (Buk)"
|
|
unique_together = ('employee', 'month', 'year')
|
|
ordering = ['-year', '-month', 'employee__last_name']
|
|
|
|
|
|
class PayrollCentralizationEntry(models.Model):
|
|
"""
|
|
Asiento Contable de Centralización de Remuneraciones.
|
|
Imputa a Cuenta 5101: Remuneraciones y Cargas Sociales I+D+i.
|
|
Valida cuadratura Débitos = Créditos.
|
|
"""
|
|
month = models.IntegerField(verbose_name="Mes")
|
|
year = models.IntegerField(verbose_name="Año")
|
|
entry_date = models.DateField(verbose_name="Fecha del Asiento")
|
|
|
|
glosa = models.CharField(max_length=255, verbose_name="Glosa Contable", default="Centralización de Remuneraciones y Leyes Sociales")
|
|
account_code = models.CharField(max_length=50, default="5101", verbose_name="Cuenta Contable Débito (5101)")
|
|
account_name = models.CharField(max_length=150, default="Gastos en Personal y Cargas Sociales I+D+i", verbose_name="Nombre Cuenta Débito")
|
|
|
|
# Componentes del Asiento
|
|
total_taxable = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Total Haberes Imponibles")
|
|
total_non_taxable = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Total Haberes No Imponibles")
|
|
total_employer_charges = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Total Cargas Sociales Empleador (SIS/ACHS/AFC)")
|
|
|
|
total_net_payable = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Sueldos Líquidos por Pagar (Banco)")
|
|
total_previred_payable = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Cotizaciones por Pagar (Previred)")
|
|
total_sii_payable = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Retención Impuesto Único SII")
|
|
|
|
total_debit = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Total Debe (CLP)")
|
|
total_credit = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Total Haber (CLP)")
|
|
is_balanced = models.BooleanField(default=True, verbose_name="Asiento Cuadrado (Debe == Haber)")
|
|
|
|
status = models.CharField(max_length=30, default="CONTABILIZADO", verbose_name="Estado Asiento")
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return f"Asiento Centralización {self.month:02d}/{self.year} - {self.account_code}: {self.total_debit:,.0f} CLP"
|
|
|
|
class Meta:
|
|
verbose_name = "Asiento de Centralización de Remuneraciones"
|
|
verbose_name_plural = "Asientos de Centralización (Cuenta 5101)"
|
|
unique_together = ('month', 'year')
|
|
ordering = ['-year', '-month']
|