[ADD] PAYROLL: Advance salary/Loan in payroll
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
("", "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"),
|
||||
]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
55
payroll/templates/payroll/loan/filter.html
Normal file
55
payroll/templates/payroll/loan/filter.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% load i18n %}
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button class="oh-btn ml-2" @click="open = !open">
|
||||
<ion-icon name="filter" class="mr-1"></ion-icon>Filter
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Loan Filter" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Employee" %}</label>
|
||||
{{f.form.employee_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Provided date" %}</label>
|
||||
{{f.form.provided_date}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Reporting manager" %}</label>
|
||||
{{f.form.employee_id__employee_work_info__reporting_manager_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Department" %}</label>
|
||||
{{f.form.employee_id__employee_work_info__department_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Job position" %}</label>
|
||||
{{f.form.employee_id__employee_work_info__job_position_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Settled" %}</label>
|
||||
{{f.form.settled}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-dropdown__filter-footer">
|
||||
<button class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton">Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
3
payroll/templates/payroll/loan/form.html
Normal file
3
payroll/templates/payroll/loan/form.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<form hx-post="{% url "create-loan" %}?instance_id={{instance_id}}" hx-target="#loanModalBody">
|
||||
{{form.as_p}}
|
||||
</form>
|
||||
45
payroll/templates/payroll/loan/installments.html
Normal file
45
payroll/templates/payroll/loan/installments.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% load i18n %}
|
||||
<div class="oh-timeoff-modal__profile-content">
|
||||
<div class="oh-profile mb-2">
|
||||
<div class="oh-profile__avatar">
|
||||
<img src="{{ loan.employee_id.get_avatar }}" class="oh-profile__image me-2" />
|
||||
</div>
|
||||
<div class="oh-timeoff-modal__profile-info">
|
||||
<span class="oh-timeoff-modal__user m-0 fw-bold">{{ loan.employee_id }}</span>
|
||||
<span class="oh-timeoff-modal__user m-0" style="font-size: 12px; color: #4d4a4a">{{ loan.employee_id.get_department }} /{{ loan.employee_id.get_job_position }}</span>
|
||||
<span class="oh-timeoff-modal__user m-0" style="font-size: 12px; color: #4d4a4a">{{ record.provided_date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="oh-faq-card__title">
|
||||
{{loan.title}}
|
||||
</h3>
|
||||
<div class="oh-sticky-table__table mt-3">
|
||||
<div class="oh-sticky-table__thead">
|
||||
<div class="oh-sticky-table__tr">
|
||||
<div class="oh-sticky-table__th" align="center" style="width: 50px;">{% trans "S/n" %}</div>
|
||||
<div class="oh-sticky-table__th" align="center" style="width: 80px;">{% trans "One Time Date" %}</div>
|
||||
<div class="oh-sticky-table__th" align="center" style="width: 80px;">{% trans "Amount" %}</div>
|
||||
<div class="oh-sticky-table__th" align="center" style="width: 60px;">{% trans "Status" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-sticky-table__tbody">
|
||||
{% for deduction in installments %}
|
||||
<div class="oh-sticky-table__tr">
|
||||
<div class="oh-sticky-table__td" align="center">{{ forloop.counter }}</div>
|
||||
<div class="oh-sticky-table__td dateformat_changer" align="center">{{ deduction.one_time_date }}</div>
|
||||
<div class="oh-sticky-table__td" align="center">{{ deduction.amount|floatformat:2 }}</div>
|
||||
<div class="oh-sticky-table__td" align="center">
|
||||
{% if deduction.installment_payslip %}
|
||||
<span title="Installment paid, Click to view">
|
||||
<a href="{% url "view-created-payslip" deduction.installment_payslip.id %}">
|
||||
✅
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
24
payroll/templates/payroll/loan/nav.html
Normal file
24
payroll/templates/payroll/loan/nav.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% load i18n %}
|
||||
<section class="oh-wrapper oh-main__topbar">
|
||||
<div class="oh-main__titlebar oh-main__titlebar--left oh-d-flex-column--resp oh-mb-3--small">
|
||||
<h1 class="oh-main__titlebar-title fw-bold">{% trans 'Loan / Advanced Salary' %}</h1>
|
||||
</div>
|
||||
|
||||
<form hx-get="{% url 'search-loan' %}" hx-target="#loanContainer">
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right oh-d-flex-column--resp oh-mb-3--small">
|
||||
<div class="oh-input-group oh-input__search-group mr-4">
|
||||
<ion-icon name="search-outline" class="oh-input-group__icon oh-input-group__icon--left md hydrated" role="img" aria-label="search outline"></ion-icon>
|
||||
<input type="text" id="pipelineSearch" placeholder="Search" style="margin-right:10px" onkeyup="$('.filterButton').click()" name="search" class="oh-input oh-input__icon mr-3" aria-label="Search Input" />
|
||||
</div>
|
||||
{% include 'payroll/loan/filter.html' %}
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<a hx-get="{% url 'create-loan' %}" hx-target="#loanModalBody" data-toggle="oh-modal-toggle" data-target="#loanModal" class="oh-btn oh-btn--secondary">
|
||||
<ion-icon name="add-outline"></ion-icon>
|
||||
{% trans 'Create' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
63
payroll/templates/payroll/loan/records.html
Normal file
63
payroll/templates/payroll/loan/records.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% load i18n %}
|
||||
<div class="oh-wrapper oh-faq-cards">
|
||||
{% include 'filter_tags.html' %}
|
||||
{% for record in records %}
|
||||
<div class="oh-faq-card">
|
||||
<div class="text-danger d-flex flex-row-reverse">
|
||||
<a href="{% url 'delete-loan' %}?ids={{ record.id }}" onclick="return confirm('Do you want to delete this record?')"><ion-icon class="text-danger" name="trash-outline"></ion-icon></a>
|
||||
<a class="mr-2" hx-get="{% url 'create-loan' %}?instance_id={{ record.id }}" hx-target="#loanModalBody" data-toggle="oh-modal-toggle" data-target="#loanModal"><ion-icon class="text-dark" name="create-outline"></ion-icon></a>
|
||||
</div>
|
||||
<div class="oh-timeoff-modal__profile-content">
|
||||
<div class="oh-profile mb-2">
|
||||
<div class="oh-profile__avatar">
|
||||
<img src="{{ record.employee_id.get_avatar }}" class="oh-profile__image me-2" alt="Mary Magdalene" />
|
||||
</div>
|
||||
<div class="oh-timeoff-modal__profile-info">
|
||||
<span class="oh-timeoff-modal__user m-0 fw-bold">{{ record.employee_id }}</span>
|
||||
<span class="oh-timeoff-modal__user m-0" style="font-size: 12px; color: #4d4a4a">{{ record.employee_id.get_department }} /{{ record.employee_id.get_job_position }}</span>
|
||||
<span class="oh-timeoff-modal__user m-0" style="font-size: 12px; color: #4d4a4a">{{ record.provided_date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="oh-faq-card__title">{{ record.title }}</h3>
|
||||
<div class="oh-recuritment__progress-bar">
|
||||
<div class="oh-progress-bar__state" role="progressbar" aria-valuemax="100" style="width:{{ record.installment_ratio }}%"></div>
|
||||
</div>
|
||||
<p class="oh-faq-card__desc">{{ record.description }}.</p>
|
||||
<a hx-get="{% url 'view-installments' %}?loan_id={{ record.id }}" hx-target="#viewInstallmentModalBody" data-target="#viewInstallmentModal" data-toggle="oh-modal-toggle" class="oh-btn oh-btn--secondary oh-btn--block">{% trans 'Installmetns' %}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="oh-wrapper w-100">
|
||||
<div class="oh-pagination">
|
||||
<span class="oh-pagination__page" data-toggle="modal" data-target="#addEmployeeModal">{% trans 'Page' %} {{ records.number }} {% trans 'of' %} {{ records.paginator.num_pages }}.</span>
|
||||
|
||||
<nav class="oh-pagination__nav">
|
||||
<div class="oh-pagination__input-container me-3">
|
||||
<span class="oh-pagination__label me-1">{% trans 'Page' %}</span>
|
||||
|
||||
<input type="number" name="page" class="oh-pagination__input" value="{{ records.number }}" hx-get="{% url 'search-loan' %}?{{ pd }}" hx-target="#loanContainer" min="1" />
|
||||
<span class="oh-pagination__label">{% trans 'of' %} {{ records.paginator.num_pages }}</span>
|
||||
</div>
|
||||
|
||||
<ul class="oh-pagination__items">
|
||||
{% if records.has_previous %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-target="#loanContainer" hx-get="{% url 'search-loan' %}?{{ pd }}&page=1" class="oh-pagination__link">{% trans 'First' %}</a>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-target="#loanContainer" hx-get="{% url 'search-loan' %}?{{ pd }}&page={{ records.previous_page_number }}" class="oh-pagination__link">{% trans 'Previous' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if records.has_next %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-target="#loanContainer" hx-get="{% url 'search-loan' %}?{{ pd }}&page={{ records.next_page_number }}" class="oh-pagination__link">{% trans 'Next' %}</a>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-target="#loanContainer" hx-get="{% url 'search-loan' %}?{{ pd }}&page={{ records.paginator.num_pages }}" class="oh-pagination__link">{% trans 'Last' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
26
payroll/templates/payroll/loan/view_loan.html
Normal file
26
payroll/templates/payroll/loan/view_loan.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'index.html' %}
|
||||
{% block content %}
|
||||
{% load i18n %}
|
||||
{% include 'payroll/loan/nav.html' %}
|
||||
<div id="loanContainer">
|
||||
{% include 'payroll/loan/records.html' %}
|
||||
</div>
|
||||
<div class="oh-modal" id="loanModal" role="dialog" aria-hidden="true">
|
||||
<div class="oh-modal__dialog" style="max-width: 550px">
|
||||
<div class="oh-modal__dialog-header">
|
||||
<button type="button" class="oh-modal__close" aria-label="Close"><ion-icon name="close-outline"></ion-icon></button>
|
||||
</div>
|
||||
|
||||
<div class="oh-modal__dialog-body" id="loanModalBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-modal" id="viewInstallmentModal" role="dialog" aria-hidden="true">
|
||||
<div class="oh-modal__dialog" style="max-width: 550px">
|
||||
<div class="oh-modal__dialog-header">
|
||||
<button type="button" class="oh-modal__close" aria-label="Close"><ion-icon name="close-outline"></ion-icon></button>
|
||||
</div>
|
||||
|
||||
<div class="oh-modal__dialog-body" id="viewInstallmentModalBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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"),
|
||||
|
||||
]
|
||||
|
||||
@@ -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("<script>window.location.reload()</script>")
|
||||
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("<script>window.location.reload()</script>")
|
||||
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()},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user