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']