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 %} +
+
+ +
+
+
+
{% trans "Loan Filter" %}
+
+
+
+
+ + {{f.form.employee_id}} +
+
+ + {{f.form.provided_date}} +
+
+ + {{f.form.employee_id__employee_work_info__reporting_manager_id}} +
+
+
+
+ + {{f.form.employee_id__employee_work_info__department_id}} +
+
+ + {{f.form.employee_id__employee_work_info__job_position_id}} +
+
+ + {{f.form.settled}} +
+
+
+
+
+
+ +
+
+ +
\ 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 @@ +
+ {{form.as_p}} +
\ 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 %} +
+
+
+ +
+
+ {{ loan.employee_id }} + {{ loan.employee_id.get_department }} /{{ loan.employee_id.get_job_position }} + {{ record.provided_date }} +
+
+
+

+ {{loan.title}} +

+
+
+
+
{% trans "S/n" %}
+
{% trans "One Time Date" %}
+
{% trans "Amount" %}
+
{% trans "Status" %}
+
+
+
+ {% for deduction in installments %} +
+
{{ forloop.counter }}
+
{{ deduction.one_time_date }}
+
{{ deduction.amount|floatformat:2 }}
+
+ {% if deduction.installment_payslip %} + + + ✅ + + + {% endif %} +
+
+ {% endfor %} +
+
+ \ No newline at end of file diff --git a/payroll/templates/payroll/loan/nav.html b/payroll/templates/payroll/loan/nav.html new file mode 100644 index 000000000..30acb7b2b --- /dev/null +++ b/payroll/templates/payroll/loan/nav.html @@ -0,0 +1,24 @@ +{% load i18n %} +
+
+

{% trans 'Loan / Advanced Salary' %}

+
+ +
+
+
+ + +
+ {% include 'payroll/loan/filter.html' %} + +
+
+
diff --git a/payroll/templates/payroll/loan/records.html b/payroll/templates/payroll/loan/records.html new file mode 100644 index 000000000..aedc3eef9 --- /dev/null +++ b/payroll/templates/payroll/loan/records.html @@ -0,0 +1,63 @@ +{% load i18n %} +
+ {% include 'filter_tags.html' %} + {% for record in records %} +
+
+ + +
+
+
+
+ Mary Magdalene +
+
+ {{ record.employee_id }} + {{ record.employee_id.get_department }} /{{ record.employee_id.get_job_position }} + {{ record.provided_date }} +
+
+
+

{{ record.title }}

+
+
+
+

{{ record.description }}.

