diff --git a/payroll/admin.py b/payroll/admin.py index 61dc550ac..40b938967 100644 --- a/payroll/admin.py +++ b/payroll/admin.py @@ -10,6 +10,7 @@ from payroll.models.models import ( FilingStatus, Payslip, WorkRecord, + LoanAccount, ) from payroll.models.tax_models import ( PayrollSettings, @@ -25,3 +26,4 @@ admin.site.register(Allowance) admin.site.register(Deduction) admin.site.register(Payslip) admin.site.register(PayrollSettings) +admin.site.register(LoanAccount) diff --git a/payroll/filters.py b/payroll/filters.py index 32fe30962..317663178 100644 --- a/payroll/filters.py +++ b/payroll/filters.py @@ -12,7 +12,13 @@ from django import forms from employee.models import Employee from horilla.filters import filter_by_name from base.filters import FilterSet -from payroll.models.models import Allowance, Contract, Deduction, FilingStatus +from payroll.models.models import ( + Allowance, + Contract, + Deduction, + FilingStatus, + LoanAccount, +) from payroll.models.models import Payslip @@ -277,7 +283,7 @@ class PayslipFilter(FilterSet): "deduction__gte", "net_pay__lte", "net_pay__gte", - "sent_to_employee" + "sent_to_employee", ] def __init__(self, data=None, queryset=None, *, request=None, prefix=None): @@ -285,19 +291,47 @@ class PayslipFilter(FilterSet): for field in self.form.fields.keys(): self.form.fields[field].widget.attrs["id"] = f"{uuid.uuid4()}" + +class LoanAccountFilter(FilterSet): + """ + LoanAccountFilter + """ + + search = django_filters.CharFilter(field_name="title", lookup_expr="icontains") + search_employee = django_filters.CharFilter(method=filter_by_name) + provided_date = django_filters.DateFilter( + widget=forms.DateInput(attrs={"type": "date"}), + field_name="provided_date", + ) + + class Meta: + model = LoanAccount + fields = [ + "search", + "search_employee", + "provided_date", + "settled", + "employee_id", + "employee_id__employee_work_info__department_id", + "employee_id__employee_work_info__job_position_id", + "employee_id__employee_work_info__reporting_manager_id", + ] + + class ContractReGroup: """ Class to keep the field name for group by option """ + fields = [ - ("","select"), - ("employee_id","Employee"), - ("employee_id.employee_work_info.job_position_id","Job Position"), - ("employee_id.employee_work_info.department_id","Department"), - ("contract_status","Status"), - ("employee_id.employee_work_info.shift_id","Shift"), - ("employee_id.employee_work_info.work_type_id","Work Type"), - ("employee_id.employee_work_info.job_role_id","Job Role"), - ("employee_id.employee_work_info.reporting_manager_id","Reporting Manager"), - ("employee_id.employee_work_info.company_id","Company"), - ] \ No newline at end of file + ("", "select"), + ("employee_id", "Employee"), + ("employee_id.employee_work_info.job_position_id", "Job Position"), + ("employee_id.employee_work_info.department_id", "Department"), + ("contract_status", "Status"), + ("employee_id.employee_work_info.shift_id", "Shift"), + ("employee_id.employee_work_info.work_type_id", "Work Type"), + ("employee_id.employee_work_info.job_role_id", "Job Role"), + ("employee_id.employee_work_info.reporting_manager_id", "Reporting Manager"), + ("employee_id.employee_work_info.company_id", "Company"), + ] diff --git a/payroll/forms/component_forms.py b/payroll/forms/component_forms.py index c08a6c042..b0a2939a0 100644 --- a/payroll/forms/component_forms.py +++ b/payroll/forms/component_forms.py @@ -15,7 +15,7 @@ from employee.models import Employee from employee.filters import EmployeeFilter from payroll.models import tax_models as models from payroll.widgets import component_widgets as widget -from payroll.models.models import Allowance, Contract +from payroll.models.models import Allowance, Contract, LoanAccount, Payslip import payroll.models.models from base.methods import reload_queryset @@ -315,9 +315,10 @@ class BonusForm(Form): """ Bonus Creating Form """ + title = forms.CharField(max_length=100) date = forms.DateField(widget=forms.DateInput()) - employee_id = forms.IntegerField(label="Employee",widget=forms.HiddenInput()) + employee_id = forms.IntegerField(label="Employee", widget=forms.HiddenInput()) amount = forms.DecimalField(label="Amount") def save(self, commit=True): @@ -346,3 +347,39 @@ class BonusForm(Form): self.fields["date"].widget = forms.DateInput( attrs={"type": "date", "class": "oh-input w-100"} ) + + +class LoanAccountForm(ModelForm): + """ + LoanAccountForm + """ + + verbose_name = "Loan / Advanced Sarlary" + + class Meta: + model = LoanAccount + fields = "__all__" + widgets = { + "provided_date": forms.DateTimeInput(attrs={"type": "date"}), + "installment_start_date": forms.DateTimeInput(attrs={"type": "date"}), + } + + def as_p(self): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + context = {"form": self} + table_html = render_to_string("common_form.html", context) + return table_html + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.verbose_name = self.instance.title + fields_to_exclude = ["employee_id", "installment_start_date"] + if Payslip.objects.filter(installment_ids__in=list(self.instance.deduction_ids.values_list("id",flat=True))).exists(): + fields_to_exclude = fields_to_exclude + ["loan_amount", "installments"] + self.initial["provided_date"] = str(self.instance.provided_date) + for field in fields_to_exclude: + if field in self.fields: + del self.fields[field] diff --git a/payroll/methods/methods.py b/payroll/methods/methods.py index 73cacad10..f9f4c37a2 100644 --- a/payroll/methods/methods.py +++ b/payroll/methods/methods.py @@ -596,6 +596,5 @@ def save_payslip(**kwargs): instance.net_pay = round(kwargs["net_pay"], 2) instance.pay_head_data = kwargs["pay_data"] instance.save() + instance.installment_ids.set(kwargs["installments"]) return instance - - diff --git a/payroll/methods/payslip_calc.py b/payroll/methods/payslip_calc.py index 9f46e73df..35ff53a73 100644 --- a/payroll/methods/payslip_calc.py +++ b/payroll/methods/payslip_calc.py @@ -8,7 +8,7 @@ import operator import contextlib from attendance.models import Attendance from payroll.models import models -from payroll.models.models import Contract, Allowance +from payroll.models.models import Contract, Allowance, LoanAccount from payroll.methods.limits import compute_limit operator_mapping = { @@ -366,6 +366,9 @@ def calculate_pre_tax_deduction(*_args, **kwargs): .exclude(one_time_date__gt=end_date) .exclude(update_compensation__isnull=False) ) + # Installment deductions + installments = deductions.filter(is_installment=True) + pre_tax_deductions = [] pre_tax_deductions_amt = [] serialized_deductions = [] @@ -414,7 +417,7 @@ def calculate_pre_tax_deduction(*_args, **kwargs): "amount": amount, } serialized_deductions.append(serialized_deduction) - return {"pretax_deductions": serialized_deductions} + return {"pretax_deductions": serialized_deductions, "installments": installments} def calculate_post_tax_deduction(*_args, **kwargs): @@ -453,6 +456,9 @@ def calculate_post_tax_deduction(*_args, **kwargs): .exclude(one_time_date__gt=end_date) .exclude(update_compensation__isnull=False) ) + # Installment deductions + installments = deductions.filter(is_installment=True) + post_tax_deductions = [] post_tax_deductions_amt = [] serialized_deductions = [] @@ -512,6 +518,7 @@ def calculate_post_tax_deduction(*_args, **kwargs): return { "post_tax_deductions": serialized_deductions, "net_pay_deduction": serialized_net_pay_deductions, + "installments":installments, } diff --git a/payroll/models/models.py b/payroll/models/models.py index fc00112cd..4a2f248e0 100644 --- a/payroll/models/models.py +++ b/payroll/models/models.py @@ -3,9 +3,12 @@ models.py Used to register models """ from datetime import date, datetime, timedelta +import threading from django import forms from django.db import models from django.dispatch import receiver +from django.contrib import messages +from django.db.models.signals import post_save from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from django.db.models.signals import pre_save, pre_delete @@ -21,10 +24,10 @@ from attendance.models import ( Attendance, strtime_seconds, ) - from leave.models import LeaveRequest + # Create your models here. @@ -810,7 +813,8 @@ class Allowance(models.Model): company_id = models.ForeignKey( Company, null=True, editable=False, on_delete=models.PROTECT ) - only_show_under_employee = models.BooleanField(default=False,editable=False) + only_show_under_employee = models.BooleanField(default=False, editable=False) + is_loan = models.BooleanField(default=False,editable=False) objects = HorillaCompanyManager() class Meta: @@ -1102,9 +1106,15 @@ class Deduction(models.Model): company_id = models.ForeignKey( Company, null=True, editable=False, on_delete=models.PROTECT ) - only_show_under_employee = models.BooleanField(default=False,editable=False) + only_show_under_employee = models.BooleanField(default=False, editable=False) objects = HorillaCompanyManager() + is_installment = models.BooleanField(default=False,editable=False) + + def installment_payslip(self): + payslip = Payslip.objects.filter(installment_ids=self).first() + return payslip + def clean(self): super().clean() @@ -1203,7 +1213,7 @@ class Payslip(models.Model): ) sent_to_employee = models.BooleanField(null=True, default=False) objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") - + installment_ids = models.ManyToManyField(Deduction,editable=False) def __str__(self) -> str: return f"Payslip for {self.employee_id} - Period: {self.start_date} to {self.end_date}" @@ -1276,3 +1286,105 @@ class Payslip(models.Model): ordering = [ "-end_date", ] + + +class LoanAccount(models.Model): + """ + This modal is used to store the loan Account details + """ + + title = models.CharField(max_length=20) + employee_id = models.ForeignKey( + Employee, on_delete=models.PROTECT, verbose_name=_("Employee") + ) + loan_amount = models.FloatField(default=0) + provided_date = models.DateField() + allowance_id = models.ForeignKey( + Allowance, on_delete=models.SET_NULL, editable=False, null=True + ) + description = models.TextField(null=True) + deduction_ids = models.ManyToManyField(Deduction, editable=False) + is_fixed = models.BooleanField(default=True, editable=False) + rate = models.FloatField(default=0, editable=False) + installments = models.IntegerField(verbose_name=_("Total installments")) + installment_start_date = models.DateField( + help_text="From the start date deduction will apply" + ) + apply_on = models.CharField(default="end_of_month", max_length=10, editable=False) + settled = models.BooleanField(default=False) + + def get_installments(self): + loan_amount = self.loan_amount + total_installments = self.installments + installment_amount = loan_amount / total_installments + installment_start_date = self.installment_start_date + + installment_schedule = {} + + installment_date = installment_start_date + for i in range(total_installments): + installment_schedule[str(installment_date)] = installment_amount + installment_date = installment_date + timedelta(days=30 * (i + 1)) + + return installment_schedule + + def delete(self, *args, **kwargs): + self.deduction_ids.all().delete() + self.allowance_id.delete() + if not Payslip.objects.filter(installment_ids__in=list(self.deduction_ids.values_list("id",flat=True))).exists(): + super().delete(*args, **kwargs) + return + + def installment_ratio(self): + total_installments = self.installments + installment_paid = Payslip.objects.filter(installment_ids__in = self.deduction_ids.all() ).count() + if not installment_paid: + return 0 + return (installment_paid/total_installments)*100 + + +@receiver(post_save, sender=LoanAccount) +def create_installments(sender, instance, created, **kwargs): + """ + This is post save method, used to create initial stage for the recruitment + """ + installments = [] + if created: + loan = Allowance() + loan.amount = instance.loan_amount + loan.title = instance.title + loan.include_active_employees = False + loan.amount = instance.loan_amount + loan.only_show_under_employee = True + loan.is_fixed = True + loan.one_time_date = instance.provided_date + loan.is_loan = True + loan.save() + loan.include_active_employees = False + loan.specific_employees.add(instance.employee_id) + loan.save() + instance.allowance_id = loan + # Here create the instance... + super(LoanAccount, instance).save() + else: + deductions = instance.deduction_ids.values_list("id", flat=True) + # Re create deduction only when existing installment not exists in payslip + if not Payslip.objects.filter(installment_ids__in=deductions).exists(): + Deduction.objects.filter(id__in=deductions).delete() + + # Installment deductions + for installment_date, installment_amount in instance.get_installments().items(): + installment = Deduction() + installment.title = instance.title + installment.include_active_employees = False + installment.amount = installment_amount + installment.is_fixed = True + installment.one_time_date = installment_date + installment.only_show_under_employee = True + installment.is_installment = True + installment.save() + installment.include_active_employees = False + installment.specific_employees.add(instance.employee_id) + installment.save() + installments.append(installment) + instance.deduction_ids.set(installments) \ No newline at end of file diff --git a/payroll/templates/payroll/loan/filter.html b/payroll/templates/payroll/loan/filter.html new file mode 100644 index 000000000..c61d53dbd --- /dev/null +++ b/payroll/templates/payroll/loan/filter.html @@ -0,0 +1,55 @@ +{% load i18n %} +
\ No newline at end of file diff --git a/payroll/templates/payroll/loan/form.html b/payroll/templates/payroll/loan/form.html new file mode 100644 index 000000000..19985121b --- /dev/null +++ b/payroll/templates/payroll/loan/form.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/payroll/templates/payroll/loan/installments.html b/payroll/templates/payroll/loan/installments.html new file mode 100644 index 000000000..3b9896363 --- /dev/null +++ b/payroll/templates/payroll/loan/installments.html @@ -0,0 +1,45 @@ +{% load i18n %} +{{ record.description }}.
+ {% trans 'Installmetns' %} +