+ {% trans 'Installmetns' %} +
+ {% endfor %} +
+
+
+ {% trans 'Page' %} {{ records.number }} {% trans 'of' %} {{ records.paginator.num_pages }}. + + +
+
diff --git a/payroll/templates/payroll/loan/view_loan.html b/payroll/templates/payroll/loan/view_loan.html new file mode 100644 index 000000000..6184c4a98 --- /dev/null +++ b/payroll/templates/payroll/loan/view_loan.html @@ -0,0 +1,26 @@ +{% extends 'index.html' %} +{% block content %} + {% load i18n %} + {% include 'payroll/loan/nav.html' %} +
+ {% include 'payroll/loan/records.html' %} +
+ + +{% endblock %} diff --git a/payroll/urls/component_urls.py b/payroll/urls/component_urls.py index 1552b77ea..b3edea7d3 100644 --- a/payroll/urls/component_urls.py +++ b/payroll/urls/component_urls.py @@ -71,6 +71,11 @@ urlpatterns = [ name="hx-create-allowance", ), path("send-slip",component_views.send_slip,name="send-slip"), - path("add-bonus/",component_views.add_bonus,name="add-bonus") + path("add-bonus/",component_views.add_bonus,name="add-bonus"), + path("view-loan/",component_views.view_loans,name="view-loan"), + path("create-loan/",component_views.create_loan,name="create-loan"), + path("view-installments/",component_views.view_installments,name="view-installments"), + path("delete-loan/",component_views.delete_loan,name="delete-loan"), + path("search-loan/",component_views.search_loan,name="search-loan"), ] diff --git a/payroll/views/component_views.py b/payroll/views/component_views.py index 0db22840f..1afc93afc 100644 --- a/payroll/views/component_views.py +++ b/payroll/views/component_views.py @@ -22,7 +22,7 @@ from horilla.settings import EMAIL_HOST_USER from base.methods import get_key_instances from base.methods import closest_numbers import payroll.models.models -from payroll.models.models import Allowance, Deduction, Payslip +from payroll.models.models import Allowance, Deduction, LoanAccount, Payslip from payroll.methods.payslip_calc import ( calculate_allowance, calculate_gross_pay, @@ -33,7 +33,12 @@ from payroll.methods.payslip_calc import ( calculate_pre_tax_deduction, calculate_tax_deduction, ) -from payroll.filters import AllowanceFilter, DeductionFilter, PayslipFilter +from payroll.filters import ( + AllowanceFilter, + DeductionFilter, + LoanAccountFilter, + PayslipFilter, +) from payroll.forms import component_forms as forms from payroll.methods.payslip_calc import ( calculate_net_pay_deduction, @@ -118,6 +123,10 @@ def payroll_calculation(employee, start_date, end_date): pretax_deductions = calculate_pre_tax_deduction(**kwargs) post_tax_deductions = calculate_post_tax_deduction(**kwargs) + installments = ( + pretax_deductions["installments"] | post_tax_deductions["installments"] + ) + taxable_gross_pay = calculate_taxable_gross_pay(**kwargs) tax_deductions = calculate_tax_deduction(**kwargs) federal_tax = calculate_taxable_amount(**kwargs) @@ -197,6 +206,7 @@ def payroll_calculation(employee, start_date, end_date): json_data = json.dumps(data_to_json) payslip_data["json_data"] = json_data + payslip_data["installments"] = installments return payslip_data @@ -497,6 +507,7 @@ def generate_payslip(request): data["deduction"] = payslip["total_deductions"] data["net_pay"] = payslip["net_pay"] data["pay_data"] = json.loads(payslip["json_data"]) + data["installments"] = payslip["installments"] instance = save_payslip(**data) instances.append(instance) messages.success(request, f"{employees.count()} payslip saved as draft") @@ -558,6 +569,7 @@ def create_payslip(request): data["deduction"] = payslip_data["total_deductions"] data["net_pay"] = payslip_data["net_pay"] data["pay_data"] = json.loads(payslip_data["json_data"]) + data["installments"] = payslip_data["installments"] payslip_data["instance"] = save_payslip(**data) form = forms.PayslipForm() messages.success(request, _("Payslip Saved")) @@ -849,9 +861,6 @@ def send_slip(request): @login_required @permission_required("payroll.add_allowance") def add_bonus(request): - print("========================================") - print(request.GET) - print("========================================") employee_id = request.GET["employee_id"] form = forms.BonusForm(initial={"employee_id": employee_id}) if request.method == "POST": @@ -860,4 +869,88 @@ def add_bonus(request): form.save() messages.success(request, "Bonus Added") return HttpResponse("") - return render(request, "payroll/bonus/form.html", {"form": form,"employee_id":employee_id}) + return render( + request, "payroll/bonus/form.html", {"form": form, "employee_id": employee_id} + ) + + +@login_required +@permission_required("payroll.view_loanaccount") +def view_loans(request): + """ + This method is used to render template to disply all the loan records + """ + records = LoanAccount.objects.all() + filter_instance = LoanAccountFilter() + return render( + request, + "payroll/loan/view_loan.html", + {"records": paginator_qry(records, request.GET.get("page")), "f": filter_instance}, + ) + + +@login_required +@permission_required("payroll.add_loanaccount") +def create_loan(request): + """ + This method is used to create and update the loan instance + """ + instance_id = eval(str(request.GET.get("instance_id"))) + instance = LoanAccount.objects.filter(id=instance_id).first() + form = forms.LoanAccountForm(instance=instance) + if request.method == "POST": + form = forms.LoanAccountForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, "Loan created/updated") + return HttpResponse("") + return render( + request, "payroll/loan/form.html", {"form": form, "instance_id": instance_id} + ) + + +@login_required +@permission_required("payroll.view_loanaccount") +def view_installments(request): + """ + View install ments + """ + loan_id = request.GET["loan_id"] + loan = LoanAccount.objects.get(id=loan_id) + installments = loan.deduction_ids.all() + return render( + request, + "payroll/loan/installments.html", + {"installments": installments, "loan": loan}, + ) + + +@login_required +@permission_required("payroll.delete_loanaccount") +def delete_loan(request): + """ + Delete loan + """ + ids = request.GET.getlist("ids") + loans = LoanAccount.objects.filter(id__in=ids) + # This 👇 would'nt trigger the delete method in the model + # loans.delete() + for loan in loans: + loan.delete() + messages.success(request, "Loan account deleted") + return redirect(view_loans) + + +@login_required +@permission_required("payroll.view_loanaccount") +def search_loan(request): + """ + Search loan method + """ + records = LoanAccountFilter(request.GET).qs + data_dict = parse_qs(request.GET.urlencode()) + return render( + request, + "payroll/loan/records.html", + {"records": paginator_qry(records, request.GET.get("page")), "filter_dict": data_dict,"pd":request.GET.urlencode()}, + )