diff --git a/horilla/urls.py b/horilla/urls.py index 052176d37..9bed4ea20 100755 --- a/horilla/urls.py +++ b/horilla/urls.py @@ -31,6 +31,7 @@ urlpatterns = [ path('pms/',include('pms.urls')), path('asset/',include('asset.urls')), path('attendance/',include('attendance.urls')), + path('payroll/',include('payroll.urls.urls')), re_path('^inbox/notifications/', include(notifications.urls, namespace='notifications')), path('i18n/', include('django.conf.urls.i18n')), diff --git a/payroll/__init__.py b/payroll/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/admin.py b/payroll/admin.py new file mode 100644 index 000000000..20ea37905 --- /dev/null +++ b/payroll/admin.py @@ -0,0 +1,31 @@ +""" +admin.py + +Used to register models on admin site +""" +from django.contrib import admin +from payroll.models.tax_models import ( + FederalTax, +) +from payroll.models.models import ( + Allowance, + Deduction, + FilingStatus, + Payslip, + WorkRecord, +) +from payroll.models.tax_models import ( + PayrollSettings, + TaxBracket, +) +from payroll.models.models import Contract + +# Register your models here. +admin.site.register(FilingStatus) +admin.site.register(TaxBracket) +admin.site.register(FederalTax) +admin.site.register([Contract, WorkRecord]) +admin.site.register(Allowance) +admin.site.register(Deduction) +admin.site.register(Payslip) +admin.site.register(PayrollSettings) diff --git a/payroll/apps.py b/payroll/apps.py new file mode 100644 index 000000000..a1eab344e --- /dev/null +++ b/payroll/apps.py @@ -0,0 +1,12 @@ +""" +App configuration for the 'payroll' app. +""" +from django.apps import AppConfig + + +class PayrollConfig(AppConfig): + """ + AppConfig for the 'payroll' app. + """ + default_auto_field = 'django.db.models.BigAutoField' + name = 'payroll' diff --git a/payroll/context_processors.py b/payroll/context_processors.py new file mode 100644 index 000000000..e3aabf9e6 --- /dev/null +++ b/payroll/context_processors.py @@ -0,0 +1,18 @@ +""" +context_processor.py + +This module is used to register context processor` +""" +from payroll.models import tax_models as models + + +def default_currency(request): + """ + This method will return the currency + """ + if models.PayrollSettings.objects.first() is None: + settings = models.PayrollSettings() + settings.currency_symbol = "$" + settings.save() + symbol = models.PayrollSettings.objects.first().currency_symbol + return {"currency": request.session.get("currency", symbol)} diff --git a/payroll/filters.py b/payroll/filters.py new file mode 100644 index 000000000..905e53edf --- /dev/null +++ b/payroll/filters.py @@ -0,0 +1,239 @@ +""" +Module containing filter set classes for payroll models. + +This module defines the filter set classes used for filtering data in the payroll app. +Each filter set class corresponds to a specific model and contains filter fields and methods +to customize the filtering behavior. + +""" +import django_filters +from django import forms +from horilla.filters import filter_by_name +from attendance.filters import FilterSet +from payroll.models.models import Allowance, Contract, Deduction +from payroll.models.models import Payslip + + +class ContractFilter(FilterSet): + """ + Filter set class for Contract model + + Args: + FilterSet (class): custom filter set class to apply styling + """ + + search = django_filters.CharFilter(method="filter_by_contract") + contract_start_date = django_filters.DateFilter( + field_name="contract_start_date", + widget=forms.DateInput(attrs={"type": "date"}), + ) + contract_end_date = django_filters.DateFilter( + field_name="contract_end_date", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + class Meta: + """ + Meta class to add additional options + """ + + model = Contract + fields = [ + "employee_id", + "contract_name", + "contract_start_date", + "contract_end_date", + "wage_type", + "filing_status", + "pay_frequency", + "contract_status", + ] + + def filter_by_contract(self, queryset, _, value): + """ + Filter queryset by first name or last name. + """ + # Split the search value into first name and last name + parts = value.split() + first_name = parts[0] + og_queryset = queryset + last_name = " ".join(parts[1:]) if len(parts) > 1 else "" + # Filter the queryset by first name and last name + if first_name and last_name: + queryset = queryset.filter( + employee_id__employee_first_name__icontains=first_name, + employee_id__employee_last_name__icontains=last_name, + ) + elif first_name: + queryset = queryset.filter( + employee_id__employee_first_name__icontains=first_name + ) + elif last_name: + queryset = queryset.filter( + employee_id__employee_last_name__icontains=last_name + ) + queryset = queryset | og_queryset.filter(contract_name__icontains=value) + return queryset + + +class AllowanceFilter(FilterSet): + """ + Filter set class for Allowance model. + """ + + search = django_filters.CharFilter(method="filter_by_employee") + + class Meta: + """ + Meta class to add additional options + """ + + model = Allowance + fields = [ + "title", + "is_taxable", + "is_condition_based", + "is_fixed", + "based_on", + ] + + def filter_by_employee(self, queryset, _, value): + """ + Filter queryset by first name or last name. + """ + # Split the search value into first name and last name + parts = value.split() + first_name = parts[0] + og_queryset = queryset + last_name = " ".join(parts[1:]) if len(parts) > 1 else "" + # Filter the queryset by first name and last name + if first_name and last_name: + queryset = queryset.filter( + specific_employees__employee_first_name__icontains=first_name, + specific_employees__employee_last_name__icontains=last_name, + ) + elif first_name: + queryset = queryset.filter( + specific_employees__employee_first_name__icontains=first_name + ) + elif last_name: + queryset = queryset.filter( + specific_employees__employee_last_name__icontains=last_name + ) + queryset = queryset | og_queryset.filter(title__icontains=value) + return queryset + + +class DeductionFilter(FilterSet): + """ + Filter set class for Deduction model. + """ + + search = django_filters.CharFilter(method="filter_by_employee") + + class Meta: + """ + Meta class to add additional options + """ + + model = Deduction + fields = [ + "title", + "is_pretax", + "is_condition_based", + "is_fixed", + "based_on", + ] + + def filter_by_employee(self, queryset, _, value): + """ + Filter queryset by first name or last name. + """ + # Split the search value into first name and last name + parts = value.split() + first_name = parts[0] + og_queryset = queryset + last_name = " ".join(parts[1:]) if len(parts) > 1 else "" + # Filter the queryset by first name and last name + if first_name and last_name: + queryset = queryset.filter( + specific_employees__employee_first_name__icontains=first_name, + specific_employees__employee_last_name__icontains=last_name, + ) + elif first_name: + queryset = queryset.filter( + specific_employees__employee_first_name__icontains=first_name + ) + elif last_name: + queryset = queryset.filter( + specific_employees__employee_last_name__icontains=last_name + ) + queryset = queryset | og_queryset.filter(title__icontains=value) + return queryset + + +class PayslipFilter(FilterSet): + """ + Filter set class for payslip model. + """ + + search = django_filters.CharFilter(method=filter_by_name) + start_date = django_filters.DateFilter( + widget=forms.DateInput(attrs={"type": "date"}), + ) + end_date = django_filters.DateFilter( + widget=forms.DateInput(attrs={"type": "date"}), + ) + start_date_from = django_filters.DateFilter( + widget=forms.DateInput(attrs={"type": "date"}), + field_name="start_date", + lookup_expr="gte", + ) + start_date_till = django_filters.DateFilter( + widget=forms.DateInput(attrs={"type": "date"}), + field_name="start_date", + lookup_expr="lte", + ) + end_date_from = django_filters.DateFilter( + widget=forms.DateInput(attrs={"type": "date"}), + field_name="end_date", + lookup_expr="gte", + ) + end_date_till = django_filters.DateFilter( + widget=forms.DateInput(attrs={"type": "date"}), + field_name="end_date", + lookup_expr="lte", + ) + gross_pay__lte = django_filters.NumberFilter( + field_name="gross_pay", lookup_expr="lte" + ) + gross_pay__gte = django_filters.NumberFilter( + field_name="gross_pay", lookup_expr="gte" + ) + deduction__lte = django_filters.NumberFilter( + field_name="deduction", lookup_expr="lte" + ) + deduction__gte = django_filters.NumberFilter( + field_name="deduction", lookup_expr="gte" + ) + net_pay__lte = django_filters.NumberFilter(field_name="net_pay", lookup_expr="lte") + net_pay__gte = django_filters.NumberFilter(field_name="net_pay", lookup_expr="gte") + + class Meta: + """ + Meta class to add additional options + """ + + model = Payslip + fields = [ + "employee_id", + "start_date", + "end_date", + "status", + "gross_pay__lte", + "gross_pay__gte", + "deduction__lte", + "deduction__gte", + "net_pay__lte", + "net_pay__gte", + ] diff --git a/payroll/forms/__init__.py b/payroll/forms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/forms/component_forms.py b/payroll/forms/component_forms.py new file mode 100644 index 000000000..f14205494 --- /dev/null +++ b/payroll/forms/component_forms.py @@ -0,0 +1,225 @@ +""" +These forms provide a convenient way to handle data input, validation, and customization +of form fields and widgets for the corresponding models in the payroll management system. +""" +import uuid +import datetime +from django import forms +from django.utils.translation import gettext_lazy as _ +from django.template.loader import render_to_string +from base.forms import ModelForm +from employee.models import Employee +from payroll.models import tax_models as models +from payroll.widgets import component_widgets as widget +from payroll.models.models import Contract +import payroll.models.models + + +class AllowanceForm(forms.ModelForm): + """ + Form for Allowance model + """ + + load = forms.CharField(widget=widget.AllowanceConditionalVisibility, required=False) + style = forms.CharField(required=False) + verbose_name = _("Allowance") + + class Meta: + """ + Meta class for additional options + """ + + model = payroll.models.models.Allowance + fields = "__all__" + widgets = { + "one_time_date": forms.DateTimeInput(attrs={"type": "date"}), + } + + def __init__(self, *args, **kwargs): + if instance := kwargs.get("instance"): + # django forms not showing value inside the date, time html element. + # so here overriding default forms instance method to set initial value + initial = {} + if instance.one_time_date is not None: + initial = { + "one_time_date": instance.one_time_date.strftime("%Y-%m-%d"), + } + kwargs["initial"] = initial + super().__init__(*args, **kwargs) + self.fields["style"].widget = widget.StyleWidget(form=self) + + 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 + + +class DeductionForm(forms.ModelForm): + """ + Form for Deduction model + """ + + load = forms.CharField(widget=widget.DeductionConditionalVisibility, required=False) + style = forms.CharField(required=False) + verbose_name = _("Deduction") + + + class Meta: + """ + Meta class for additional options + """ + + model = payroll.models.models.Deduction + fields = "__all__" + widgets = { + "one_time_date": forms.DateTimeInput(attrs={"type": "date"}), + } + + def __init__(self, *args, **kwargs): + if instance := kwargs.get("instance"): + # django forms not showing value inside the date, time html element. + # so here overriding default forms instance method to set initial value + initial = {} + if instance.one_time_date is not None: + initial = { + "one_time_date": instance.one_time_date.strftime("%Y-%m-%d"), + } + kwargs["initial"] = initial + super().__init__(*args, **kwargs) + self.fields["style"].widget = widget.StyleWidget(form=self) + + def clean(self): + if ( + self.data.get("update_compensation") is not None + and self.data.get("update_compensation") != "" + ): + if ( + self.data.getlist("specific_employees") is None + and len(self.data.getlist("specific_employees")) == 0 + ): + raise forms.ValidationError( + {"specific_employees": "You need to choose the employee."} + ) + + if ( + self.data.get("one_time_date") is None + and self.data.get("one_time_date") == "" + ): + raise forms.ValidationError( + {"one_time_date": "This field is required."} + ) + if self.data.get("amount") is None and self.data.get("amount") == "": + raise forms.ValidationError({"amount": "This field is required."}) + return super().clean() + + 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 + + +class PayslipForm(ModelForm): + """ + Form for Payslip + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + active_contracts = Contract.objects.filter(contract_status="active") + self.fields["employee_id"].choices = [ + (contract.employee_id.id, contract.employee_id) + for contract in active_contracts + ] + + class Meta: + """ + Meta class for additional options + """ + + model = payroll.models.models.Payslip + fields = [ + "employee_id", + "start_date", + "end_date", + ] + widgets = { + "start_date": forms.DateInput(attrs={"type": "date"}), + "end_date": forms.DateInput(attrs={"type": "date"}), + } + + +class GeneratePayslipForm(forms.Form): + """ + Form for Payslip + """ + + employee_id = forms.ModelMultipleChoiceField( + queryset=Employee.objects.filter( + contract_set__isnull=False, contract_set__contract_status="active" + ), + widget=forms.SelectMultiple, + ) + start_date = forms.DateField(widget=forms.DateInput(attrs={"type": "date"})) + end_date = forms.DateField(widget=forms.DateInput(attrs={"type": "date"})) + + def clean(self): + cleaned_data = super().clean() + start_date = cleaned_data.get("start_date") + end_date = cleaned_data.get("end_date") + + today = datetime.date.today() + if end_date <= start_date: + raise forms.ValidationError( + { + "end_date": "The end date must be greater than or equal to the start date." + } + ) + if start_date > today: + raise forms.ValidationError( + {"end_date": "The start date cannot be in the future."} + ) + + if end_date > today: + raise forms.ValidationError( + {"end_date": "The end date cannot be in the future."} + ) + return cleaned_data + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["employee_id"].widget.attrs.update( + {"class": "oh-select oh-select-2", "id": uuid.uuid4()} + ) + self.fields["start_date"].widget.attrs.update({"class": "oh-input w-100"}) + self.fields["end_date"].widget.attrs.update({"class": "oh-input w-100"}) + + class Meta: + """ + Meta class for additional options + """ + + widgets = { + "start_date": forms.DateInput(attrs={"type": "date"}), + "end_date": forms.DateInput(attrs={"type": "date"}), + } + + +class PayrollSettingsForm(ModelForm): + """ + Form for PayrollSettings model + """ + + class Meta: + """ + Meta class for additional options + """ + + model = models.PayrollSettings + fields = "__all__" diff --git a/payroll/forms/forms.py b/payroll/forms/forms.py new file mode 100644 index 000000000..3c07d4b84 --- /dev/null +++ b/payroll/forms/forms.py @@ -0,0 +1,110 @@ +""" +forms.py +""" +from django import forms +from django.utils.translation import gettext_lazy as trans +from django.template.loader import render_to_string +from payroll.models.models import WorkRecord +from payroll.models.models import Contract + + +class ModelForm(forms.ModelForm): + """ + ModelForm override for additional style + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for _, field in self.fields.items(): + widget = field.widget + + if isinstance(widget, (forms.DateInput)): + field.widget.attrs.update({"class": "oh-input oh-calendar-input w-100"}) + elif isinstance( + widget, (forms.NumberInput, forms.EmailInput, forms.TextInput) + ): + label = trans(field.label) + field.widget.attrs.update( + {"class": "oh-input w-100", "placeholder": label} + ) + elif isinstance(widget, (forms.Select,)): + field.widget.attrs.update( + {"class": "oh-select oh-select-2 select2-hidden-accessible"} + ) + elif isinstance(widget, (forms.Textarea)): + label = trans(field.label) + field.widget.attrs.update( + { + "class": "oh-input w-100", + "placeholder": label, + "rows": 2, + "cols": 40, + } + ) + elif isinstance( + widget, + ( + forms.CheckboxInput, + forms.CheckboxSelectMultiple, + ), + ): + field.widget.attrs.update({"class": "oh-switch__checkbox"}) + + +class ContractForm(ModelForm): + """ + ContactForm + """ + verbose_name = trans("Contract") + class Meta: + """ + Meta class for additional options + """ + + fields = "__all__" + exclude = [ + "is_active", + ] + model = Contract + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["contract_name"].widget.attrs["autocomplete"] = "off" + self.fields["contract_start_date"].widget.attrs["autocomplete"] = "off" + self.fields["contract_start_date"].widget.attrs["class"] = "oh-input w-100" + self.fields["contract_start_date"].widget = forms.TextInput( + attrs={"type": "date", "class": "oh-input w-100"} + ) + self.fields["contract_end_date"].widget.attrs["autocomplete"] = "off" + self.fields["contract_end_date"].widget.attrs["class"] = "oh-input w-100" + self.fields["contract_end_date"].widget = forms.TextInput( + attrs={"type": "date", "class": "oh-input w-100"} + ) + self.fields["employee_id"].widget.attrs["data-contract-style"] = "" + self.fields["department"].widget.attrs["data-contract-style"] = "" + self.fields["job_position"].widget.attrs["data-contract-style"] = "" + self.fields["job_role"].widget.attrs["data-contract-style"] = "" + self.fields["work_type"].widget.attrs["data-contract-style"] = "" + self.fields["shift"].widget.attrs["data-contract-style"] = "" + + def as_p(self): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + context = {"form": self} + table_html = render_to_string("contract_form.html", context) + return table_html + + +class WorkRecordForm(ModelForm): + """ + WorkRecordForm + """ + + class Meta: + """ + Meta class for additional options + """ + + fields = "__all__" + model = WorkRecord diff --git a/payroll/forms/tax_forms.py b/payroll/forms/tax_forms.py new file mode 100644 index 000000000..c442a4410 --- /dev/null +++ b/payroll/forms/tax_forms.py @@ -0,0 +1,101 @@ +""" +Forms for handling payroll-related operations. + +This module provides Django ModelForms for creating and managing payroll-related data, +including filing status, tax brackets, and federal tax records. + +The forms in this module inherit from the Django `forms.ModelForm` class and customize +the widget attributes to enhance the user interface and provide a better user experience. + +""" +import uuid +from django import forms +from django.utils.translation import gettext_lazy as _ +from payroll.models.tax_models import TaxBracket + + +from payroll.models.tax_models import ( + FederalTax, +) +from payroll.models.models import FilingStatus + + +class ModelForm(forms.ModelForm): + """Custom ModelForm with enhanced widget attributes.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name, field in self.fields.items(): + input_widget = field.widget + if isinstance( + input_widget, (forms.NumberInput, forms.EmailInput, forms.TextInput) + ): + label = _(field.label) + input_widget.attrs.update( + {"class": "oh-input w-100", "placeholder": label} + ) + elif isinstance(input_widget, (forms.Select,)): + label = "" + if field.label is not None: + label = _(field.label) + field.empty_label = str(_("---Choose {label}---")).format(label=label) + self.fields[field_name].widget.attrs.update( + { + "class": "oh-select oh-select-2 w-100", + "id": uuid.uuid4(), + "style": "height:50px;border-radius:0;", + } + ) + elif isinstance(input_widget, (forms.Textarea)): + label = _(field.label.title()) + input_widget.attrs.update( + { + "class": "oh-input w-100", + "placeholder": label, + "rows": 2, + "cols": 40, + } + ) + elif isinstance( + input_widget, + ( + forms.CheckboxInput, + forms.CheckboxSelectMultiple, + ), + ): + input_widget.attrs.update({"class": "oh-switch__checkbox"}) + + +class FilingStatusForm(ModelForm): + """Form for creating and updating filing status.""" + + class Meta: + """Meta options for the form.""" + + model = FilingStatus + fields = "__all__" + + +class TaxBracketForm(ModelForm): + """Form for creating and updating tax bracket.""" + + class Meta: + """Meta options for the form.""" + + model = TaxBracket + fields = "__all__" + widgets = { + "filing_status_id": forms.Select( + attrs={"class": "oh-select oh-select-2 select2-hidden-accessible"} + ), + } + + +class FederalTaxForm(ModelForm): + """Form for creating and updating tax bracket.""" + + class Meta: + """Meta options for the form.""" + + model = FederalTax + fields = "__all__" diff --git a/payroll/methods/__init__.py b/payroll/methods/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/methods/deductions.py b/payroll/methods/deductions.py new file mode 100644 index 000000000..459d2bf08 --- /dev/null +++ b/payroll/methods/deductions.py @@ -0,0 +1,37 @@ +""" +deductions.py + +This module is used to compute the deductions of employees +""" +from payroll.models.models import Deduction + + +def update_compensation_deduction( + employee, compensation_amount, compensation_type, start_date, end_date +): + """ + This method is used to update the basic or gross pay + + Args: + compensation_amount (_type_): Gross pay or Basic pay or employee + """ + deduction_heads = ( + Deduction.objects.filter( + update_compensation=compensation_type, specific_employees=employee + ) + .exclude(one_time_date__lt=start_date) + .exclude(one_time_date__gt=end_date) + # .exclude(exclude_employees=employee) + ) + deductions = [] + temp = compensation_amount + for deduction in deduction_heads: + compensation_amount = compensation_amount - float(deduction.amount) + deductions.append({"title": deduction.title, "amount": deduction.amount}) + + difference_amount = temp - compensation_amount + return { + "compensation_amount": compensation_amount, + "deductions": deductions, + "difference_amount": difference_amount, + } diff --git a/payroll/methods/limits.py b/payroll/methods/limits.py new file mode 100644 index 000000000..71d0d334a --- /dev/null +++ b/payroll/methods/limits.py @@ -0,0 +1,56 @@ +""" +limit.py + +This module is used to compute the limit for allowance and deduction +""" +# from payroll.models.models import Allowance + + +def compute_limit(component, amount, _day_dict): + """ + limit compute method + """ + # day_dict = [ + # { + # "month": 5, + # "year": 2023, + # "days": 31, + # "start_date": "2023-05-01", + # "end_date": "2023-05-31", + # "working_days_on_period": 27, + # "working_days_on_month": 27, + # "per_day_amount": 555.5555555555555, + # }, + # { + # "month": 6, + # "year": 2023, + # "days": 30, + # "start_date": "2023-06-01", + # "end_date": "2023-06-23", + # "working_days_on_period": 20, + # "working_days_on_month": 26, + # "per_day_amount": 576.9230769230769, + # }, + # ] + if component.has_max_limit: + max_amount = component.maximum_amount + amount = min(amount, max_amount) + # if ( + # isinstance(component, Allowance) + # and component.has_max_limit + # and not component.is_fixed + # ): + # amount = min(amount, max_amount) + + # elif component.has_max_limit: + # unit = component.maximum_unit + # amount = 0 + # if unit == "month_working_days": + # for month in day_dict: + # working_days_on_month = month["working_days_on_month"] + # working_days_on_period = month["working_days_on_period"] + # # maximum amount for one working day + # max_day_amount = max_amount / working_days_on_month + # amount = amount + (max_day_amount * working_days_on_period) + + return amount diff --git a/payroll/methods/methods.py b/payroll/methods/methods.py new file mode 100644 index 000000000..4485e6d10 --- /dev/null +++ b/payroll/methods/methods.py @@ -0,0 +1,573 @@ +""" +methods.py + +Payroll related module to write custom calculation methods +""" +import calendar +from datetime import timedelta, datetime, date +from django.db.models import F +from django.core.paginator import Paginator +from dateutil.relativedelta import relativedelta +from leave.models import Holiday, CompanyLeave +from attendance.models import Attendance +from payroll.models.models import Contract + + +def get_holiday_dates(): + """ + :return: this functions returns a list of all holiday dates. + """ + holiday_dates = [] + holidays = Holiday.objects.all() + for holiday in holidays: + holiday_start_date = holiday.start_date + holiday_end_date = holiday.end_date + if holiday_end_date is None: + holiday_end_date = holiday_start_date + holiday_days = holiday_end_date - holiday_start_date + for i in range(holiday_days.days + 1): + holiday_date = holiday_start_date + timedelta(i) + holiday_dates.append(holiday_date) + return holiday_dates + + +def get_company_leave_dates(year): + """ + :return: This function returns a list of all company leave dates + """ + company_leaves = CompanyLeave.objects.all() + company_leave_dates = [] + for company_leave in company_leaves: + based_on_week = company_leave.based_on_week + based_on_week_day = company_leave.based_on_week_day + for month in range(1, 13): + if based_on_week is not None: + # Set Sunday as the first day of the week + calendar.setfirstweekday(6) + month_calendar = calendar.monthcalendar(year, month) + weeks = month_calendar[int(based_on_week)] + weekdays_in_weeks = [day for day in weeks if day != 0] + for day in weekdays_in_weeks: + leave_date = datetime.strptime( + f"{year}-{month:02}-{day:02}", "%Y-%m-%d" + ).date() + if ( + leave_date.weekday() == int(based_on_week_day) + and leave_date not in company_leave_dates + ): + company_leave_dates.append(leave_date) + else: + # Set Monday as the first day of the week + calendar.setfirstweekday(0) + month_calendar = calendar.monthcalendar(year, month) + for week in month_calendar: + if week[int(based_on_week_day)] != 0: + leave_date = datetime.strptime( + f"{year}-{month:02}-{week[int(based_on_week_day)]:02}", + "%Y-%m-%d", + ).date() + if leave_date not in company_leave_dates: + company_leave_dates.append(leave_date) + return company_leave_dates + + +def get_date_range(start_date, end_date): + """ + Returns a list of all dates within a given date range. + + Args: + start_date (date): The start date of the range. + end_date (date): The end date of the range. + + Returns: + list: A list of date objects representing all dates within the range. + + Example: + start_date = date(2023, 1, 1) + end_date = date(2023, 1, 10) + date_range = get_date_range(start_date, end_date) + for date_obj in date_range: + print(date_obj) + """ + date_list = [] + delta = end_date - start_date + + for i in range(delta.days + 1): + current_date = start_date + timedelta(days=i) + date_list.append(current_date) + + return date_list + + +def get_total_days(start_date, end_date): + """ + Calculates the total number of days in a given period. + + Args: + start_date (date): The start date of the period. + end_date (date): The end date of the period. + + Returns: + int: The total number of days in the period, including the end date. + + Example: + start_date = date(2023, 1, 1) + end_date = date(2023, 1, 10) + days_on_period = get_total_days(start_date, end_date) + print(days_on_period) # Output: 10 + """ + delta = end_date - start_date + total_days = delta.days + 1 # Add 1 to include the end date itself + return total_days + + +def get_working_days(start_date, end_date): + """ + This method is used to calculate the total working days, total leave, worked days on that period + + Args: + start_date (_type_): the start date from the data needed + end_date (_type_): the end date till the date needed + """ + + holiday_dates = get_holiday_dates() + + # appending company/holiday leaves + # Note: Duplicate entry may exist + company_leave_dates = ( + list( + set( + get_company_leave_dates(start_date.year) + + get_company_leave_dates(end_date.year) + ) + ) + + holiday_dates + ) + + date_range = get_date_range(start_date, end_date) + + # making unique list of company/holiday leave dates then filtering + # the leave dates only between the start and end date + company_leave_dates = [ + date + for date in list(set(company_leave_dates)) + if start_date <= date <= end_date + ] + + working_days_between_ranges = list(set(date_range) - set(company_leave_dates)) + total_working_days = len(working_days_between_ranges) + + return { + # Total working days on that period + "total_working_days": total_working_days, + # All the working dates between the start and end date + "working_days_on": working_days_between_ranges, + # All the company/holiday leave dates between the range + "company_leave_dates": company_leave_dates, + } + + +def get_leaves(employee, start_date, end_date): + """ + This method is used to return all the leaves taken by the employee + between the period. + + Args: + employee (obj): Employee model instance + start_date (obj): the start date from the data needed + end_date (obj): the end date till the date needed + """ + approved_leaves = employee.leaverequest_set.filter(status="approved") + paid_leave = 0 + unpaid_leave = 0 + paid_half = 0 + unpaid_half = 0 + paid_leave_dates = [] + unpaid_leave_dates = [] + company_leave_dates = get_working_days(start_date, end_date)["company_leave_dates"] + + if approved_leaves.exists(): + for instance in approved_leaves: + if instance.leave_type_id.payment == "paid": + # if the taken leave is paid + # for the start date + all_the_paid_leave_taken_dates = instance.requested_dates() + paid_leave_dates = paid_leave_dates + [ + date + for date in all_the_paid_leave_taken_dates + if start_date <= date <= end_date + ] + else: + # if the taken leave is unpaid + # for the start date + all_unpaid_leave_taken_dates = instance.requested_dates() + unpaid_leave_dates = unpaid_leave_dates + [ + date + for date in all_unpaid_leave_taken_dates + if start_date <= date <= end_date + ] + + half_day_data = find_half_day_leaves() + + unpaid_half = half_day_data["half_unpaid_leaves"] + paid_half = half_day_data["half_paid_leaves"] + + paid_leave_dates = list(set(paid_leave_dates) - set(company_leave_dates)) + unpaid_leave_dates = list(set(unpaid_leave_dates) - set(company_leave_dates)) + paid_leave = len(paid_leave_dates) - paid_half + unpaid_leave = len(unpaid_leave_dates) - unpaid_half + + return { + "paid_leave": paid_leave, + "unpaid_leaves": unpaid_leave, + "total_leaves": paid_leave + unpaid_leave, + # List of paid leave date between range + "paid_leave_dates": paid_leave_dates, + # List of un paid date between range + "unpaid_leave_dates": unpaid_leave_dates, + "leave_dates": unpaid_leave_dates + paid_leave_dates, + } + + +def get_attendance(employee, start_date, end_date): + """ + This method is used to render attendance details between the range + + Args: + employee (obj): Employee user instance + start_date (obj): start date of the period + end_date (obj): end date of the period + """ + + attendances_on_period = Attendance.objects.filter( + employee_id=employee, + attendance_date__range=(start_date, end_date), + attendance_validated=True, + ) + present_on = [attendance.attendance_date for attendance in attendances_on_period] + working_days_between_range = get_working_days(start_date, end_date)[ + "working_days_on" + ] + leave_dates = get_leaves(employee, start_date, end_date)["leave_dates"] + conflict_dates = list( + set(working_days_between_range) - set(attendances_on_period) - set(leave_dates) + ) + conflict_dates = conflict_dates + [ + date + for date in present_on + if date in get_holiday_dates() + or date + in list( + set( + get_company_leave_dates(start_date.year) + + get_company_leave_dates(end_date.year) + ) + ) + ] + + return { + "attendances_on_period": attendances_on_period, + "present_on": present_on, + "conflict_dates": conflict_dates, + } + + +def hourly_computation(employee, wage, start_date, end_date): + """ + Hourly salary computation for period. + + Args: + employee (obj): Employee instance + wage (float): wage of the employee + start_date (obj): start of the pay period + end_date (obj): end date of the period + """ + attendance_data = get_attendance(employee, start_date, end_date) + attendances_on_period = attendance_data["attendances_on_period"] + total_worked_hour_in_second = 0 + for attendance in attendances_on_period: + total_worked_hour_in_second = total_worked_hour_in_second + ( + attendance.at_work_second - attendance.overtime_second + ) + + # to find wage per second + # wage_per_second = wage_per_hour / total_seconds_in_hour + wage_in_second = wage / 3600 + basic_pay = float(f"{(wage_in_second * total_worked_hour_in_second):.2f}") + + return { + "basic_pay": basic_pay, + "loss_of_pay": 0, + } + + +def find_half_day_leaves(): + """ + This method is used to return the half day leave details + + Args: + employee (obj): Employee model instance + start_date (obj): start date of the period + end_date (obj): end date of the period + """ + paid_queryset = [] + unpaid_queryset = [] + + paid_leaves = list(filter(None, list(set(paid_queryset)))) + unpaid_leaves = list(filter(None, list(set(unpaid_queryset)))) + + paid_half = len(paid_leaves) * 0.5 + unpaid_half = len(unpaid_leaves) * 0.5 + queryset = paid_leaves + unpaid_leaves + total_leaves = len(queryset) * 0.50 + return { + "half_day_query_set": queryset, + "half_day_leaves": total_leaves, + "half_paid_leaves": paid_half, + "half_unpaid_leaves": unpaid_half, + } + + +def daily_computation(employee, wage, start_date, end_date): + """ + Hourly salary computation for period. + + Args: + employee (obj): Employee instance + wage (float): wage of the employee + start_date (obj): start of the pay period + end_date (obj): end date of the period + """ + working_day_data = get_working_days(start_date, end_date) + total_working_days = working_day_data["total_working_days"] + + leave_data = get_leaves(employee, start_date, end_date) + + contract = employee.contract_set.filter(contract_status="active").first() + basic_pay = wage * total_working_days + loss_of_pay = 0 + + date_range = get_date_range(start_date, end_date) + half_day_leaves_between_period_on_start_date = ( + employee.leaverequest_set.filter( + leave_type_id__payment="unpaid", + start_date__in=date_range, + status="approved", + ) + .exclude(start_date_breakdown="full_day") + .count() + ) + + half_day_leaves_between_period_on_end_date = ( + employee.leaverequest_set.filter( + leave_type_id__payment="unpaid", end_date__in=date_range, status="approved" + ) + .exclude(end_date_breakdown="full_day") + .exclude(start_date=F("end_date")) + .count() + ) + unpaid_half_leaves = ( + half_day_leaves_between_period_on_start_date + + half_day_leaves_between_period_on_end_date + ) * 0.5 + + contract = employee.contract_set.filter( + is_active=True, contract_status="active" + ).first() + + unpaid_leaves = leave_data["unpaid_leaves"] - unpaid_half_leaves + if contract.calculate_daily_leave_amount: + loss_of_pay = (unpaid_leaves) * wage + else: + fixed_penalty = contract.deduction_for_one_leave_amount + loss_of_pay = (unpaid_leaves) * fixed_penalty + if contract.deduct_leave_from_basic_pay: + basic_pay = basic_pay - loss_of_pay + + return { + "basic_pay": basic_pay, + "loss_of_pay": loss_of_pay, + } + + +def get_daily_salary(wage, wage_date) -> dict: + """ + This method is used to calculate daily salary for the date + """ + last_day = calendar.monthrange(wage_date.year, wage_date.month)[1] + end_date = date(wage_date.year, wage_date.month, last_day) + start_date = date(wage_date.year, wage_date.month, 1) + working_days = get_working_days(start_date, end_date)["total_working_days"] + day_wage = wage / working_days + + return { + "day_wage": day_wage, + } + + +def months_between_range(wage, start_date, end_date): + """ + This method is used to find the months between range + """ + months_data = [] + + for current_date in ( + start_date + relativedelta(months=i) + for i in range( + (end_date.year - start_date.year) * 12 + + end_date.month + - start_date.month + + 1 + ) + ): + month = current_date.month + year = current_date.year + + days_in_month = ( + current_date + relativedelta(day=1, months=1) - relativedelta(days=1) + ).day + + # Calculate the end date for the current month + current_end_date = current_date + relativedelta(day=days_in_month) + current_end_date = min(current_end_date, end_date) + working_days_on_month = get_working_days( + current_date.replace(day=1), current_date.replace(day=days_in_month) + )["total_working_days"] + + month_start_date = ( + date(year=year, month=month, day=1) + if start_date < date(year=year, month=month, day=1) + else start_date + ) + total_working_days_on_period = get_working_days( + month_start_date, current_end_date + )["total_working_days"] + + month_info = { + "month": month, + "year": year, + "days": days_in_month, + "start_date": month_start_date.strftime("%Y-%m-%d"), + "end_date": current_end_date.strftime("%Y-%m-%d"), + # month period + "working_days_on_period": total_working_days_on_period, + "working_days_on_month": working_days_on_month, + "per_day_amount": wage / working_days_on_month, + } + + months_data.append(month_info) + # Set the start date for the next month as the first day of the next month + current_date = (current_date + relativedelta(day=1, months=1)).replace(day=1) + + return months_data + + +def monthly_computation(employee, wage, start_date, end_date): + """ + Hourly salary computation for period. + + Args: + employee (obj): Employee instance + wage (float): wage of the employee + start_date (obj): start of the pay period + end_date (obj): end date of the period + """ + basic_pay = 0 + month_data = months_between_range(wage, start_date, end_date) + + leave_data = get_leaves(employee, start_date, end_date) + + for data in month_data: + basic_pay = basic_pay + ( + data["working_days_on_period"] * data["per_day_amount"] + ) + + contract = employee.contract_set.filter(contract_status="active").first() + loss_of_pay = 0 + date_range = get_date_range(start_date, end_date) + half_day_leaves_between_period_on_start_date = ( + employee.leaverequest_set.filter( + leave_type_id__payment="unpaid", + start_date__in=date_range, + status="approved", + ) + .exclude(start_date_breakdown="full_day") + .count() + ) + + half_day_leaves_between_period_on_end_date = ( + employee.leaverequest_set.filter( + leave_type_id__payment="unpaid", end_date__in=date_range, status="approved" + ) + .exclude(end_date_breakdown="full_day") + .exclude(start_date=F("end_date")) + .count() + ) + unpaid_half_leaves = ( + half_day_leaves_between_period_on_start_date + + half_day_leaves_between_period_on_end_date + ) * 0.5 + + contract = employee.contract_set.filter( + is_active=True, contract_status="active" + ).first() + unpaid_leaves = abs(leave_data["unpaid_leaves"] - unpaid_half_leaves) + daily_computed_salary = get_daily_salary(wage=wage, wage_date=start_date)[ + "day_wage" + ] + if contract.calculate_daily_leave_amount: + loss_of_pay = (unpaid_leaves) * daily_computed_salary + else: + fixed_penalty = contract.deduction_for_one_leave_amount + loss_of_pay = (unpaid_leaves) * fixed_penalty + + if contract.deduct_leave_from_basic_pay: + basic_pay = basic_pay - loss_of_pay + return { + "basic_pay": basic_pay, + "loss_of_pay": loss_of_pay, + "month_data": month_data, + } + + +def compute_salary_on_period(employee, start_date, end_date, wage=None): + """ + This method is used to compute salary on the start to end date period + + Args: + employee (obj): Employee instance + start_date (obj): start date of the period + end_date (obj): end date of the period + """ + contract = Contract.objects.filter( + employee_id=employee, contract_status="active" + ).first() + if contract is None: + return contract + + wage = contract.wage if wage is None else wage + wage_type = contract.wage_type + data = None + if wage_type == "hourly": + data = hourly_computation(employee, wage, start_date, end_date) + month_data = months_between_range(wage, start_date, end_date) + data["month_data"] = month_data + elif wage_type == "daily": + data = daily_computation(employee, wage, start_date, end_date) + month_data = months_between_range(wage, start_date, end_date) + data["month_data"] = month_data + + else: + data = monthly_computation(employee, wage, start_date, end_date) + data["contract_wage"] = wage + data["contract"] = contract + return data + + +def paginator_qry(qryset, page_number): + """ + This method is used to paginate queryset + """ + paginator = Paginator(qryset, 50) + qryset = paginator.get_page(page_number) + return qryset diff --git a/payroll/methods/payslip_calc.py b/payroll/methods/payslip_calc.py new file mode 100644 index 000000000..c55bc21ad --- /dev/null +++ b/payroll/methods/payslip_calc.py @@ -0,0 +1,822 @@ +""" +This module contains various functions for calculating payroll-related information for employees. +It includes functions for calculating gross pay, taxable gross pay, allowances, tax deductions, +pre-tax deductions, and post-tax deductions. + +""" +import operator +import contextlib +from attendance.models import Attendance +from payroll.models import models +from payroll.models.models import Contract, Allowance +from payroll.methods.limits import compute_limit + +operator_mapping = { + "equal": operator.eq, + "notequal": operator.ne, + "lt": operator.lt, + "gt": operator.gt, + "le": operator.le, + "ge": operator.ge, + "icontains": operator.contains, +} +filter_mapping = { + "work_type_id": { + "filter": lambda employee, allowance, start_date, end_date: { + "employee_id": employee, + "work_type_id__id": allowance.work_type_id.id, + "attendance_date__range": (start_date, end_date), + "attendance_validated": True, + } + }, + "shift_id": { + "filter": lambda employee, allowance, start_date, end_date: { + "employee_id": employee, + "shift_id__id": allowance.shift_id.id, + "attendance_date__range": (start_date, end_date), + "attendance_validated": True, + } + }, + "overtime": { + "filter": lambda employee, allowance, start_date, end_date: { + "employee_id": employee, + "attendance_date__range": (start_date, end_date), + "attendance_overtime_approve": True, + "attendance_validated": True, + } + }, + "attendance": { + "filter": lambda employee, allowance, start_date, end_date: { + "employee_id": employee, + "attendance_date__range": (start_date, end_date), + "attendance_validated": True, + } + }, +} + + +def dynamic_attr(obj, attribute_path): + """ + Retrieves the value of a nested attribute from a related object dynamically. + + Args: + obj: The base object from which to start accessing attributes. + attribute_path (str): The path of the nested attribute to retrieve, using + double underscores ('__') to indicate relationship traversal. + + Returns: + The value of the nested attribute if it exists, or None if it doesn't exist. + """ + attributes = attribute_path.split("__") + + for attr in attributes: + with contextlib.suppress(Exception): + if isinstance(obj.first(), Contract): + obj = obj.filter(is_active=True).first() + + obj = getattr(obj, attr, None) + if obj is None: + break + return obj + + +def calculate_gross_pay(*_args, **kwargs): + """ + Calculate the gross pay for an employee within a given date range. + + Args: + employee: The employee object for whom to calculate the gross pay. + start_date: The start date of the period for which to calculate the gross pay. + end_date: The end date of the period for which to calculate the gross pay. + + Returns: + A dictionary containing the gross pay as the "gross_pay" key. + + """ + basic_pay = kwargs["basic_pay"] + total_allowance = kwargs["total_allowance"] + # basic_pay = compute_salary_on_period(employee, start_date, end_date)["basic_pay"] + gross_pay = total_allowance + basic_pay + return { + "gross_pay": gross_pay, + "basic_pay": basic_pay, + } + + +def calculate_taxable_gross_pay(*_args, **kwargs): + """ + Calculate the taxable gross pay for an employee within a given date range. + + Args: + employee: The employee object for whom to calculate the taxable gross pay. + start_date: The start date of the period for which to calculate the taxable gross pay. + end_date: The end date of the period for which to calculate the taxable gross pay. + + Returns: + A dictionary containing the taxable gross pay as the "taxable_gross_pay" key. + + """ + allowances = kwargs["allowances"] + gross_pay = calculate_gross_pay(**kwargs) + gross_pay = gross_pay["gross_pay"] + pre_tax_deductions = calculate_pre_tax_deduction(**kwargs) + non_taxable_allowance_total = sum( + allowance["amount"] + for allowance in allowances["allowances"] + if not allowance["is_taxable"] + ) + pretax_deduction_total = sum( + deduction["amount"] + for deduction in pre_tax_deductions["pretax_deductions"] + if deduction["is_pretax"] + ) + taxable_gross_pay = gross_pay - non_taxable_allowance_total - pretax_deduction_total + return { + "taxable_gross_pay": taxable_gross_pay, + } + + +def calculate_allowance(**kwargs): + """ + Calculate the allowances for an employee within the specified payroll period. + + Args: + employee (Employee): The employee object for which to calculate the allowances. + start_date (datetime.date): The start date of the payroll period. + end_date (datetime.date): The end date of the payroll period. + + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + basic_pay = kwargs["basic_pay"] + day_dict = kwargs["day_dict"] + specific_allowances = Allowance.objects.filter(specific_employees=employee) + conditional_allowances = Allowance.objects.filter(is_condition_based=True).exclude( + exclude_employees=employee + ) + active_employees = Allowance.objects.filter(include_active_employees=True).exclude( + exclude_employees=employee + ) + + allowances = specific_allowances | conditional_allowances | active_employees + + allowances = allowances.exclude(one_time_date__lt=start_date).exclude( + one_time_date__gt=end_date + ) + + employee_allowances = [] + tax_allowances = [] + no_tax_allowances = [] + tax_allowances_amt = [] + no_tax_allowances_amt = [] + # Append allowances based on condition, or unconditionally to employee + for allowance in allowances: + if allowance.is_condition_based: + condition_field = allowance.field + condition_operator = allowance.condition + condition_value = allowance.value + employee_value = dynamic_attr(employee, condition_field) + operator_func = operator_mapping.get(condition_operator) + if employee_value is not None: + condition_value = type(employee_value)(condition_value) + if operator_func(employee_value, condition_value): + employee_allowances.append(allowance) + else: + if allowance.based_on in filter_mapping: + filter_params = filter_mapping[allowance.based_on]["filter"]( + employee, allowance, start_date, end_date + ) + if Attendance.objects.filter(**filter_params): + employee_allowances.append(allowance) + else: + employee_allowances.append(allowance) + # Filter and append taxable allowance and not taxable allowance + for allowance in employee_allowances: + if allowance.is_taxable: + tax_allowances.append(allowance) + else: + no_tax_allowances.append(allowance) + # Find and append the amount of tax_allowances + for allowance in tax_allowances: + if allowance.is_fixed: + amount = allowance.amount + kwargs["amount"] = amount + kwargs["component"] = allowance + + amount = if_condition_on(**kwargs) + tax_allowances_amt.append(amount) + else: + calculation_function = calculation_mapping.get(allowance.based_on) + amount = calculation_function( + **{ + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "component": allowance, + "allowances": None, + "total_allowance": None, + "basic_pay": basic_pay, + "day_dict": day_dict, + }, + ) + kwargs["amount"] = amount + kwargs["component"] = allowance + amount = if_condition_on(**kwargs) + tax_allowances_amt.append(amount) + # Find and append the amount of not tax_allowances + for allowance in no_tax_allowances: + if allowance.is_fixed: + amount = allowance.amount + kwargs["amount"] = amount + kwargs["component"] = allowance + amount = if_condition_on(**kwargs) + no_tax_allowances_amt.append(amount) + + else: + calculation_function = calculation_mapping.get(allowance.based_on) + amount = calculation_function( + **{ + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "component": allowance, + "day_dict": day_dict, + "basic_pay": basic_pay, + } + ) + kwargs["amount"] = amount + kwargs["component"] = allowance + amount = if_condition_on(**kwargs) + no_tax_allowances_amt.append(amount) + serialized_allowances = [] + + # Serialize taxable allowances + for allowance, amount in zip(tax_allowances, tax_allowances_amt): + serialized_allowance = { + "title": allowance.title, + "is_taxable": allowance.is_taxable, + "amount": amount, + } + serialized_allowances.append(serialized_allowance) + + # Serialize no-taxable allowances + for allowance, amount in zip(no_tax_allowances, no_tax_allowances_amt): + serialized_allowance = { + "title": allowance.title, + "is_taxable": allowance.is_taxable, + "amount": amount, + } + serialized_allowances.append(serialized_allowance) + return {"allowances": serialized_allowances} + + +def calculate_tax_deduction(*_args, **kwargs): + """ + Calculates the tax deductions for the specified employee within the given date range. + + Args: + employee (Employee): The employee for whom the tax deductions are being calculated. + start_date (date): The start date of the tax deduction period. + end_date (date): The end date of the tax deduction period. + allowances (dict): Dictionary containing the calculated allowances. + total_allowance (float): The total amount of allowances. + basic_pay (float): The basic pay amount. + day_dict (dict): Dictionary containing working day details. + + Returns: + dict: A dictionary containing the serialized tax deductions. + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + specific_deductions = models.Deduction.objects.filter( + specific_employees=employee, is_pretax=False, is_tax=True + ) + active_employee_deduction = models.Deduction.objects.filter( + include_active_employees=True, is_pretax=False, is_tax=True + ).exclude(exclude_employees=employee) + deductions = specific_deductions | active_employee_deduction + deductions = ( + deductions.exclude(one_time_date__lt=start_date) + .exclude(one_time_date__gt=end_date) + .exclude(update_compensation__isnull=False) + ) + deductions_amt = [] + serialized_deductions = [] + for deduction in deductions: + calculation_function = calculation_mapping.get(deduction.based_on) + amount = calculation_function( + **{ + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "component": deduction, + "allowances": kwargs["allowances"], + "total_allowance": kwargs["total_allowance"], + "basic_pay": kwargs["basic_pay"], + "day_dict": kwargs["day_dict"], + } + ) + kwargs["amount"] = amount + kwargs["component"] = deduction + amount = if_condition_on(**kwargs) + deductions_amt.append(amount) + for deduction, amount in zip(deductions, deductions_amt): + serialized_deduction = { + "title": deduction.title, + "is_tax": deduction.is_tax, + "amount": amount, + } + serialized_deductions.append(serialized_deduction) + return {"tax_deductions": serialized_deductions} + + +def calculate_pre_tax_deduction(*_args, **kwargs): + """ + This function retrieves pre-tax deductions applicable to the employee and calculates + their amounts + + Args: + employee: The employee object for whom to calculate the pre-tax deductions. + start_date: The start date of the period for which to calculate the pre-tax deductions. + end_date: The end date of the period for which to calculate the pre-tax deductions. + + Returns: + A dictionary containing the pre-tax deductions as the "pretax_deductions" key. + + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + + specific_deductions = models.Deduction.objects.filter( + specific_employees=employee, is_pretax=True, is_tax=False + ) + conditional_deduction = models.Deduction.objects.filter( + is_condition_based=True, is_pretax=True, is_tax=False + ).exclude(exclude_employees=employee) + active_employee_deduction = models.Deduction.objects.filter( + include_active_employees=True, is_pretax=True, is_tax=False + ).exclude(exclude_employees=employee) + + deductions = specific_deductions | conditional_deduction | active_employee_deduction + deductions = ( + deductions.exclude(one_time_date__lt=start_date) + .exclude(one_time_date__gt=end_date) + .exclude(update_compensation__isnull=False) + ) + pre_tax_deductions = [] + pre_tax_deductions_amt = [] + serialized_deductions = [] + + for deduction in deductions: + if deduction.is_condition_based: + condition_field = deduction.field + condition_operator = deduction.condition + condition_value = deduction.value + employee_value = dynamic_attr(employee, condition_field) + operator_func = operator_mapping.get(condition_operator) + + if employee_value is not None: + condition_value = type(employee_value)(condition_value) + if operator_func(employee_value, condition_value): + pre_tax_deductions.append(deduction) + else: + pre_tax_deductions.append(deduction) + + for deduction in pre_tax_deductions: + if deduction.is_fixed: + kwargs["amount"] = deduction.amount + kwargs["component"] = deduction + pre_tax_deductions_amt.append(if_condition_on(**kwargs)) + else: + calculation_function = calculation_mapping.get(deduction.based_on) + amount = calculation_function( + **{ + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "component": deduction, + "allowances": kwargs["allowances"], + "total_allowance": kwargs["total_allowance"], + "basic_pay": kwargs["basic_pay"], + "day_dic": kwargs["day_dict"], + } + ) + kwargs["amount"] = amount + kwargs["component"] = deduction + pre_tax_deductions_amt.append(if_condition_on(**kwargs)) + for deduction, amount in zip(pre_tax_deductions, pre_tax_deductions_amt): + serialized_deduction = { + "title": deduction.title, + "is_pretax": deduction.is_pretax, + "amount": amount, + } + serialized_deductions.append(serialized_deduction) + return {"pretax_deductions": serialized_deductions} + + +def calculate_post_tax_deduction(*_args, **kwargs): + """ + This function retrieves post-tax deductions applicable to the employee and calculates + their amounts + + Args: + employee: The employee object for whom to calculate the pre-tax deductions. + start_date: The start date of the period for which to calculate the pre-tax deductions. + end_date: The end date of the period for which to calculate the pre-tax deductions. + + Returns: + A dictionary containing the pre-tax deductions as the "post_tax_deductions" key. + + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + allowances = kwargs["allowances"] + total_allowance = kwargs["total_allowance"] + basic_pay = kwargs["basic_pay"] + day_dict = kwargs["day_dict"] + specific_deductions = models.Deduction.objects.filter( + specific_employees=employee, is_pretax=False, is_tax=False + ) + conditional_deduction = models.Deduction.objects.filter( + is_condition_based=True, is_pretax=False, is_tax=False + ).exclude(exclude_employees=employee) + active_employee_deduction = models.Deduction.objects.filter( + include_active_employees=True, is_pretax=False, is_tax=False + ).exclude(exclude_employees=employee) + deductions = specific_deductions | conditional_deduction | active_employee_deduction + deductions = ( + deductions.exclude(one_time_date__lt=start_date) + .exclude(one_time_date__gt=end_date) + .exclude(update_compensation__isnull=False) + ) + post_tax_deductions = [] + post_tax_deductions_amt = [] + serialized_deductions = [] + serialized_net_pay_deductions = [] + + for deduction in deductions: + if deduction.is_condition_based: + condition_field = deduction.field + condition_operator = deduction.condition + condition_value = deduction.value + employee_value = dynamic_attr(employee, condition_field) + operator_func = operator_mapping.get(condition_operator) + if employee_value is not None: + condition_value = type(employee_value)(condition_value) + if operator_func(employee_value, condition_value): + post_tax_deductions.append(deduction) + else: + post_tax_deductions.append(deduction) + for deduction in post_tax_deductions: + if deduction.is_fixed: + amount = deduction.amount + kwargs["amount"] = amount + kwargs["component"] = deduction + amount = if_condition_on(**kwargs) + post_tax_deductions_amt.append(amount) + else: + if deduction.based_on != "net_pay": + calculation_function = calculation_mapping.get(deduction.based_on) + amount = calculation_function( + **{ + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "component": deduction, + "allowances": allowances, + "total_allowance": total_allowance, + "basic_pay": basic_pay, + "day_dict": day_dict, + } + ) + kwargs["amount"] = amount + kwargs["component"] = deduction + amount = if_condition_on(**kwargs) + post_tax_deductions_amt.append(amount) + + for deduction, amount in zip(post_tax_deductions, post_tax_deductions_amt): + serialized_deduction = { + "title": deduction.title, + "is_pretax": deduction.is_pretax, + "amount": amount, + } + serialized_deductions.append(serialized_deduction) + for deduction in post_tax_deductions: + if deduction.based_on == "net_pay": + serialized_net_pay_deduction = {"deduction": deduction} + serialized_net_pay_deductions.append(serialized_net_pay_deduction) + return { + "post_tax_deductions": serialized_deductions, + "net_pay_deduction": serialized_net_pay_deductions, + } + + +def calculate_net_pay_deduction(net_pay, net_pay_deductions, day_dict): + """ + Calculates the deductions based on the net pay amount. + + Args: + net_pay (float): The net pay amount. + net_pay_deductions (list): List of net pay deductions. + day_dict (dict): Dictionary containing working day details. + + Returns: + dict: A dictionary containing the serialized deductions and deduction amount. + """ + serialized_net_pay_deductions = [] + deductions = [item["deduction"] for item in net_pay_deductions] + deduction_amt = [] + for deduction in deductions: + amount = calculate_based_on_net_pay(deduction, net_pay, day_dict) + deduction_amt.append(amount) + net_deduction = 0 + for deduction, amount in zip(deductions, deduction_amt): + serialized_deduction = { + "title": deduction.title, + "is_pretax": deduction.is_pretax, + "amount": amount, + } + net_deduction = amount + net_deduction + serialized_net_pay_deductions.append(serialized_deduction) + return { + "net_pay_deductions": serialized_net_pay_deductions, + "net_deduction": net_deduction, + } + + +def if_condition_on(*_args, **kwargs): + """ + This method is used to check the allowance or deduction through the the conditions + + Args: + employee (obj): Employee instance + amount (float): calculated amount of the component + component (obj): Allowance or Deduction instance + start_date (obj): Start date of the period + end_date (obj): End date of the period + + Returns: + _type_: _description_ + """ + component = kwargs["component"] + basic_pay = kwargs["basic_pay"] + amount = kwargs["amount"] + gross_pay = 0 + amount = float(amount) + if not isinstance(component, Allowance): + gross_pay = calculate_gross_pay( + **kwargs, + )["gross_pay"] + operator_func = operator_mapping.get(component.if_condition) + condition_value = basic_pay if component.if_choice == "basic_pay" else gross_pay + if not operator_func(condition_value, component.if_amount): + amount = 0 + return amount + + +def calculate_based_on_basic_pay(*_args, **kwargs): + """ + Calculate the amount of an allowance or deduction based on the employee's + basic pay with rate provided in the allowance or deduction object + + Args: + employee (Employee): The employee object for whom to calculate the amount. + start_date (datetime.date): The start date of the period for which to calculate the amount. + end_date (datetime.date): The end date of the period for which to calculate the amount. + component (Component): The allowance or deduction object that defines the rate or percentage + to apply. + + Returns: + The calculated allowance or deduction amount based on the employee's basic pay. + + """ + component = kwargs["component"] + basic_pay = kwargs["basic_pay"] + day_dict = kwargs["day_dict"] + rate = component.rate + amount = basic_pay * rate / 100 + amount = compute_limit(component, amount, day_dict) + + return amount + + +def calculate_based_on_gross_pay(*_args, **kwargs): + """ + Calculate the amount of an allowance or deduction based on the employee's gross pay with rate + provided in the allowance or deduction object + + Args: + employee (Employee): The employee object for whom to calculate the amount. + start_date (datetime.date): The start date of the period for which to calculate the amount. + end_date (datetime.date): The end date of the period for which to calculate the amount. + component (Component): The allowance or deduction object that defines the rate or percentage + to apply. + + Returns:+- + The calculated allowance or deduction amount based on the employee's gross pay. + + """ + + component = kwargs["component"] + gross_pay = calculate_gross_pay(**kwargs) + rate = component.rate + amount = gross_pay["gross_pay"] * rate / 100 + return amount + + +def calculate_based_on_taxable_gross_pay(*_args, **kwargs): + """ + Calculate the amount of an allowance or deduction based on the employee's taxable gross pay with + rate provided in the allowance or deduction object + + Args: + employee (Employee): The employee object for whom to calculate the amount. + start_date (datetime.date): The start date of the period for which to calculate the amount. + end_date (datetime.date): The end date of the period for which to calculate the amount. + component (Component): The allowance or deduction object that defines the rate or percentage + to apply. + + Returns: + The calculated component amount based on the employee's taxable gross pay. + + """ + component = kwargs["component"] + taxable_gross_pay = calculate_taxable_gross_pay(**kwargs) + taxable_gross_pay = taxable_gross_pay["taxable_gross_pay"] + rate = component.rate + amount = taxable_gross_pay * rate / 100 + return amount + + +def calculate_based_on_net_pay(component, net_pay, day_dict): + """ + Calculates the amount of an allowance or deduction based on the net pay of an employee. + + Args: + component (Allowance or Deduction): The allowance or deduction object. + net_pay (float): The net pay of the employee. + day_dict (dict): Dictionary containing working day details. + + Returns: + float: The calculated amount of the component based on the net pay. + """ + rate = float(component.rate) + amount = net_pay * rate / 100 + amount = compute_limit(component, amount, day_dict) + + amount = compute_limit(component, amount, day_dict) + return amount + + +def calculate_based_on_attendance(*_args, **kwargs): + """ + Calculates the amount of an allowance or deduction based on the attendance of an employee. + + Args: + employee (Employee): The employee for whom the attendance is being calculated. + start_date (date): The start date of the attendance period. + end_date (date): The end date of the attendance period. + component (Allowance or Deduction): The allowance or deduction object. + day_dict (dict): Dictionary containing working day details. + + Returns: + float: The calculated amount of the component based on the attendance. + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + component = kwargs["component"] + day_dict = kwargs["day_dict"] + + count = Attendance.objects.filter( + employee_id=employee, + attendance_date__range=(start_date, end_date), + attendance_validated=True, + ).count() + amount = count * component.per_attendance_fixed_amount + + amount = compute_limit(component, amount, day_dict) + + return amount + + +def calculate_based_on_shift(*_args, **kwargs): + """ + Calculates the amount of an allowance or deduction based on the employee's shift attendance. + + Args: + employee (Employee): The employee for whom the shift attendance is being calculated. + start_date (date): The start date of the attendance period. + end_date (date): The end date of the attendance period. + component (Allowance or Deduction): The allowance or deduction object. + day_dict (dict): Dictionary containing working day details. + + Returns: + float: The calculated amount of the component based on the shift attendance. + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + component = kwargs["component"] + day_dict = kwargs["day_dict"] + + shift_id = component.shift_id.id + count = Attendance.objects.filter( + employee_id=employee, + shift_id=shift_id, + attendance_date__range=(start_date, end_date), + attendance_validated=True, + ).count() + amount = count * component.shift_per_attendance_amount + + amount = compute_limit(component, amount, day_dict) + return amount + + +def calculate_based_on_overtime(*_args, **kwargs): + """ + Calculates the amount of an allowance or deduction based on employee's overtime hours. + + Args: + employee (Employee): The employee for whom the overtime is being calculated. + start_date (date): The start date of the overtime period. + end_date (date): The end date of the overtime period. + component (Allowance or Deduction): The allowance or deduction object. + day_dict (dict): Dictionary containing working day details. + + Returns: + float: The calculated amount of the allowance or deduction based on the overtime hours. + """ + + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + component = kwargs["component"] + day_dict = kwargs["day_dict"] + + attendances = Attendance.objects.filter( + employee_id=employee, + attendance_date__range=(start_date, end_date), + attendance_overtime_approve=True, + ) + overtime = sum(attendance.overtime_second for attendance in attendances) + amount_per_hour = component.amount_per_one_hr + amount_per_second = amount_per_hour / (60 * 60) + amount = overtime * amount_per_second + amount = round(amount, 2) + + amount = compute_limit(component, amount, day_dict) + + return amount + + +def calculate_based_on_work_type(*_args, **kwargs): + """ + Calculates the amount of an allowance or deduction based on the employee's + attendance with a specific work type. + + Args: + employee (Employee): The employee for whom the attendance is being considered. + start_date (date): The start date of the attendance period. + end_date (date): The end date of the attendance period. + component (Allowance or Deduction): The allowance or deduction object. + day_dict (dict): Dictionary containing working day details. + + Returns: + float: The calculated amount of the allowance or deduction based on the + attendance with the specified work type. + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + component = kwargs["component"] + day_dict = kwargs["day_dict"] + + work_type_id = component.work_type_id.id + count = Attendance.objects.filter( + employee_id=employee, + work_type_id=work_type_id, + attendance_date__range=(start_date, end_date), + attendance_validated=True, + ).count() + amount = count * component.work_type_per_attendance_amount + + amount = compute_limit(component, amount, day_dict) + + return amount + + +calculation_mapping = { + "basic_pay": calculate_based_on_basic_pay, + "gross_pay": calculate_based_on_gross_pay, + "taxable_gross_pay": calculate_based_on_taxable_gross_pay, + "net_pay": calculate_based_on_net_pay, + "attendance": calculate_based_on_attendance, + "shift_id": calculate_based_on_shift, + "overtime": calculate_based_on_overtime, + "work_type_id": calculate_based_on_work_type, +} diff --git a/payroll/methods/tax_calc.py b/payroll/methods/tax_calc.py new file mode 100644 index 000000000..43114533c --- /dev/null +++ b/payroll/methods/tax_calc.py @@ -0,0 +1,78 @@ +""" +Module: payroll.tax_calc + +This module contains a function for calculating the taxable amount for an employee +based on their contract details and income information. +""" +import datetime +from payroll.methods.payslip_calc import ( + calculate_taxable_gross_pay, + calculate_gross_pay, +) +from payroll.models.models import Contract +from payroll.models.tax_models import TaxBracket + + +def calculate_taxable_amount(**kwargs): + """Calculate the taxable amount for a given employee within a specific period. + + Args: + employee (int): The ID of the employee. + start_date (datetime.date): The start date of the period. + end_date (datetime.date): The end date of the period. + allowances (int): The number of allowances claimed by the employee. + total_allowance (float): The total allowance amount. + basic_pay (float): The basic pay amount. + day_dict (dict): A dictionary containing specific day-related information. + + Returns: + float: The federal tax amount for the specified period. + """ + employee = kwargs["employee"] + start_date = kwargs["start_date"] + end_date = kwargs["end_date"] + basic_pay = kwargs["basic_pay"] + contract = Contract.objects.filter(employee_id=employee,contract_status='active').first() + filing = contract.filing_status + federal_tax_for_period = 0 + if filing is not None: + based = filing.based_on + num_days = (end_date - start_date).days + 1 + calculation_functions = { + "taxable_gross_pay": calculate_taxable_gross_pay, + "gross_pay": calculate_gross_pay, + } + if based in calculation_functions: + calculation_function = calculation_functions[based] + income = calculation_function(**kwargs) + income = float(income[based]) + else: + income = float(basic_pay) + year = end_date.year + check_start_date = datetime.date(year, 1, 1) + check_end_date = datetime.date(year, 12, 31) + total_days = (check_end_date - check_start_date).days + 1 + yearly_income = income / num_days * total_days + yearly_income = round(yearly_income, 2) + tax_brackets = TaxBracket.objects.filter(filing_status_id=filing).order_by( + "min_income" + ) + federal_tax = 0 + remaining_income = yearly_income + if tax_brackets.exists(): + if tax_brackets.first().min_income <= yearly_income: + for tax_bracket in tax_brackets: + min_income = tax_bracket.min_income + max_income = tax_bracket.max_income + tax_rate = tax_bracket.tax_rate + if remaining_income <= 0: + break + taxable_amount = min(remaining_income, max_income - min_income) + tax_amount = taxable_amount * tax_rate / 100 + federal_tax += tax_amount + remaining_income -= taxable_amount + daily_federal_tax = federal_tax / total_days + federal_tax_for_period = daily_federal_tax * num_days + else: + federal_tax_for_period = 0 + return federal_tax_for_period diff --git a/payroll/migrations/__init__.py b/payroll/migrations/__init__.py new file mode 100644 index 000000000..990750e89 --- /dev/null +++ b/payroll/migrations/__init__.py @@ -0,0 +1,3 @@ +from payroll import settings +from payroll import context_processors +from payroll import scheduler diff --git a/payroll/models/__init__.py b/payroll/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/models/models.py b/payroll/models/models.py new file mode 100644 index 000000000..51b80f3d5 --- /dev/null +++ b/payroll/models/models.py @@ -0,0 +1,1183 @@ +""" +models.py +Used to register models +""" +from datetime import date, datetime, timedelta +from django import forms +from django.db import models +from django.dispatch import receiver +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +from django.db.models.signals import pre_save, pre_delete +from django.http import QueryDict +from employee.models import EmployeeWorkInformation +from employee.models import Employee, Department, JobPosition +from base.models import EmployeeShift, WorkType, JobRole +from attendance.models import ( + validate_time_format, +) +from attendance.models import ( + Attendance, + strtime_seconds, +) + +from leave.models import LeaveRequest + + +# Create your models here. + + +def get_date_range(start_date, end_date): + """ + Returns a list of all dates within a given date range. + + Args: + start_date (date): The start date of the range. + end_date (date): The end date of the range. + + Returns: + list: A list of date objects representing all dates within the range. + + Example: + start_date = date(2023, 1, 1) + end_date = date(2023, 1, 10) + date_range = get_date_range(start_date, end_date) + for date_obj in date_range: + print(date_obj) + """ + date_list = [] + delta = end_date - start_date + + for i in range(delta.days + 1): + current_date = start_date + timedelta(days=i) + date_list.append(current_date) + + return date_list + + +class FilingStatus(models.Model): + """ + FilingStatus model + """ + + based_on_choice = [ + ("basic_pay", _("Basic Pay")), + ("gross_pay", _("Gross Pay")), + ("taxable_gross_pay", _("Taxable Gross Pay")), + ] + filing_status = models.CharField( + max_length=30, + blank=False, + verbose_name=_("Filing status"), + ) + based_on = models.CharField( + max_length=255, + choices=based_on_choice, + null=False, + blank=False, + default="taxable_gross_pay", + verbose_name=_("Based on"), + ) + description = models.TextField( + blank=True, + verbose_name=_("Description"), + ) + + objects = models.Manager() + + def __str__(self) -> str: + return str(self.filing_status) + + +class Contract(models.Model): + """ + Contract Model + """ + + COMPENSATION_CHOICES = ( + ("salary", _("Salary")), + ("hourly", _("Hourly")), + ("commission", _("Commission")), + ) + + PAY_FREQUENCY_CHOICES = ( + ("weekly", _("Weekly")), + ("monthly", _("Monthly")), + ("semi_monthly", _("Semi-Monthly")), + ) + WAGE_CHOICES = ( + ("hourly", _("Hourly")), + ("daily", _("Daily")), + ("monthly", _("Monthly")), + ) + CONTRACT_STATUS_CHOICES = ( + ("draft", _("Draft")), + ("active", _("Active")), + ("expired", _("Expired")), + ("terminated", _("Terminated")), + ) + + contract_name = models.CharField(max_length=250, help_text=_("Contract Title")) + employee_id = models.ForeignKey( + Employee, + on_delete=models.CASCADE, + related_name="contract_set", + verbose_name=_("Employee"), + ) + contract_start_date = models.DateField() + contract_end_date = models.DateField(null=True, blank=True) + wage_type = models.CharField( + choices=WAGE_CHOICES, max_length=250, default="monthly" + ) + wage = models.FloatField(verbose_name=_("Basic Salary"), null=True, default=0) + calculate_daily_leave_amount = models.BooleanField(default=True) + deduction_for_one_leave_amount = models.FloatField(null=True, blank=True, default=0) + deduct_leave_from_basic_pay = models.BooleanField(default=True) + department = models.ForeignKey( + Department, + on_delete=models.DO_NOTHING, + null=True, + blank=True, + related_name="contracts", + ) + job_position = models.ForeignKey( + JobPosition, + on_delete=models.DO_NOTHING, + null=True, + blank=True, + related_name="contracts", + ) + job_role = models.ForeignKey( + JobRole, + on_delete=models.DO_NOTHING, + null=True, + blank=True, + related_name="contracts", + ) + shift = models.ForeignKey( + EmployeeShift, + on_delete=models.DO_NOTHING, + null=True, + blank=True, + related_name="contracts", + ) + work_type = models.ForeignKey( + WorkType, + on_delete=models.DO_NOTHING, + null=True, + blank=True, + related_name="contracts", + ) + filing_status = models.ForeignKey( + FilingStatus, + on_delete=models.DO_NOTHING, + related_name="contracts", + null=True, + blank=True, + ) + pay_frequency = models.CharField( + max_length=20, null=True, choices=PAY_FREQUENCY_CHOICES, default="monthly" + ) + contract_document = models.FileField(upload_to="uploads/", null=True, blank=True) + is_active = models.BooleanField(default=True) + contract_status = models.CharField( + choices=CONTRACT_STATUS_CHOICES, max_length=250, default="draft" + ) + note = models.TextField(null=True, blank=True) + + objects = models.Manager() + + def __str__(self) -> str: + return f"{self.contract_name} -{self.contract_start_date} - {self.contract_end_date}" + + def clean(self): + if self.contract_end_date is not None: + if self.contract_end_date < self.contract_start_date: + raise ValidationError( + {"contract_end_date": _("End date must be greater than start date")} + ) + if ( + self.contract_status == "active" + and Contract.objects.filter( + employee_id=self.employee_id, contract_status="active" + ) + .exclude(id=self.id) + .count() + >= 1 + ): + raise forms.ValidationError( + _("An active contract already exists for this employee.") + ) + if ( + self.contract_status == "draft" + and Contract.objects.filter( + employee_id=self.employee_id, contract_status="draft" + ) + .exclude(id=self.id) + .count() + >= 1 + ): + raise forms.ValidationError( + _("A draft contract already exists for this employee.") + ) + + if self.wage_type in ["daily", "monthly"]: + if not self.calculate_daily_leave_amount: + if self.deduction_for_one_leave_amount is None: + raise ValidationError( + {"deduction_for_one_leave_amount": _("This field is required")} + ) + + def save(self, *args, **kwargs): + if EmployeeWorkInformation.objects.filter( + employee_id=self.employee_id + ).exists(): + if self.department is None: + self.department = self.employee_id.employee_work_info.department_id + + if self.job_position is None: + self.job_position = self.employee_id.employee_work_info.job_position_id + + if self.job_role is None: + self.job_role = self.employee_id.employee_work_info.job_role_id + + if self.work_type is None: + self.work_type = self.employee_id.employee_work_info.work_type_id + + if self.shift is None: + self.shift = self.employee_id.employee_work_info.shift_id + if self.contract_end_date is not None and self.contract_end_date < date.today(): + self.contract_status = "expired" + if ( + self.contract_status == "active" + and Contract.objects.filter( + employee_id=self.employee_id, contract_status="active" + ) + .exclude(id=self.id) + .count() + >= 1 + ): + raise forms.ValidationError( + _("An active contract already exists for this employee.") + ) + + if ( + self.contract_status == "draft" + and Contract.objects.filter( + employee_id=self.employee_id, contract_status="draft" + ) + .exclude(id=self.id) + .count() + >= 1 + ): + raise forms.ValidationError( + _("A draft contract already exists for this employee.") + ) + + super().save(*args, **kwargs) + return self + + class Meta: + """ + Meta class to add additional options + """ + + unique_together = ["employee_id", "contract_start_date", "contract_end_date"] + + +class WorkRecord(models.Model): + """ + WorkRecord Model + """ + + choices = [ + ("FDP", _("Present")), + ("HDP", _("Half Day Present")), + ("ABS", _("Absent")), + ("HD", _("Holiday/Company Leave")), + ("CONF", _("Conflict")), + ("DFT", _("Draft")), + ] + + record_name = models.CharField(max_length=250, null=True, blank=True) + work_record_type = models.CharField(max_length=5, null=True, choices=choices) + employee_id = models.ForeignKey( + Employee, on_delete=models.CASCADE, verbose_name=_("Employee") + ) + date = models.DateField(null=True, blank=True) + at_work = models.CharField( + null=True, + blank=True, + validators=[ + validate_time_format, + ], + default="00:00", + max_length=5, + ) + min_hour = models.CharField( + null=True, + blank=True, + validators=[ + validate_time_format, + ], + default="00:00", + max_length=5, + ) + at_work_second = models.IntegerField(null=True, blank=True, default=0) + min_hour_second = models.IntegerField(null=True, blank=True, default=0) + note = models.TextField() + color = models.CharField( + max_length=10, + null=True, + blank=True, + ) + message = models.CharField(max_length=30, null=True, blank=True) + is_attendance_record = models.BooleanField(default=False) + is_leave_record = models.BooleanField(default=False) + day_percentage = models.FloatField(default=0) + objects = models.Manager() + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + def clean(self): + super().clean() + if not 0.0 <= self.day_percentage <= 1.0: + raise ValidationError(_("Day percentage must be between 0.0 and 1.0")) + + def __str__(self): + return ( + self.record_name + if self.record_name is not None + else f"{self.work_record_type}-{self.date}" + ) + + +class OverrideAttendance(Attendance): + """ + Class to override Attendance model save method + """ + + # Additional fields and methods specific to AnotherModel + @receiver(pre_save, sender=Attendance) + def attendance_pre_save(sender, instance, **_kwargs): + """ + Overriding Attendance model save method + """ + min_hour_second = strtime_seconds(instance.minimum_hour) + at_work_second = strtime_seconds(instance.attendance_worked_hour) + status = "FDP" if instance.at_work_second >= min_hour_second else "HDP" + status = ( + "CONF" + if WorkRecord.objects.filter( + date=instance.attendance_date, + is_attendance_record=False, + employee_id=instance.employee_id, + ).exists() + or instance.attendance_validated is False + else status + ) + message = _("Validate the attendance") if status == "CONF" else _("Validated") + message = ( + _("Incomplete minimum hour") + if status == "HDP" and min_hour_second / 2 > at_work_second + else message + ) + work_record = ( + WorkRecord() + if not WorkRecord.objects.filter( + is_attendance_record=True, + date=instance.attendance_date, + employee_id=instance.employee_id, + ).exists() + else WorkRecord.objects.filter( + is_attendance_record=True, + date=instance.attendance_date, + employee_id=instance.employee_id, + ).first() + ) + work_record.employee_id = instance.employee_id + work_record.date = instance.attendance_date + work_record.at_work = instance.attendance_worked_hour + work_record.min_hour = instance.minimum_hour + work_record.min_hour_second = min_hour_second + work_record.at_work_second = at_work_second + work_record.work_record_type = status + work_record.message = message + work_record.is_attendance_record = True + if instance.attendance_validated: + work_record.day_percentage = ( + 1.00 if at_work_second > min_hour_second / 2 else 0.50 + ) + work_record.save() + + @receiver(pre_delete, sender=Attendance) + def attendance_pre_delete(sender, instance, **_kwargs): + """ + Overriding Attendance model delete method + """ + # Perform any actions before deleting the instance + # ... + WorkRecord.objects.filter( + employee_id=instance.employee_id, + is_attendance_record=True, + date=instance.attendance_date, + ).delete() + + +class OverrideLeaveRequest(LeaveRequest): + """ + Class to override Attendance model save method + """ + + # Additional fields and methods specific to AnotherModel + @receiver(pre_save, sender=LeaveRequest) + def leaverequest_pre_save(sender, instance, **_kwargs): + """ + Overriding LeaveRequest model save method + """ + if ( + instance.start_date == instance.end_date + and instance.end_date_breakdown != "full_day" + ): + instance.end_date_breakdown = "full_day" + + super(LeaveRequest, instance).save() + + period_dates = get_date_range(instance.start_date, instance.end_date) + if instance.status == "approved": + for date in period_dates: + work_entry = ( + WorkRecord.objects.filter( + is_leave_record=True, + date=date, + employee_id=instance.employee_id, + ) + if WorkRecord.objects.filter( + is_leave_record=True, + date=date, + employee_id=instance.employee_id, + ).exists() + else WorkRecord() + ) + work_entry.employee_id = instance.employee_id + work_entry.is_leave_record = True + work_entry.day_percentage = ( + 0.50 + if instance.start_date == date + and instance.start_date_breakdown == "first_half" + or instance.end_date == date + and instance.end_date_breakdown == "second_half" + else 0.00 + ) + # scheduler task to validate the conflict entry for half day if they + # take half day leave is when they mark the attendance. + status = ( + "CONF" + if instance.start_date == date + and instance.start_date_breakdown == "first_half" + or instance.end_date == date + and instance.end_date_breakdown == "second_half" + else "ABS" + ) + work_entry.work_record_type = status + work_entry.date = date + work_entry.message = ( + "Validated" if status == "ABS" else _("Half day need to validate") + ) + work_entry.save() + + else: + for date in period_dates: + WorkRecord.objects.filter( + is_leave_record=True, date=date, employee_id=instance.employee_id + ).delete() + + +class OverrideWorkInfo(EmployeeWorkInformation): + """ + This class is to override the Model default methods + """ + + @receiver(pre_save, sender=EmployeeWorkInformation) + def employeeworkinformation_pre_save(sender, instance, **_kwargs): + """ + This method is used to override the save method for EmployeeWorkInformation Model + """ + active_employee = ( + instance.employee_id if instance.employee_id.is_active == True else None + ) + if active_employee is not None: + contract_exists = active_employee.contract_set.exists() + if not contract_exists: + contract = Contract() + contract.contract_name = f"{active_employee}'s Contract" + contract.employee_id = active_employee + contract.contract_start_date = datetime.today() + contract.wage = ( + instance.basic_salary if instance.basic_salary is not None else 0 + ) + contract.save() + + +# Create your models here. +def rate_validator(value): + """ + Percentage validator + """ + if value < 0: + raise ValidationError(_("Rate must be greater than 0")) + if value > 100: + raise ValidationError(_("Rate must be less than 100")) + + +def min_zero(value): + """ + The minimum value zero validation method + """ + if value < 0: + raise ValidationError(_("Value must be greater than zero")) + + +CONDITION_CHOICE = [ + ("equal", _("Equal (==)")), + ("notequal", _("Not Equal (!=)")), + ("lt", _("Less Than (<)")), + ("gt", _("Greater Than (>)")), + ("le", _("Less Than or Equal To (<=)")), + ("ge", _("Greater Than or Equal To (>=)")), + ("icontains", _("Contains")), +] +IF_CONDITION_CHOICE = [ + ("equal", _("Equal (==)")), + ("notequal", _("Not Equal (!=)")), + ("lt", _("Less Than (<)")), + ("gt", _("Greater Than (>)")), + ("le", _("Less Than or Equal To (<=)")), + ("ge", _("Greater Than or Equal To (>=)")), +] +FIELD_CHOICE = [ + ("children", _("Children")), + ("marital_status", _("Marital Status")), + ("experience", _("Experience")), + ("employee_work_info__experience", _("Company Experience")), + ("gender", _("Gender")), + ("country", _("Country")), + ("state", _("State")), + ("contract_set__pay_frequency", _("Pay Frequency")), + ("contract_set__wage_type", _("Wage Type")), + ("contract_set__department__department", _("Department on Contract")), +] + + +class Allowance(models.Model): + """ + Allowance model + """ + + exceed_choice = [ + ("ignore", _("Exclude the allowance")), + ("max_amount", _("Provide max amount")), + ] + + based_on_choice = [ + ("basic_pay", _("Basic Pay")), + ("attendance", _("Attendance")), + ("shift_id", _("Shift")), + ("overtime", _("Overtime")), + ("work_type_id", _("Work Type")), + ] + + if_condition_choice = [ + ("basic_pay", _("Basic Pay")), + ] + title = models.CharField( + max_length=255, null=False, blank=False, help_text=_("Title of the allowance") + ) + one_time_date = models.DateField( + null=True, + blank=True, + help_text=_( + "The one-time allowance in which the allowance will apply to the payslips \ + if the date between the payslip period" + ), + ) + include_active_employees = models.BooleanField( + default=False, + verbose_name=_("Include all active employees"), + help_text=_("Target allowance to all active employees in the company"), + ) + specific_employees = models.ManyToManyField( + Employee, + verbose_name=_("Employees Specific"), + blank=True, + related_name="allowance_specific", + help_text=_("Target allowance to the specific employees"), + ) + exclude_employees = models.ManyToManyField( + Employee, + verbose_name=_("Exclude Employees"), + related_name="allowance_excluded", + blank=True, + help_text=_( + "To ignore the allowance to the employees when target them by all employees \ + or through condition-based" + ), + ) + is_taxable = models.BooleanField( + default=True, + help_text=_("This field is used to calculate the taxable allowances"), + ) + is_condition_based = models.BooleanField( + default=False, + help_text=_( + "This field is used to target allowance \ + to the specific employees when the condition satisfies with the employee's information" + ), + ) + # If condition based + field = models.CharField( + max_length=255, + choices=FIELD_CHOICE, + null=True, + blank=True, + help_text=_("The related field of the employees"), + ) + condition = models.CharField( + max_length=255, choices=CONDITION_CHOICE, null=True, blank=True + ) + value = models.CharField( + max_length=255, + null=True, + blank=True, + help_text=_("The value must be like the data stored in the database"), + ) + + is_fixed = models.BooleanField( + default=True, help_text=_("To specify, the allowance is fixed or not") + ) + amount = models.FloatField( + null=True, + blank=True, + validators=[min_zero], + help_text=_("Fixed amount for this allowance"), + ) + # If is fixed is false + based_on = models.CharField( + max_length=255, + default="basic_pay", + choices=based_on_choice, + null=True, + blank=True, + help_text=_( + "If the allowance is not fixed then specifies how the allowance provided" + ), + ) + rate = models.FloatField( + null=True, + blank=True, + validators=[ + rate_validator, + ], + help_text=_("The percentage of based on"), + ) + # If based on attendance + per_attendance_fixed_amount = models.FloatField( + null=True, + blank=True, + default=0.00, + validators=[min_zero], + help_text=_("The attendance fixed amount for one validated attendance"), + ) + # If based on shift + shift_id = models.ForeignKey( + EmployeeShift, + on_delete=models.DO_NOTHING, + null=True, + blank=True, + verbose_name=_("Shift"), + ) + shift_per_attendance_amount = models.FloatField( + null=True, + default=0.00, + blank=True, + validators=[min_zero], + help_text=_("The fixed amount for one validated attendance with that shift"), + ) + amount_per_one_hr = models.FloatField( + null=True, + default=0.00, + blank=True, + validators=[min_zero], + help_text=_( + "The fixed amount for one hour overtime that are validated \ + and approved the overtime attendance" + ), + ) + work_type_id = models.ForeignKey( + WorkType, + on_delete=models.DO_NOTHING, + null=True, + blank=True, + verbose_name=_("Work Type"), + ) + work_type_per_attendance_amount = models.FloatField( + null=True, + default=0.00, + blank=True, + validators=[min_zero], + help_text=_( + "The fixed amount for one validated attendance with that work type" + ), + ) + # for apply only + has_max_limit = models.BooleanField( + default=False, + verbose_name=_("Has max limit for allowance"), + help_text=_("Limit the allowance amount"), + ) + maximum_amount = models.FloatField( + null=True, + blank=True, + validators=[min_zero], + help_text=_("The maximum amount for the allowance"), + ) + maximum_unit = models.CharField( + max_length=20, + null=True, + default="month_working_days", + choices=[ + ( + "month_working_days", + _("For working days on month"), + ), + # ("monthly_working_days", "For working days on month"), + ], + help_text="The maximum amount for ?", + ) + if_choice = models.CharField( + max_length=10, + choices=if_condition_choice, + default="basic_pay", + help_text=_("The pay head for the if condition"), + ) + if_condition = models.CharField( + max_length=10, + choices=IF_CONDITION_CHOICE, + default="gt", + help_text=_("Apply for those, if the pay-head conditions satisfy"), + ) + if_amount = models.FloatField( + default=0.00, help_text=_("The amount of the pay-head") + ) + objects = models.Manager() + + class Meta: + """ + Meta class for additional options + """ + + unique_together = [ + "title", + "is_taxable", + "is_condition_based", + "field", + "condition", + "value", + "is_fixed", + "amount", + "based_on", + "rate", + "per_attendance_fixed_amount", + "shift_id", + "shift_per_attendance_amount", + "amount_per_one_hr", + "work_type_id", + "work_type_per_attendance_amount", + ] + verbose_name = _("Allowance") + + def reset_based_on(self): + """Reset the this fields when is_fixed attribute is true""" + attributes_to_reset = [ + "based_on", + "rate", + "per_attendance_fixed_amount", + "shift_id", + "shift_per_attendance_amount", + "amount_per_one_hr", + "work_type_id", + "work_type_per_attendance_amount", + "maximum_amount", + ] + for attribute in attributes_to_reset: + setattr(self, attribute, None) + self.has_max_limit = False + + def clean(self): + super().clean() + self.clean_fixed_attributes() + if not self.is_condition_based: + self.field = None + self.condition = None + self.value = None + if self.is_condition_based: + if not self.field or not self.value or not self.condition: + raise ValidationError( + _( + "If condition based, all fields (field, value, condition) must be filled." + ) + ) + if self.based_on == "attendance" and not self.per_attendance_fixed_amount: + raise ValidationError( + { + "based_on": _( + "If based on is attendance, \ + then per attendance fixed amount must be filled." + ) + } + ) + if self.based_on == "shift_id" and not self.shift_id: + raise ValidationError(_("If based on is shift, then shift must be filled.")) + if self.based_on == "work_type_id" and not self.work_type_id: + raise ValidationError( + _("If based on is work type, then work type must be filled.") + ) + + if self.is_fixed and self.amount < 0: + raise ValidationError({"amount": _("Amount should be greater than zero.")}) + + if self.has_max_limit and self.maximum_amount is None: + raise ValidationError({"maximum_amount": _("This field is required")}) + + if not self.has_max_limit: + self.maximum_amount = None + + def clean_fixed_attributes(self): + """Clean the amount field and trigger the reset_based_on function based on the condition""" + if not self.is_fixed: + self.amount = None + if self.is_fixed: + if self.amount is None: + raise ValidationError({"amount": _("This field is required")}) + self.reset_based_on() + + def __str__(self) -> str: + return str(self.title) + + +class Deduction(models.Model): + """ + Deduction model + """ + + if_condition_choice = [ + ("basic_pay", _("Basic Pay")), + ("gross_pay", _("Gross Pay")), + ] + + based_on_choice = [ + ("basic_pay", _("Basic Pay")), + ("gross_pay", _("Gross Pay")), + ("taxable_gross_pay", _("Taxable Gross Pay")), + ("net_pay", _("Net Pay")), + ] + + exceed_choice = [ + ("ignore", _("Exclude the deduction")), + ("max_amount", _("Provide max amount")), + ] + + title = models.CharField(max_length=255, help_text=_("Title of the deduction")) + one_time_date = models.DateField( + null=True, + blank=True, + help_text=_( + "The one-time deduction in which the deduction will apply to the payslips \ + if the date between the payslip period" + ), + ) + include_active_employees = models.BooleanField( + default=False, + verbose_name=_("Include all active employees"), + help_text=_("Target deduction to all active employees in the company"), + ) + specific_employees = models.ManyToManyField( + Employee, + verbose_name=_("Employees Specific"), + related_name="deduction_specific", + help_text=_("Target deduction to the specific employees"), + blank=True, + ) + exclude_employees = models.ManyToManyField( + Employee, + verbose_name=_("Exclude Employees"), + related_name="deduction_exclude", + blank=True, + help_text=_( + "To ignore the deduction to the employees when target them by all employees \ + or through condition-based" + ), + ) + + is_tax = models.BooleanField( + default=False, + help_text=_("To specify the deduction is tax or normal deduction"), + ) + + is_pretax = models.BooleanField( + default=True, + help_text=_( + "To find taxable gross, \ + taxable_gross = (basic_pay + taxable_deduction)-pre_tax_deductions " + ), + ) + + is_condition_based = models.BooleanField( + default=False, + help_text=_( + "This field is used to target deduction \ + to the specific employees when the condition satisfies with the employee's information" + ), + ) + # If condition based then must fill field, value, and condition, + field = models.CharField( + max_length=255, + choices=FIELD_CHOICE, + null=True, + blank=True, + help_text=_("The related field of the employees"), + ) + condition = models.CharField( + max_length=255, choices=CONDITION_CHOICE, null=True, blank=True + ) + value = models.CharField( + max_length=255, + null=True, + blank=True, + help_text=_("The value must be like the data stored in the database"), + ) + update_compensation = models.CharField( + null=True, + blank=True, + max_length=10, + choices=[ + ( + "basic_pay", + _("Basic pay"), + ), + ("gross_pay", _("Gross Pay")), + ("net_pay", _("Net Pay")), + ], + help_text=_( + "Update compensation is used to update \ + pay-head before any other deduction calculation starts" + ), + ) + is_fixed = models.BooleanField( + default=True, + help_text=_("To specify, the deduction is fixed or not"), + ) + # If fixed amount then fill amount + amount = models.FloatField( + null=True, + blank=True, + validators=[min_zero], + help_text=_("Fixed amount for this deduction"), + ) + based_on = models.CharField( + max_length=255, + choices=based_on_choice, + null=True, + blank=True, + help_text=_( + "If the deduction is not fixed then specifies how the deduction provided" + ), + ) + rate = models.FloatField( + null=True, + blank=True, + default=0.00, + validators=[ + rate_validator, + ], + verbose_name=_("Employee rate"), + help_text=_("The percentage of based on"), + ) + + employer_rate = models.FloatField( + default=0.00, + validators=[ + rate_validator, + ], + ) + has_max_limit = models.BooleanField( + default=False, + verbose_name=_("Has max limit for deduction"), + help_text=_("Limit the deduction"), + ) + maximum_amount = models.FloatField( + null=True, + blank=True, + validators=[min_zero], + help_text=_("The maximum amount for the deduction"), + ) + + maximum_unit = models.CharField( + max_length=20, + null=True, + default="month_working_days", + choices=[ + ("month_working_days", _("For working days on month")), + # ("monthly_working_days", "For working days on month"), + ], + help_text=_("The maximum amount for ?"), + ) + if_choice = models.CharField( + max_length=10, + choices=if_condition_choice, + default="basic_pay", + help_text=_("The pay head for the if condition"), + ) + if_condition = models.CharField( + max_length=10, + choices=IF_CONDITION_CHOICE, + default="gt", + help_text=_("Apply for those, if the pay-head conditions satisfy"), + ) + if_amount = models.FloatField( + default=0.00, help_text=_("The amount of the pay-head") + ) + + objects = models.Manager() + + def clean(self): + super().clean() + + if self.is_tax: + self.is_pretax = False + if self.is_pretax and self.based_on in ["taxable_gross_pay"]: + raise ValidationError( + { + "based_on": _( + " Don't choose taxable gross pay when pretax is enabled." + ) + } + ) + if self.is_pretax and self.based_on in ["net_pay"]: + raise ValidationError( + {"based_on": _(" Don't choose net pay when pretax is enabled.")} + ) + if self.is_tax and self.based_on in ["net_pay"]: + raise ValidationError( + {"based_on": _(" Don't choose net pay when the tax is enabled.")} + ) + if not self.is_fixed: + self.amount = None + else: + self.based_on = None + self.rate = None + self.clean_condition_based_on() + if self.has_max_limit: + if self.maximum_amount is None: + raise ValidationError({"maximum_amount": _("This fields required")}) + + if self.is_condition_based: + if not self.field or not self.value or not self.condition: + raise ValidationError( + { + "is_condition_based": _( + "If condition based, all fields \ + (field, value, condition) must be filled." + ) + } + ) + if self.update_compensation is None: + if self.is_fixed: + if self.amount is None: + raise ValidationError({"amount": _("This field is required")}) + + def clean_condition_based_on(self): + """ + Clean the field, condition, and value attributes when not condition-based. + """ + if not self.is_condition_based: + self.field = None + self.condition = None + self.value = None + + def __str__(self) -> str: + return str(self.title) + + +class Payslip(models.Model): + """ + Payslip model + """ + + status_choices = [ + ("draft", _("Draft")), + ("review_ongoing", _("Review Ongoing")), + ("confirmed", _("Confirmed")), + ("paid", _("Paid")), + ] + reference = models.CharField(max_length=255, unique=False) + employee_id = models.ForeignKey( + Employee, on_delete=models.CASCADE, verbose_name=_("Employee") + ) + start_date = models.DateField() + end_date = models.DateField() + pay_head_data = models.JSONField() + contract_wage = models.FloatField(null=True, default=0) + basic_pay = models.FloatField(null=True, default=0) + gross_pay = models.FloatField(null=True, default=0) + deduction = models.FloatField(null=True, default=0) + net_pay = models.FloatField(null=True, default=0) + status = models.CharField( + max_length=20, null=True, default="draft", choices=status_choices + ) + objects = models.Manager() + + def __str__(self) -> str: + return f"Payslip for {self.employee_id} - Period: {self.start_date} to {self.end_date}" + + def clean(self): + super().clean() + today = date.today() + if self.end_date <= self.start_date: + raise ValidationError( + { + "end_date": _( + "The end date must be greater than or equal to the start date" + ) + } + ) + if self.end_date > today: + raise ValidationError(_("The end date cannot be in the future.")) + if self.start_date > today: + raise ValidationError(_("The start date cannot be in the future.")) + + def save(self, *args, **kwargs): + if ( + Payslip.objects.filter( + employee_id=self.employee_id, + start_date=self.start_date, + end_date=self.end_date, + ).count() + > 1 + ): + raise ValidationError(_("Employee ,start and end date must be unique")) + + if not isinstance(self.pay_head_data, (QueryDict, dict)): + raise ValidationError(_("The data must be in dictionary or querydict type")) + + super().save(*args, **kwargs) + + class Meta: + """ + Meta class for additional options + """ + + ordering = [ + "-end_date", + ] diff --git a/payroll/models/tax_models.py b/payroll/models/tax_models.py new file mode 100644 index 000000000..b6776fb94 --- /dev/null +++ b/payroll/models/tax_models.py @@ -0,0 +1,119 @@ +""" +tax_models.py + +This module contains the models for the tax calculation of taxable income. +""" + + +import math +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.text import format_lazy +from django.utils.translation import gettext_lazy as _ + +from payroll.models.models import FilingStatus + + +class PayrollSettings(models.Model): + """ + Payroll settings model""" + + currency_symbol = models.CharField(null=True, default="$", max_length=5) + objects = models.Manager() + + def __str__(self): + return f"Payroll Settings {self.currency_symbol}" + + def save(self, *args, **kwargs): + if 1 < PayrollSettings.objects.count(): + raise ValidationError("You cannot add more conditions.") + + return super().save(*args, **kwargs) + + +class TaxBracket(models.Model): + """ + TaxBracket model + """ + + filing_status_id = models.ForeignKey( + FilingStatus, + on_delete=models.CASCADE, + verbose_name=_("Filing status"), + ) + min_income = models.FloatField(null=False, blank=False,verbose_name=_("Min. Income")) + max_income = models.FloatField(null=True, blank=True,verbose_name=_("Max. Income")) + tax_rate = models.FloatField(null=False, blank=False, default=0.0,verbose_name=_("Tax Rate")) + + objects = models.Manager() + + def __str__(self): + if self.max_income != math.inf: + return ( + f"{self.filing_status_id}" + f"{self.tax_rate}% tax rate on " + f"{self.min_income} and {self.max_income}" + ) + return ( + f"{self.tax_rate}% tax rate on taxable income equal or above " + f"{self.min_income} for {self.filing_status_id}" + ) + + def get_display_max_income(self): + """ + Retrieves the maximum income. + Returns: + float or None: The maximum income if it is a finite value, otherwise None. + """ + if self.max_income != math.inf: + return self.max_income + return None + + def clean(self): + super().clean() + + existing_bracket = TaxBracket.objects.filter( + filing_status_id=self.filing_status_id, + min_income=self.min_income, + max_income=self.max_income, + tax_rate=self.tax_rate, + ).exclude(pk=self.pk) + if existing_bracket.exists(): + raise ValidationError("This tax bracket already exists") + + if self.max_income is None: + self.max_income = math.inf + + if self.min_income >= self.max_income: + raise ValidationError( + {"max_income": "Maximum income must be greater than minimum income."} + ) + + existing_brackets = TaxBracket.objects.filter( + filing_status_id=self.filing_status_id + ).exclude(pk=self.pk) + if existing_brackets.filter(max_income__gte=self.min_income).exists(): + tax_bracket = existing_brackets.filter( + max_income__gte=self.min_income + ).first() + if tax_bracket.min_income <= self.max_income: + raise ValidationError( + { + "min_income": format_lazy( + "The minimum income of this tax bracket must be \ + greater than the maximum income of {}.", + tax_bracket, + ) + } + ) + + +class FederalTax(models.Model): + """ + FederalTax models + """ + + filing_status_id = models.ForeignKey( + FilingStatus, models.CASCADE, verbose_name=_("Filing Status") + ) + taxable_gross = models.IntegerField(null=False, blank=False) diff --git a/payroll/scheduler.py b/payroll/scheduler.py new file mode 100644 index 000000000..8ad38928d --- /dev/null +++ b/payroll/scheduler.py @@ -0,0 +1,32 @@ +""" +scheduler.py + +This module is used to register scheduled tasks +""" +from datetime import date +from apscheduler.schedulers.background import BackgroundScheduler +from .models.models import Contract + + +def generate_work_entry(): + """ + This is a automated task on time + """ + return + + +def expire_contract(): + """ + Finds all active contracts whose end date is earlier than the current date + and updates their status to "expired". + """ + Contract.objects.filter( + contract_status="active", contract_end_date__lt=date.today() + ).update(contract_status="expired") + return + + +scheduler = BackgroundScheduler() +scheduler.add_job(generate_work_entry, "interval", seconds=10) +scheduler.add_job(expire_contract, "interval", seconds=5) +scheduler.start() diff --git a/payroll/settings.py b/payroll/settings.py new file mode 100644 index 000000000..5161b9ea8 --- /dev/null +++ b/payroll/settings.py @@ -0,0 +1,11 @@ +""" +payroll/settings.py + +This module is used to write settings contents related to payroll app +""" + +from horilla.settings import TEMPLATES + +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "payroll.context_processors.default_currency", +) diff --git a/payroll/templates/common_form.html b/payroll/templates/common_form.html new file mode 100644 index 000000000..4e44f2cf1 --- /dev/null +++ b/payroll/templates/common_form.html @@ -0,0 +1,48 @@ +{% load widget_tweaks %} +{% load i18n %} +
+
+
+
+ {{ form.verbose_name }} +
+
+
+
+
+
+ {{form.non_field_errors}} +
+ {% for field in form.visible_fields %} +
+
+ + {% if field.help_text != "" %} + + + {% endif %} +
+ + {% if field.field.widget.input_type == "checkbox" %} +
+ {{ field|add_class:"oh-switch__checkbox" }} +
+ {% else %} + {{ field|add_class:"form-control" }} + {% endif %} + {{field.errors}} +
+ {% endfor %} +
+ +
+ +
+
+
+ diff --git a/payroll/templates/contract_form.html b/payroll/templates/contract_form.html new file mode 100644 index 000000000..3358ff2c8 --- /dev/null +++ b/payroll/templates/contract_form.html @@ -0,0 +1,46 @@ +{% load widget_tweaks %} +{% load i18n %} + +
+
+
+
+ {{ form.verbose_name }} +
+
+
+
+
+
+ {{form.non_field_errors}} +
+ + {% for field in form.visible_fields %} +
+
+ + {% if field.help_text != "" %} + + + {% endif %} +
+ {% if field.field.widget.input_type == "checkbox" %} {{ field.errors }} +
+ {{ field|add_class:"oh-switch__checkbox" }} +
+ {% else %} {{ field|add_class:"form-control" }} {% endif %} + {{field.errors}} +
+ {% endfor %} +
+ +
+ +
+
+
diff --git a/payroll/templates/payroll/allowance/filter_allowance.html b/payroll/templates/payroll/allowance/filter_allowance.html new file mode 100644 index 000000000..c8dcdac3b --- /dev/null +++ b/payroll/templates/payroll/allowance/filter_allowance.html @@ -0,0 +1,58 @@ +{% load static %} +{% load i18n %} + +
+
+
+
{% trans "Allowance" %}
+
+
+
+
+ + {{ f.form.is_taxable }} +
+
+
+
+ + {{ f.form.is_condition_based }} +
+
+
+
+
+
+ + {{ f.form.is_fixed }} +
+
+
+
+ + {{ f.form.based_on }} +
+
+
+
+
+ {% comment %} {{f.form}} {% endcomment %} +
+ +
+ + diff --git a/payroll/templates/payroll/allowance/list_allowance.html b/payroll/templates/payroll/allowance/list_allowance.html new file mode 100644 index 000000000..e86088f4d --- /dev/null +++ b/payroll/templates/payroll/allowance/list_allowance.html @@ -0,0 +1,146 @@ +{% load i18n %} {% load yes_no %} +
+
+
+
+
+
+
+ +
+
+ {% trans "Allowance" %} +
+
+
+
{% trans "Specific Employees" %}
+
{% trans "Excluded Employees" %}
+
{% trans "Is Taxable" %}
+
{% trans "Is Condition Based" %}
+
{% trans "Condition" %}
+
{% trans "Is Fixed" %}
+
{% trans "Amount" %}
+
{% trans "Based On" %}
+
{% trans "Rate" %}
+
+
+
+ {% for allowance in allowances %} +
+
+
{{allowance.title}}
+
+ {% for employee in allowance.specific_employees.all %} {{employee}}
+ {% endfor %} +
+
+ {% for employee in allowance.exclude_employees.all %} {{employee}}
+ {% endfor %} +
+
+ {{allowance.is_taxable|yesno|capfirst}} +
+
+ {{allowance.is_condition_based|yesno|capfirst}} +
+
+ {% if allowance.field %} {{allowance.get_field_display}} + {{allowance.get_condition_display}} {{allowance.value}} {% endif %} +
+
+ {{allowance.is_fixed|yesno|capfirst}} +
+
+ {% if allowance.amount %}{{allowance.amount}}{% endif %} +
+
+ {% if allowance.get_based_on_display%} + {{allowance.get_based_on_display}} + {% endif %} +
+
+ {% if allowance.rate %}{{allowance.rate}}{% endif %} +
+
+ +
+
+
+ {% endfor %} +
+
+
+ + {% trans "Page" %} {{ allowances.number }} {% trans "of" %} {{ allowances.paginator.num_pages }}. + + +
\ No newline at end of file diff --git a/payroll/templates/payroll/allowance/view_allowance.html b/payroll/templates/payroll/allowance/view_allowance.html new file mode 100644 index 000000000..a598eecb7 --- /dev/null +++ b/payroll/templates/payroll/allowance/view_allowance.html @@ -0,0 +1,109 @@ +{% extends 'index.html' %} {% block content %} {% load i18n %} {% load yes_no %} +
+
+

{% trans "Allowances" %}

+ + + +
+
+
+ + +
+
+ + +
+ +
+
+
+ {% include 'payroll/allowance/list_allowance.html' %} +
+ + + + +{% endblock content %} diff --git a/payroll/templates/payroll/allowance/view_single_allowance.html b/payroll/templates/payroll/allowance/view_single_allowance.html new file mode 100644 index 000000000..418cc2499 --- /dev/null +++ b/payroll/templates/payroll/allowance/view_single_allowance.html @@ -0,0 +1,98 @@ +{% load i18n %} {% load yes_no %} +
+ +
{{allowance.title}}
+
+ +
+
+ {% trans "Taxable" %} + {{allowance.is_taxable|yesno|capfirst}} +
+
+ {% trans "One Time Allowance" %} + {% if allowance.one_time_date %} + {% trans "On" %} {{allowance.one_time_date}} + {% else %} + {% trans "No" %} + {% endif %} +
+
+
+
+ {% trans "Condition Based" %} + {% if allowance.is_condition_based %} + {{allowance.get_field_display}} {{allowance.get_condition_display}} {{allowance.value}} + {% else %} + {% trans "No" %} + {% endif %} +
+
+ {% trans "Amount" %} + {% if allowance.is_fixed %} + {{allowance.amount}} + {% else %} + {% if allowance.based_on == "basic_pay" %} + {{allowance.rate}}% of {{allowance.get_based_on_display}} + {% endif %} + {% if allowance.based_on == "attendance" %} + {{allowance.per_attendance_fixed_amount}} {{currency}} {% trans "Amount Per Attendance" %} + {% endif %} + {% if allowance.based_on == "shift_id" %} + {{allowance.shift_per_attendance_amount}} {{currency}} {% trans "Amount Per" %} {{allowance.shift_id}} + {% endif %} + {% if allowance.based_on == "work_type_id" %} + {{allowance.work_type_per_attendance_amount}} {{currency}} {% trans "Amount Per" %} {{allowance.work_type_id}} + {% endif %} + {% if allowance.based_on == "overtime" %} + {{allowance.amount_per_one_hr}} {{currency}} {% trans "Amount Per One Hour" %} + {% endif %} + {% endif %} +
+
+
+
+ {% trans "Has Maximum Limit" %} + {% if allowance.has_max_limit %} + {{allowance.maximum_amount}} {{currency}} {% if allowance.based_on == "basic_pay" %}{% trans "For working days on a month" %}{% endif %} + {% else %} + {% trans "No" %} + {% endif %} +
+
+ {% trans "Allowance Eligibility" %} + If {{allowance.get_if_choice_display}} {{allowance.get_if_condition_display}} {{allowance.if_amount}} +
+
+
+ +
+
+ + diff --git a/payroll/templates/payroll/common/form.html b/payroll/templates/payroll/common/form.html new file mode 100644 index 000000000..009ff6b71 --- /dev/null +++ b/payroll/templates/payroll/common/form.html @@ -0,0 +1,90 @@ +{% extends 'index.html' %} {% block content %} {% load static %} + +
+
+ {% csrf_token %} {{form.as_p}} +
+
+ +{% endblock content %} diff --git a/payroll/templates/payroll/contract/contract_create.html b/payroll/templates/payroll/contract/contract_create.html new file mode 100644 index 000000000..e26cf6fc0 --- /dev/null +++ b/payroll/templates/payroll/contract/contract_create.html @@ -0,0 +1,31 @@ +{% extends 'index.html' %} {% block content %} {% load static %} +
+
+ {% csrf_token %} {{form.as_p}} + +
+ +
+{% endblock content %} \ No newline at end of file diff --git a/payroll/templates/payroll/contract/contract_list.html b/payroll/templates/payroll/contract/contract_list.html new file mode 100644 index 000000000..2b7c5ee86 --- /dev/null +++ b/payroll/templates/payroll/contract/contract_list.html @@ -0,0 +1,109 @@ +{% load i18n %} +
+
+
+
+
{% trans "Contract" %}
+
{% trans "Employee" %}
+
{% trans "Start Date" %}
+
{% trans "End Date" %}
+
{% trans "Wage Type" %}
+
{% trans "Basic Salary" %}
+
{% trans "Filing Status" %}
+
{% trans "Status" %}
+
+
+
+
+ {% for contract in contracts %} +
+
{{ contract.contract_name }}
+
{{ contract.employee_id }}
+
{{ contract.contract_start_date}}
+
+ {% if contract.contract_end_date %}{{ contract.contract_end_date}}{% endif %} +
+
+ {{ contract.get_wage_type_display}} +
+
{{ contract.wage}}
+
{{ contract.filing_status}}
+
+ {{ contract.get_contract_status_display}} +
+
+ +
+
+ {% endfor %} +
+
+
+
+ + {% trans "Page" %} {{ contracts.number }} {% trans "of" %} {{ contracts.paginator.num_pages }}. + + +
\ No newline at end of file diff --git a/payroll/templates/payroll/contract/contract_single_view.html b/payroll/templates/payroll/contract/contract_single_view.html new file mode 100644 index 000000000..a8064bfe9 --- /dev/null +++ b/payroll/templates/payroll/contract/contract_single_view.html @@ -0,0 +1,131 @@ +{% load i18n %} {% load yes_no %} +
+ +
{{contract.contract_name}}
+
+ +
+
+ {% trans "Employee" %} + {{contract.employee_id}} +
+
+ {% trans "Status" %} + {{contract.get_contract_status_display}} +
+
+
+
+ {% trans "Start Date" %} + {{contract.contract_start_date}} +
+
+ {% trans "End Date" %} + {{contract.contract_end_date}} +
+
+
+
+ {% trans "Wage Type" %} + {{contract.get_wage_type_display}} +
+
+ {% trans "Wage" %} + {{contract.wage}} +
+
+
+
+ {% if contract.calculate_daily_leave_amount %} + {% trans "Calculate Leave Amount" %} + {{contract.calculate_daily_leave_amount|yesno|capfirst}} + {% else %} + {% trans "Deduction Amount For One Leave" %} + {{contract.deduction_for_one_leave_amount}} + {% endif %} +
+
+ {% trans "Deduct From Basic Pay" %} + {{contract.deduct_leave_from_basic_pay|yesno|capfirst}} +
+
+
+
+ {% trans "Department" %} + {{contract.department}} +
+
+ {% trans "Job Position" %} + {{contract.job_position}} +
+
+
+
+ {% trans "Job Role" %} + {{contract.job_role}} +
+
+ {% trans "Shift" %} + {{contract.shift}} +
+
+
+
+ {% trans "Work Type" %} + {{contract.work_type}} +
+
+ {% trans "Filing Status" %} + {{contract.filing_status}} +
+
+
+
+ {% trans "Pay Frequency" %} + {{contract.get_pay_frequency_display}} +
+
+ {% trans "Document" %} + {% if contract.contract_document %} + {{ contract.contract_document.name }} + {% endif %} +
+
+
+
+ {% trans "Note" %} + {{contract.note}} +
+
+
+ +
+
+ + diff --git a/payroll/templates/payroll/contract/contract_view.html b/payroll/templates/payroll/contract/contract_view.html new file mode 100644 index 000000000..1421eae2f --- /dev/null +++ b/payroll/templates/payroll/contract/contract_view.html @@ -0,0 +1,76 @@ +{% extends 'index.html' %} {% block content %} {% load i18n %} +
+
+
+

{% trans "Contracts" %}

+ + + +
+
+
+ + +
+
+ + +
+ +
+
+
+ + {% include 'payroll/contract/contract_list.html' %} +
+ +
+{% endblock content %} diff --git a/payroll/templates/payroll/contract/filter_contract.html b/payroll/templates/payroll/contract/filter_contract.html new file mode 100644 index 000000000..13f4c2324 --- /dev/null +++ b/payroll/templates/payroll/contract/filter_contract.html @@ -0,0 +1,71 @@ +{% load static %} +{% load i18n %} + +
+
+
+
{% trans "Contract" %}
+
+
+
+
+ + {{ f.form.contract_start_date }} +
+
+
+
+ + {{ f.form.contract_end_date }} +
+
+
+
+
+
+ + {{ f.form.wage_type }} +
+
+
+
+ + {{ f.form.filing_status }} +
+
+
+
+
+
+ + {{ f.form.pay_frequency }} +
+
+
+
+ + {{ f.form.contract_status }} +
+
+
+
+
+
+ +
+ + diff --git a/payroll/templates/payroll/contribution/contribution_deduction_assign.html b/payroll/templates/payroll/contribution/contribution_deduction_assign.html new file mode 100644 index 000000000..53dcccaa1 --- /dev/null +++ b/payroll/templates/payroll/contribution/contribution_deduction_assign.html @@ -0,0 +1,33 @@ +{% load i18n %} +
+ +
{% trans "Assign Contribution Deduction" %}
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+ diff --git a/payroll/templates/payroll/contribution/contribution_deduction_creation.html b/payroll/templates/payroll/contribution/contribution_deduction_creation.html new file mode 100644 index 000000000..90dbfda7b --- /dev/null +++ b/payroll/templates/payroll/contribution/contribution_deduction_creation.html @@ -0,0 +1,28 @@ +{% load i18n %} +
+ +
{% trans "Contribution Deduction" %}
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
\ No newline at end of file diff --git a/payroll/templates/payroll/contribution/contribution_deduction_edit.html b/payroll/templates/payroll/contribution/contribution_deduction_edit.html new file mode 100644 index 000000000..53e97b676 --- /dev/null +++ b/payroll/templates/payroll/contribution/contribution_deduction_edit.html @@ -0,0 +1,28 @@ +{% load i18n %} +
+ +
{% trans "Contribution Deduction" %}
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
diff --git a/payroll/templates/payroll/contribution/contribution_deduction_employees.html b/payroll/templates/payroll/contribution/contribution_deduction_employees.html new file mode 100644 index 000000000..5e839c388 --- /dev/null +++ b/payroll/templates/payroll/contribution/contribution_deduction_employees.html @@ -0,0 +1,54 @@ +{% extends 'index.html' %} {% block content %} {% load i18n %} +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "Employee" %}
+
{% trans "Assigned Date" %}
+
+
+
+
+ {% for employee_contribution in employee_contribution_list %} + {% for employee in employee_contribution.employee_id.all %} +
+
{{employee}}
+
+ {{employee_contribution.assign_date}} +
+
+
+ + + +
+
+
+ {% endfor %} + {% endfor %} +
+
+
+
+{% endblock content %} diff --git a/payroll/templates/payroll/contribution/contribution_deduction_list.html b/payroll/templates/payroll/contribution/contribution_deduction_list.html new file mode 100644 index 000000000..8d458b87b --- /dev/null +++ b/payroll/templates/payroll/contribution/contribution_deduction_list.html @@ -0,0 +1,81 @@ +{% load i18n %} +
+
+
+
+ {% trans "Contribution Deduction" %} +
+
{% trans "Deduct From" %}
+
{% trans "Employee Contribution" %}
+
{% trans "Employer Contribution" %}
+
+
+
+
+ {% for contribution_deduction in contribution_deductions %} +
+
{{ contribution_deduction.name }}
+
+ {{ contribution_deduction.get_deduct_from_display }} +
+
+ {{ contribution_deduction.employee_contribution|stringformat:".2f" }}% +
+
+ {{ contribution_deduction.employer_contribution|stringformat:".2f" }}% +
+ +
+ {% endfor %} +
+
diff --git a/payroll/templates/payroll/contribution/contribution_deduction_view.html b/payroll/templates/payroll/contribution/contribution_deduction_view.html new file mode 100644 index 000000000..e8a7266ac --- /dev/null +++ b/payroll/templates/payroll/contribution/contribution_deduction_view.html @@ -0,0 +1,69 @@ +{% extends 'index.html' %} {% block content %} {% load i18n %} +
+
+
+

+ {% trans "Contribution Deduction" %} +

+ + + +
+
+ +
+
+
+ + {% include 'payroll/contribution/contribution_deduction_list.html' %} +
+
+ + +{% endblock content %} diff --git a/payroll/templates/payroll/dashboard.html b/payroll/templates/payroll/dashboard.html new file mode 100644 index 000000000..0375a4195 --- /dev/null +++ b/payroll/templates/payroll/dashboard.html @@ -0,0 +1,3 @@ +{% extends 'index.html' %} {% block content %} + +{% endblock content %} diff --git a/payroll/templates/payroll/deduction/filter_deduction.html b/payroll/templates/payroll/deduction/filter_deduction.html new file mode 100644 index 000000000..0642fdd0b --- /dev/null +++ b/payroll/templates/payroll/deduction/filter_deduction.html @@ -0,0 +1,60 @@ +{% load static %} {% load i18n %} + +
+
+
+
{% trans "Deduction" %}
+
+
+
+
+ + {{ f.form.is_pretax }} +
+
+
+
+ + {{ f.form.is_condition_based }} +
+
+
+
+
+
+ + {{ f.form.is_fixed }} +
+
+
+
+ + {{ f.form.based_on }} +
+
+
+
+
+ {% comment %} {{f.form}} {% endcomment %} +
+ +
+ + diff --git a/payroll/templates/payroll/deduction/list_deduction.html b/payroll/templates/payroll/deduction/list_deduction.html new file mode 100644 index 000000000..acc840753 --- /dev/null +++ b/payroll/templates/payroll/deduction/list_deduction.html @@ -0,0 +1,153 @@ +{% load i18n %} {% load yes_no %} +
+
+
+
+
+
+
+ +
+
+ {% trans "Deduction" %} +
+
+
+
{% trans "Specific Employees" %}
+
{% trans "Excluded Employees" %}
+
{% trans "Is Pretax" %}
+
{% trans "Is Condition Based" %}
+
{% trans "Condition" %}
+
{% trans "Is Fixed" %}
+
{% trans "Amount" %}
+
{% trans "Based On" %}
+
{% trans "Rate" %}
+
+
+
+ {% for deduction in deductions %} +
+
+
{{deduction.title}}
+
+ {% for employee in deduction.specific_employees.all%} + {{employee}}
+ {% endfor %} +
+
+ {% for employee in deduction.exclude_employees.all%} + {{employee}}
+ {% endfor %} +
+
+ {{deduction.is_pretax|yesno|capfirst}} +
+
+ {{deduction.is_condition_based|yesno|capfirst}} +
+
+ {% if deduction.field %} {{deduction.get_field_display}} + {{deduction.get_condition_display}} {{deduction.value}} + {% endif %} +
+
+ {{deduction.is_fixed|yesno|capfirst}} +
+
+ {% if deduction.amount %} + {{deduction.amount}} + {% endif %} +
+
+ {% if deduction.based_on %} + {{deduction.get_based_on_display}} + {% endif%} +
+
+ {% if deduction.based_on %} + {{deduction.rate}} + {% endif %} +
+
+ +
+
+
+ {% endfor %} +
+
+
+ + {% trans "Page" %} {{ deductions.number }} {% trans "of" %} {{ deductions.paginator.num_pages }}. + + +
diff --git a/payroll/templates/payroll/deduction/view_deduction.html b/payroll/templates/payroll/deduction/view_deduction.html new file mode 100644 index 000000000..c39d6e108 --- /dev/null +++ b/payroll/templates/payroll/deduction/view_deduction.html @@ -0,0 +1,82 @@ +{% extends 'index.html' %} {% block content %} {% load i18n %} {% load yes_no %} +
+
+

{% trans "Deductions" %}

+ + + +
+
+
+ + +
+
+ + +
+ +
+
+
+ {% include 'payroll/deduction/list_deduction.html' %} +
+ +{% endblock content %} diff --git a/payroll/templates/payroll/deduction/view_single_deduction.html b/payroll/templates/payroll/deduction/view_single_deduction.html new file mode 100644 index 000000000..7ee41a839 --- /dev/null +++ b/payroll/templates/payroll/deduction/view_single_deduction.html @@ -0,0 +1,101 @@ +{% load i18n %} {% load yes_no %} +
+ +
{{deduction.title}}
+
+ +
+ {% if deduction.is_tax %} +
+ {% trans "Tax" %} + {{deduction.is_tax|yesno|capfirst}} +
+ {% else %} +
+ {% trans "Pretax" %} + {{deduction.is_pretax|yesno|capfirst}} +
+ {% endif %} +
+ {% trans "One Time deduction" %} + {% if deduction.one_time_date %} + {% trans "On" %} {{deduction.one_time_date}} + {% else %} + {% trans "No" %} + {% endif %} +
+
+
+
+ {% trans "Condition Based" %} + {% if deduction.is_condition_based %} + {{deduction.get_field_display}} {{deduction.get_condition_display}} {{deduction.value}} + {% else %} + {% trans "No" %} + {% endif %} +
+
+ {% trans "Amount" %} + {% if deduction.update_compensation %} + {% if deduction.is_fixed %} + {{deduction.amount}}{{currency}} Deduct From {{deduction.get_update_compensation_display}} + {% else %} + {{deduction.rate}}% {% trans "of" %} {{deduction.get_update_compensation_display}} + {% endif %} + {% else %} + {% if deduction.is_fixed %} + {{deduction.amount}}{{currency}} + {% else %} + {% trans "Employer Rate :" %} {{deduction.employer_rate}}% {% trans "of" %} {{deduction.get_based_on_display}} + {% trans "Employee Rate :" %} {{deduction.rate}}% {% trans "of" %} {{deduction.get_based_on_display}} + {% endif %} + {% endif %} +
+
+
+
+ {% trans "Has Maximum Limit" %} + {% if deduction.has_max_limit %} + {{deduction.maximum_amount}} {{currency}} {% trans "For working days on a month" %} + {% else %} + {% trans "No" %} + {% endif %} +
+
+ {% trans "Deduction Eligibility" %} + If {{deduction.get_if_choice_display}} {{deduction.get_if_condition_display}} {{deduction.if_amount}} +
+
+
+ +
+
+ + diff --git a/payroll/templates/payroll/htmx/form.html b/payroll/templates/payroll/htmx/form.html new file mode 100644 index 000000000..24dc128b2 --- /dev/null +++ b/payroll/templates/payroll/htmx/form.html @@ -0,0 +1,132 @@ +{% comment %} {{form.as_custom}} {% endcomment %} +{{form.style}} +{{form.as_custom}} + \ No newline at end of file diff --git a/payroll/templates/payroll/payroll.html b/payroll/templates/payroll/payroll.html new file mode 100644 index 000000000..3449c9c1d --- /dev/null +++ b/payroll/templates/payroll/payroll.html @@ -0,0 +1,4 @@ +{% extends 'index.html' %} +{% block content %} + +{% endblock content %} \ No newline at end of file diff --git a/payroll/templates/payroll/payslip/filter_payslips.html b/payroll/templates/payroll/payslip/filter_payslips.html new file mode 100644 index 000000000..0b86fcbbf --- /dev/null +++ b/payroll/templates/payroll/payslip/filter_payslips.html @@ -0,0 +1,141 @@ +{% load static %} {% load i18n %} + +
+
+
+
{% trans "Payslip" %}
+
+
+
+
+ + {{ f.form.start_date }} +
+
+
+
+ + {{ f.form.end_date }} +
+
+
+
+
+
+ + {{ f.form.status }} +
+
+
+
+
+
+
{% trans "Advanced" %}
+
+
+
+
+ + {{ f.form.start_date_from }} +
+
+
+
+ + {{ f.form.start_date_till }} +
+
+
+
+
+
+ + {{ f.form.end_date_from }} +
+
+
+
+ + {{ f.form.end_date_till }} +
+
+
+
+
+
+ + {{ f.form.gross_pay__lte }} +
+
+
+
+ + {{ f.form.gross_pay__gte }} +
+
+
+
+
+
+ + {{ f.form.deduction__lte }} +
+
+
+
+ + {{ f.form.deduction__gte }} +
+
+
+
+
+
+ + {{ f.form.net_pay__lte }} +
+
+
+
+ + {{ f.form.net_pay__gte }} +
+
+
+
+
+ {% comment %} {{f.form}} {% endcomment %} +
+ +
+ + diff --git a/payroll/templates/payroll/payslip/generate_payslip_list.html b/payroll/templates/payroll/payslip/generate_payslip_list.html new file mode 100644 index 000000000..ae6d96ec3 --- /dev/null +++ b/payroll/templates/payroll/payslip/generate_payslip_list.html @@ -0,0 +1,151 @@ +{% extends 'index.html' %} {% block content %} {% load i18n %} {% load yes_no %} + +
+
+

+ {% trans "Payroll" %} - {{ start_date }} to {{ end_date }} +

+ + + +
+
+

+ +

+
+
+
+
+
+
+
+
+
+
+ +
+
+ {% trans "Employee" %} +
+
+
+
{% trans "Gross Pay" %}
+
{% trans "Deductions" %}
+
{% trans "Net Pay" %}
+
+
+
+
+ {% for data in payslip_data %} +
+
+
+
+ {% if data.employee.employee_profile %} + Username + {% else %} + Username + {% endif %} +
+ {{data.employee}} +
+
+
+ {{currency}} {{data.gross_pay|floatformat:2}} +
+
+ {{currency}} {{data.total_deductions|floatformat:2}} +
+
+ {{currency}} {{data.net_pay|floatformat:2}} +
+
+
+
+ {% csrf_token %} + + + + +
+
+
+
+ {% endfor %} +
+
+
+
+ +{% endblock content %} diff --git a/payroll/templates/payroll/payslip/individual_payslip.html b/payroll/templates/payroll/payslip/individual_payslip.html new file mode 100644 index 000000000..5483245e3 --- /dev/null +++ b/payroll/templates/payroll/payslip/individual_payslip.html @@ -0,0 +1,265 @@ +{% extends 'index.html' %} {% block content %} {% load i18n %} +
+ {% if perms.payroll.change_payslip %} +
+
+
+ +
+
+
+ {% endif %} +
+
+

{% trans "Payslip" %}

+

{{range}}

+
+
+ +
+
+
+ +
+
+

{% trans "Employee Details" %}

+
    +
  • + {% trans "Employee ID" %} + {{employee.badge_id}} +
  • +
  • + {% trans "Employee Name" %} + {{employee}} +
  • +
  • + {% trans "Department" %} + {{employee.employee_work_info.department_id.department}} +
  • +
  • + {% trans "Bank Acc./Cheque No." %} + {{employee.employee_bank_details.account_number}} +
  • +
+
+ +
+ {% trans "Employee Net Pay" %} + {{currency}}{{net_pay|floatformat:2}} +
+
+ + + +
+ + + + + + + + + + + + + {% for allowance in allowances %} + + + + + {% endfor %} + + + + + + + +
{% trans "Allowances" %}{% trans "Amount" %}
Basic Pay + {{currency}}{{basic_pay|floatformat:2}} +
{{allowance.title}} + {{currency}}{{allowance.amount|floatformat:2}} +
{% trans "Total Gross Pay" %} + {{currency}}{{gross_pay|floatformat:2}} +
+ + + + + + + + + + + + + + {% for deduction in basic_pay_deductions %} + + + + + {% endfor %} {% for deduction in gross_pay_deductions %} + + + + + {% endfor %} {% for deduction in pretax_deductions %} + + + + + {% endfor %} {% for deduction in post_tax_deductions %} + + + + + {% endfor %} + + + + + + + {% for deduction in tax_deductions %} + + + + + {% endfor %} + {% for deduction in net_deductions %} + + + + + {% endfor %} + + + + + + + +
{% trans "Deductions" %}{% trans "Amount" %}
{% trans "Loss of Pay" %} + {{currency}}{{loss_of_pay|floatformat:2}} +
{{deduction.title}} + {{currency}}{{deduction.amount|floatformat:2}} +
{{deduction.title}} + {{currency}}{{deduction.amount|floatformat:2}} +
{{deduction.title}} + {{currency}}{{deduction.amount|floatformat:2}} +
{{deduction.title}} + {{currency}}{{deduction.amount|floatformat:2}} +
{% trans "Federal Tax" %} + {{currency}}{{federal_tax|floatformat:2}} +
{{deduction.title}} + {{currency}}{{deduction.amount|floatformat:2}} +
{{deduction.title}} + {{currency}}{{deduction.amount|floatformat:2}} +
Total Deductions + {{currency}}{{total_deductions|floatformat:2}} +
+
+ + + +
+
+

{% trans "Total Net Payable" %}

+

+ {% trans "Gross Earnings - Total Deductions" %} +

+
+
+ {{currency}}{{net_pay|floatformat:2}} +
+
+ +
+
+ + + + +{% endblock content %} diff --git a/payroll/templates/payroll/payslip/list_payslips.html b/payroll/templates/payroll/payslip/list_payslips.html new file mode 100644 index 000000000..4c6fd31ae --- /dev/null +++ b/payroll/templates/payroll/payslip/list_payslips.html @@ -0,0 +1,107 @@ +{% load i18n %} +
+
+
+
+
{% trans "Employee" %}
+
{% trans "Period" %}
+
{% trans "Status" %}
+
{% trans "Gross Pay" %}
+
{% trans "Deduction" %}
+
{% trans "Net Pay" %}
+
+
+
+
+ {% for payslip in payslips %} +
+
+
+
+ Username +
+ {{payslip.employee_id}} +
+ +
+
+ {{payslip.start_date}} - {{payslip.end_date}} +
+
+ {{payslip.get_status_display}} +
+
+ {{currency}} {{payslip.gross_pay|floatformat:2}} +
+
+ {{currency}} {{payslip.deduction|floatformat:2}} +
+
+ {{currency}} {{payslip.net_pay|floatformat:2}} +
+
+
+ +
+ {% if perms.payroll.change_payslip %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+
+
+ {% endfor %} +
+
+
+
+ + {% trans "Page" %} {{ payslips.number }} {% trans "of" %} {{ payslips.paginator.num_pages }}. + + +
+ + + diff --git a/payroll/templates/payroll/payslip/view_payslips.html b/payroll/templates/payroll/payslip/view_payslips.html new file mode 100644 index 000000000..50938b44a --- /dev/null +++ b/payroll/templates/payroll/payslip/view_payslips.html @@ -0,0 +1,189 @@ +{% extends 'index.html' %} {% block content %}{% load i18n %} + + + + + +
+
+

{% trans "Payslip" %}

+ + + +
+
+ {% if perms.payroll.view_payslip %} +
+ + +
+ {% endif %} + {% if perms.payroll.view_payslip %} +
+ + +
+ {% endif %} + {% if perms.payroll.add_payslip %} + + {% endif %} + {% if perms.payroll.add_payslip %} + + {% endif %} +
+
+
+ {% include 'payroll/payslip/list_payslips.html' %} +
+ +{% endblock content %} diff --git a/payroll/templates/payroll/settings/payroll_settings.html b/payroll/templates/payroll/settings/payroll_settings.html new file mode 100644 index 000000000..665a94ef6 --- /dev/null +++ b/payroll/templates/payroll/settings/payroll_settings.html @@ -0,0 +1,13 @@ +{% extends 'settings.html' %} {% block settings %}{% load i18n %} + +
+ {% csrf_token %} {{form}} + + +
+{% endblock settings %} diff --git a/payroll/templates/payroll/tax/federal_tax.html b/payroll/templates/payroll/tax/federal_tax.html new file mode 100644 index 000000000..70a1efa20 --- /dev/null +++ b/payroll/templates/payroll/tax/federal_tax.html @@ -0,0 +1,11 @@ +{% extends 'index.html' %} +{% block content %} +
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock content %} + diff --git a/payroll/templates/payroll/tax/federaltax.html b/payroll/templates/payroll/tax/federaltax.html new file mode 100644 index 000000000..70a1efa20 --- /dev/null +++ b/payroll/templates/payroll/tax/federaltax.html @@ -0,0 +1,11 @@ +{% extends 'index.html' %} +{% block content %} +
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock content %} + diff --git a/payroll/templates/payroll/tax/filing_status_creation.html b/payroll/templates/payroll/tax/filing_status_creation.html new file mode 100644 index 000000000..2fd6554a6 --- /dev/null +++ b/payroll/templates/payroll/tax/filing_status_creation.html @@ -0,0 +1,28 @@ +{% load i18n %} +
+ +
{% trans "Filing Status" %}
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
diff --git a/payroll/templates/payroll/tax/filing_status_edit.html b/payroll/templates/payroll/tax/filing_status_edit.html new file mode 100644 index 000000000..a432642d3 --- /dev/null +++ b/payroll/templates/payroll/tax/filing_status_edit.html @@ -0,0 +1,31 @@ +{% load i18n %} +
+ +
{% trans "Filing Status" %}
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
\ No newline at end of file diff --git a/payroll/templates/payroll/tax/filing_status_list.html b/payroll/templates/payroll/tax/filing_status_list.html new file mode 100644 index 000000000..41356033d --- /dev/null +++ b/payroll/templates/payroll/tax/filing_status_list.html @@ -0,0 +1,102 @@ +{% load i18n %} +{% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+{% endif %} +
+
+
+ {% for filing_status in status %} +
+
+ {{filing_status}} +
+ +
+
+ + +
+
+
+
+ +
+
+ {% endfor %} + +
+
+
diff --git a/payroll/templates/payroll/tax/filing_status_view.html b/payroll/templates/payroll/tax/filing_status_view.html new file mode 100644 index 000000000..a174c27a3 --- /dev/null +++ b/payroll/templates/payroll/tax/filing_status_view.html @@ -0,0 +1,48 @@ +{% extends 'index.html' %} +{% block content %} +{% load i18n %} +
+
+
+

{% trans "Filing Status" %}

+ + + +
+
+ {% comment %}
+ + +
{% endcomment %} + +
+
+
+ + {% include 'payroll/tax/filing_status_list.html' %} +
+
+ + +{% endblock content %} + diff --git a/payroll/templates/payroll/tax/filingstatus.html b/payroll/templates/payroll/tax/filingstatus.html new file mode 100644 index 000000000..efa7431d5 --- /dev/null +++ b/payroll/templates/payroll/tax/filingstatus.html @@ -0,0 +1,9 @@ +{% extends 'index.html' %} +{% block content %} +
+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock content %} \ No newline at end of file diff --git a/payroll/templates/payroll/tax/tax_bracket_creation.html b/payroll/templates/payroll/tax/tax_bracket_creation.html new file mode 100644 index 000000000..c577dd210 --- /dev/null +++ b/payroll/templates/payroll/tax/tax_bracket_creation.html @@ -0,0 +1,42 @@ +{% load i18n %} +
+ +
{% trans "Tax Bracket" %}
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+{% comment %} {% endcomment %} diff --git a/payroll/templates/payroll/tax/tax_bracket_edit.html b/payroll/templates/payroll/tax/tax_bracket_edit.html new file mode 100644 index 000000000..26ca58ecb --- /dev/null +++ b/payroll/templates/payroll/tax/tax_bracket_edit.html @@ -0,0 +1,28 @@ +{% load i18n %} +
+ +
{% trans "Tax Bracket" %}
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
diff --git a/payroll/templates/payroll/tax/tax_bracket_view.html b/payroll/templates/payroll/tax/tax_bracket_view.html new file mode 100644 index 000000000..c67289de5 --- /dev/null +++ b/payroll/templates/payroll/tax/tax_bracket_view.html @@ -0,0 +1,64 @@ +{% load i18n %} +
+
+
+
{% trans "Tax Rate" %}
+
{% trans "Min. Income" %}
+
{% trans "Max. Income" %}
+
+
+
+
+
+ {% for tax_bracket in tax_brackets %} +
+
+ {{ tax_bracket.tax_rate|stringformat:".2f" }}% +
+
+ {{currency}}{{ tax_bracket.min_income|stringformat:".2f" }} +
+
+ {{currency}}{{ tax_bracket.get_display_max_income|stringformat:".2f" }} +
+
+
+ + + +
+
+
+
+ + + +
+
+
+ {% endfor %} +
+
diff --git a/payroll/templates/payroll/tax/taxbracket.html b/payroll/templates/payroll/tax/taxbracket.html new file mode 100644 index 000000000..7c776a996 --- /dev/null +++ b/payroll/templates/payroll/tax/taxbracket.html @@ -0,0 +1,29 @@ +{% extends 'index.html' %} {% load i18n %} {% block content %} +
+
+
+
{% trans "Filing Status" %}
+
{% trans "Min. Income" %}
+
{% trans "Max. Income" %}
+
{% trans "Tax Rate" %}
+
+
+
+
+ {% for tax_bracket in tax_brackets %} +
+
{{tax_bracket.filing_status_id}}
+
{{tax_bracket.min_income|stringformat:".2f"}}
+
{{tax_bracket.max_income|stringformat:".2f"}}
+
{{ tax_bracket.tax_rate|stringformat:".2f" }}%
+
+ {% endfor %} +
+
+
+
+ {% csrf_token %} {{form.as_p}} + +
+
+{% endblock content %} diff --git a/payroll/templates/payroll/work_record/work_record_create.html b/payroll/templates/payroll/work_record/work_record_create.html new file mode 100644 index 000000000..f35265dbe --- /dev/null +++ b/payroll/templates/payroll/work_record/work_record_create.html @@ -0,0 +1,17 @@ +{% extends 'index.html' %} +{% load i18n %} + +{% block content %} +
+
+ {% csrf_token %} + {{contract_form.as_p}} + +
+ + +
+ + +{% endblock content %} + \ No newline at end of file diff --git a/payroll/templates/payroll/work_record/work_record_employees_view.html b/payroll/templates/payroll/work_record/work_record_employees_view.html new file mode 100644 index 000000000..fc4e6aa97 --- /dev/null +++ b/payroll/templates/payroll/work_record/work_record_employees_view.html @@ -0,0 +1,74 @@ +{% extends 'index.html' %} +{% load current_month_record %} +{% block content %} + + + +
+
+
+

{% trans "Date:" %} {{ current_date }}

+

{% trans "Month:" %} {{ current_date|date:"F" }}

+
+ + + + + {% for day in current_month_dates_list %} + + {% endfor %} + + + + + {% for employee in employees %} + + + {% for date in current_month_dates_list %} + + {% endfor %} + + {% endfor %} + +
{% trans "Work Records" %}{{ day.day }}
{{ employee }} + {% for work_record in employee.workrecord_set.all|current_month_record %} + {% if work_record.start_datetime.date == date %} + {% if work_record.work_record_type.is_timeoff %} +
{% trans "A" %}
+ {% else %} +
{% trans "P" %}
+ {% endif %} + {% endif %} + {% endfor %} +
+
+
+ +{% endblock content %} \ No newline at end of file diff --git a/payroll/templates/payroll/work_record/work_record_view.html b/payroll/templates/payroll/work_record/work_record_view.html new file mode 100644 index 000000000..5d3713b42 --- /dev/null +++ b/payroll/templates/payroll/work_record/work_record_view.html @@ -0,0 +1,38 @@ +{% extends 'index.html' %} + +{% block content %} + + + +
+
+
+
{% trans "record_type_name" %}
+ +
+
+
+ {% for work_record in work_records %} +
+
+
+
+ Mary Magdalene +
+ {{work_record.work_record_name}} +
+
+ +
+ {% endfor %} +
+
+ + +{% endblock content %} + \ No newline at end of file diff --git a/payroll/templatetags/__inti__.py b/payroll/templatetags/__inti__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/templatetags/current_month_record.py b/payroll/templatetags/current_month_record.py new file mode 100644 index 000000000..a31720a06 --- /dev/null +++ b/payroll/templatetags/current_month_record.py @@ -0,0 +1,15 @@ +from django import template +from datetime import datetime, timedelta + +register = template.Library() + + +@register.filter(name="current_month_record") +def current_month_record(queryset): + current_month_start_date = datetime.now().replace(day=1) + next_month_start_date = current_month_start_date + timedelta(days=31) + + return queryset.filter( + start_datetime__gte=current_month_start_date, + start_datetime__lt=next_month_start_date, + ).order_by("start_datetime") diff --git a/payroll/templatetags/migrations/__init__.py b/payroll/templatetags/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/templatetags/yes_no.py b/payroll/templatetags/yes_no.py new file mode 100644 index 000000000..636a49467 --- /dev/null +++ b/payroll/templatetags/yes_no.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.filter(name="yes_no") +def yesno(value): + return "Yes" if value else "No" diff --git a/payroll/tests.py b/payroll/tests.py new file mode 100644 index 000000000..76e6bb791 --- /dev/null +++ b/payroll/tests.py @@ -0,0 +1,4 @@ +"""test cases""" +from django.test import TestCase + +# Create your tests here. diff --git a/payroll/urls/__init__.py b/payroll/urls/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/urls/component_urls.py b/payroll/urls/component_urls.py new file mode 100644 index 000000000..38deca8e6 --- /dev/null +++ b/payroll/urls/component_urls.py @@ -0,0 +1,61 @@ +""" +component_urls.py + +This module is used to bind the urls related to payslip and its pay-heads methods +""" +from django.urls import path +from payroll.views import component_views + +urlpatterns = [ + path("create-allowance", component_views.create_allowance, name="create-allowance"), + path("view-allowance", component_views.view_allowance, name="view-allowance"), + path( + "single-allowance-view/", + component_views.view_single_allowance, + name="single-allowance-view", + ), + path("filter-allowance", component_views.filter_allowance, name="filter-allowance"), + path( + "update-allowance//", + component_views.update_allowance, + name="update-allowance", + ), + path( + "delete-allowance//", + component_views.delete_allowance, + name="delete-allowance", + ), + path("create-deduction", component_views.create_deduction, name="create-deduction"), + path("view-deduction", component_views.view_deduction, name="view-deduction"), + path( + "single-deduction-view/", + component_views.view_single_deduction, + name="single-deduction-view", + ), + path("filter-deduction", component_views.filter_deduction, name="filter-deduction"), + path( + "update-deduction//", + component_views.update_deduction, + name="update-deduction", + ), + path( + "delete-deduction//", + component_views.delete_deduction, + name="delete-deduction", + ), + path("create-payslip", component_views.create_payslip, name="create-payslip"), + path("generate-payslip", component_views.generate_payslip, name="generate-payslip"), + path('validate-start-date', component_views.validate_start_date, name='validate-start-date'), + path("filter-payslip", component_views.filter_payslip, name="filter-payslip"), + path( + "view-individual-payslip////", + component_views.view_individual_payslip, + name="view-individual-payslip", + ), + path("view-payslip", component_views.view_payslip, name="view-payslip"), + path( + "hx-create-allowance", + component_views.hx_create_allowance, + name="hx-create-allowance", + ), +] diff --git a/payroll/urls/tax_urls.py b/payroll/urls/tax_urls.py new file mode 100644 index 000000000..0c1689819 --- /dev/null +++ b/payroll/urls/tax_urls.py @@ -0,0 +1,48 @@ +""" +tax_urls.py + +This module is used to bind url patterns with django views that related to federal taxes +""" +from django.urls import path +from payroll.views import tax_views + + +urlpatterns = [ + path("filing-status-view", tax_views.filing_status_view, name="filing-status-view"), + path( + "create-filing-status", + tax_views.create_filing_status, + name="create-filing-status", + ), + path( + "filing-status-update/", + tax_views.update_filing_status, + name="filing-status-update", + ), + path( + "filing-status-delete/", + tax_views.filing_status_delete, + name="filing-status-delete", + ), + path( + "tax-bracket-list/", + tax_views.tax_bracket_list, + name="tax-bracket-list", + ), + path( + "tax-bracket-create/", + tax_views.create_tax_bracket, + name="tax-bracket-create", + ), + path( + "tax-bracket-update//", + tax_views.update_tax_bracket, + name="tax-bracket-update", + ), + path( + "tax-bracket-delete//", + tax_views.delete_tax_bracket, + name="tax-bracket-delete", + ), + path("create-federal-tax", tax_views.create_federal_tax, name="create-federal-tax"), +] diff --git a/payroll/urls/urls.py b/payroll/urls/urls.py new file mode 100644 index 000000000..7db46241a --- /dev/null +++ b/payroll/urls/urls.py @@ -0,0 +1,62 @@ +""" +urls.py + +This module is used to map url pattern or request path with view functions +""" +from django.urls import path, include +from payroll.views import views + +urlpatterns = [ + path("", include("payroll.urls.component_urls")), + path("", include("payroll.urls.tax_urls")), + path("dashboard", views.dashboard, name="dashboard"), + path("contract-create", views.contract_create, name="contract-create"), + path( + "update-contract/", + views.contract_update, + name="update-contract", + ), + path( + "delete-contract/", + views.contract_delete, + name="delete-contract", + ), + path("view-contract", views.contract_view, name="view-contract"), + path( + "single-contract-view//", + views.view_single_contract, + name="single-contract-view", + ), + path("contract-filter", views.contract_filter, name="contract-filter"), + path("contract-create", views.work_record_create, name="contract-create"), + path("work-record-view", views.work_record_view, name="work-record-view"), + path( + "work-record-employees-view", + views.work_record_employee_view, + name="work-record-employees-view", + ), + path("settings", views.settings, name="payroll-settings"), + path( + "payslip-status-update", + views.update_payslip_status, + name="payslip-status-update", + ), + path( + "bulk-payslip-status-update", + views.bulk_update_payslip_status, + name="bulk-payslip-status-update", + ), + path( + "view-payslip//", + views.view_created_payslip, + name="view-created-payslip", + ), + path( + "delete-payslip//", views.delete_payslip, name="delete-payslip" + ), + path( + "contract-info-initial", + views.contract_info_initial, + name="contract-info-initial", + ), +] diff --git a/payroll/views/__init__.py b/payroll/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/views/component_views.py b/payroll/views/component_views.py new file mode 100644 index 000000000..a530559fa --- /dev/null +++ b/payroll/views/component_views.py @@ -0,0 +1,567 @@ +""" +component_views.py + +This module is used to write methods to the component_urls patterns respectively +""" +import json +import operator +from datetime import datetime +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.http import JsonResponse +from django.shortcuts import render, redirect +from django.contrib import messages +from horilla.decorators import login_required, permission_required +import payroll.models.models +from payroll.methods.payslip_calc import ( + calculate_allowance, + calculate_gross_pay, + calculate_taxable_gross_pay, +) +from payroll.methods.payslip_calc import ( + calculate_post_tax_deduction, + calculate_pre_tax_deduction, + calculate_tax_deduction, +) +from payroll.filters import AllowanceFilter, DeductionFilter, PayslipFilter +from payroll.forms import component_forms as forms +from payroll.methods.payslip_calc import ( + calculate_net_pay_deduction, +) +from payroll.methods.tax_calc import calculate_taxable_amount +from payroll.methods.methods import compute_salary_on_period, paginator_qry +from payroll.methods.deductions import update_compensation_deduction + +operator_mapping = { + "equal": operator.eq, + "notequal": operator.ne, + "lt": operator.lt, + "gt": operator.gt, + "le": operator.le, + "ge": operator.ge, + "icontains": operator.contains, +} + + +def payroll_calculation(employee, start_date, end_date): + """ + Calculate payroll components for the specified employee within the given date range. + + + Args: + employee (Employee): The employee for whom the payroll is calculated. + start_date (date): The start date of the payroll period. + end_date (date): The end date of the payroll period. + + + Returns: + dict: A dictionary containing the calculated payroll components: + """ + + basic_pay_details = compute_salary_on_period(employee, start_date, end_date) + contract = basic_pay_details["contract"] + contract_wage = basic_pay_details["contract_wage"] + basic_pay = basic_pay_details["basic_pay"] + loss_of_pay = basic_pay_details["loss_of_pay"] + + working_days_details = basic_pay_details["month_data"] + + updated_basic_pay_data = update_compensation_deduction( + employee, basic_pay, "basic_pay", start_date, end_date + ) + basic_pay = updated_basic_pay_data["compensation_amount"] + basic_pay_deductions = updated_basic_pay_data["deductions"] + + loss_of_pay_amount = float(loss_of_pay) if not contract.deduct_leave_from_basic_pay else 0 + + basic_pay = basic_pay - loss_of_pay_amount + + kwargs = { + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "basic_pay": basic_pay, + "day_dict": working_days_details, + } + # basic pay will be basic_pay = basic_pay - update_compensation_amount + allowances = calculate_allowance(**kwargs) + + # finding the total allowance + total_allowance = sum(allowance["amount"] for allowance in allowances["allowances"]) + + kwargs["allowances"] = allowances + kwargs["total_allowance"] = total_allowance + gross_pay = calculate_gross_pay(**kwargs)["gross_pay"] + updated_gross_pay_data = update_compensation_deduction( + employee, gross_pay, "gross_pay", start_date, end_date + ) + gross_pay = updated_gross_pay_data["compensation_amount"] + gross_pay_deductions = updated_gross_pay_data["deductions"] + + pretax_deductions = calculate_pre_tax_deduction(**kwargs) + post_tax_deductions = calculate_post_tax_deduction(**kwargs) + + taxable_gross_pay = calculate_taxable_gross_pay(**kwargs) + tax_deductions = calculate_tax_deduction(**kwargs) + federal_tax = calculate_taxable_amount(**kwargs) + + # gross_pay = (basic_pay + total_allowances) + # deduction = ( + # post_tax_deductions_amount + # + pre_tax_deductions _amount + # + tax_deductions + federal_tax_amount + # + lop_amount + # + one_time_basic_deduction_amount + # + one_time_gross_deduction_amount + # ) + # net_pay = gross_pay - deduction + # net_pay = net_pay - net_pay_deduction + + total_allowance = sum(item["amount"] for item in allowances["allowances"]) + total_pretax_deduction = sum( + item["amount"] for item in pretax_deductions["pretax_deductions"] + ) + total_post_tax_deduction = sum( + item["amount"] for item in post_tax_deductions["post_tax_deductions"] + ) + total_tax_deductions = sum( + item["amount"] for item in tax_deductions["tax_deductions"] + ) + + total_deductions = ( + total_pretax_deduction + + total_post_tax_deduction + + total_tax_deductions + + federal_tax + + loss_of_pay_amount + ) + + net_pay = (basic_pay + total_allowance) - total_deductions + updated_net_pay_data = update_compensation_deduction( + employee, net_pay, "net_pay", start_date, end_date + ) + net_pay = updated_net_pay_data["compensation_amount"] + update_net_pay_deductions = updated_net_pay_data["deductions"] + + net_pay_deductions = calculate_net_pay_deduction( + net_pay, post_tax_deductions["net_pay_deduction"], working_days_details + ) + net_pay_deduction_list = net_pay_deductions["net_pay_deductions"] + for deduction in update_net_pay_deductions: + net_pay_deduction_list.append(deduction) + net_pay = net_pay - net_pay_deductions["net_deduction"] + payslip_data = { + "net_pay": net_pay, + "employee": employee, + "allowances": allowances["allowances"], + "gross_pay": gross_pay, + "contract_wage": contract_wage, + "basic_pay": basic_pay, + "taxable_gross_pay": taxable_gross_pay, + "basic_pay_deductions": basic_pay_deductions, + "gross_pay_deductions": gross_pay_deductions, + "pretax_deductions": pretax_deductions["pretax_deductions"], + "post_tax_deductions": post_tax_deductions["post_tax_deductions"], + "tax_deductions": tax_deductions["tax_deductions"], + "net_deductions": net_pay_deduction_list, + "total_deductions": total_deductions, + "loss_of_pay": loss_of_pay, + "federal_tax": federal_tax, + "start_date": start_date, + "end_date": end_date, + "range": f"{start_date.strftime('%b %d %Y')} - {end_date.strftime('%b %d %Y')}", + } + data_to_json = payslip_data.copy() + data_to_json["employee"] = employee.id + data_to_json["start_date"] = start_date.strftime("%Y-%m-%d") + data_to_json["end_date"] = end_date.strftime("%Y-%m-%d") + json_data = json.dumps(data_to_json) + + payslip_data["json_data"] = json_data + return payslip_data + + +@login_required +@permission_required("payroll.add_allowance") +def create_allowance(request): + """ + This method is used to create allowance condition template + """ + form = forms.AllowanceForm() + if request.method == "POST": + form = forms.AllowanceForm(request.POST) + if form.is_valid(): + form.save() + form = forms.AllowanceForm() + messages.success(request, "Allowance created.") + return redirect(view_allowance) + return render(request, "payroll/common/form.html", {"form": form}) + + +@login_required +@permission_required("payroll.view_allowance") +def view_allowance(request): + """ + This method is used render template to view all the allowance instances + """ + allowances = payroll.models.models.Allowance.objects.all() + allowance_filter = AllowanceFilter(request.GET) + allowances = paginator_qry(allowances, request.GET.get("page")) + return render( + request, + "payroll/allowance/view_allowance.html", + {"allowances": allowances, "f": allowance_filter}, + ) + + +@login_required +@permission_required("payroll.view_allowance") +def view_single_allowance(request, allowance_id): + """ + This method is used render template to view the selected allowance instances + """ + allowance = payroll.models.models.Allowance.objects.get(id=allowance_id) + return render( + request, + "payroll/allowance/view_single_allowance.html", + {"allowance": allowance}, + ) + + +@login_required +@permission_required("payroll.view_allowance") +def filter_allowance(request): + """ + Filter and retrieve a list of allowances based on the provided query parameters. + """ + query_string = request.environ["QUERY_STRING"] + allowances = AllowanceFilter(request.GET).qs + template = "payroll/allowance/list_allowance.html" + allowances = paginator_qry(allowances, request.GET.get("page")) + return render(request, template, {"allowances": allowances, "pd": query_string}) + + +@login_required +@permission_required("payroll.change_allowance") +def update_allowance(request, allowance_id): + """ + This method is used to update the allowance + Args: + id : allowance instance id + """ + instance = payroll.models.models.Allowance.objects.get(id=allowance_id) + form = forms.AllowanceForm(instance=instance) + if request.method == "POST": + form = forms.AllowanceForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, "Allowance updated.") + return redirect(view_allowance) + return render(request, "payroll/common/form.html", {"form": form}) + + +@login_required +@permission_required("payroll.delete_allowance") +def delete_allowance(request, allowance_id): + """ + This method is used to delete the allowance instance + """ + try: + payroll.models.models.Allowance.objects.get(id=allowance_id).delete() + messages.success(request, "Allowance deleted") + except ObjectDoesNotExist(Exception): + messages.error(request, "Allowance not found") + except ValidationError as validation_error: + messages.error( + request, "Validation error occurred while deleting the allowance" + ) + messages.error(request, str(validation_error)) + except Exception as exception: + messages.error(request, "An error occurred while deleting the allowance") + messages.error(request, str(exception)) + return redirect(view_allowance) + + +@login_required +@permission_required("payroll.add_deduction") +def create_deduction(request): + """ + This method is used to create deduction + """ + form = forms.DeductionForm() + if request.method == "POST": + form = forms.DeductionForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, "Deduction created.") + return redirect(view_deduction) + return render(request, "payroll/common/form.html", {"form": form}) + + +@login_required +@permission_required("payroll.view_allowance") +def view_deduction(request): + """ + This method is used render template to view all the deduction instances + """ + + deductions = payroll.models.models.Deduction.objects.all() + deduction_filter = DeductionFilter(request.GET) + deductions = paginator_qry(deductions, request.GET.get("page")) + return render( + request, + "payroll/deduction/view_deduction.html", + {"deductions": deductions, "f": deduction_filter}, + ) + + +@login_required +@permission_required("payroll.view_allowance") +def view_single_deduction(request, deduction_id): + """ + This method is used render template to view all the deduction instances + """ + + deduction = payroll.models.models.Deduction.objects.get(id=deduction_id) + return render( + request, + "payroll/deduction/view_single_deduction.html", + {"deduction": deduction}, + ) + + +@login_required +@permission_required("payroll.view_allowance") +def filter_deduction(request): + """ + This method is used search the deduction + """ + query_string = request.environ["QUERY_STRING"] + deductions = DeductionFilter(request.GET).qs + template = "payroll/deduction/list_deduction.html" + deductions = paginator_qry(deductions, request.GET.get("page")) + return render(request, template, {"deductions": deductions, "pd": query_string}) + + +@login_required +@permission_required("payroll.change_deduction") +def update_deduction(request, deduction_id): + """ + This method is used to update the deduction instance + """ + instance = payroll.models.models.Deduction.objects.get(id=deduction_id) + form = forms.DeductionForm(instance=instance) + if request.method == "POST": + form = forms.DeductionForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, "Deduction updated.") + return redirect(view_deduction) + return render(request, "payroll/common/form.html", {"form": form}) + + +@login_required +@permission_required("payroll.delete_deduction") +def delete_deduction(_request, deduction_id): + """ + This method is used to delete the deduction instance + Args: + id : deduction instance id + """ + payroll.models.models.Deduction.objects.get(id=deduction_id).delete() + return redirect(view_deduction) + + +@login_required +@permission_required("payroll.add_payslip") +def generate_payslip(request): + """ + Generate payslips for selected employees within a specified date range. + + Requires the user to be logged in and have the 'payroll.add_payslip' permission. + + """ + payslip_data = [] + json_data = [] + form = forms.GeneratePayslipForm() + if request.method == "POST": + form = forms.GeneratePayslipForm(request.POST) + if form.is_valid(): + employees = form.cleaned_data["employee_id"] + start_date = form.cleaned_data["start_date"] + end_date = form.cleaned_data["end_date"] + for employee in employees: + contract = payroll.models.models.Contract.objects.filter( + employee_id=employee, contract_status="active" + ).first() + if start_date < contract.contract_start_date: + start_date = contract.contract_start_date + payslip = payroll_calculation(employee, start_date, end_date) + payslip_data.append(payslip) + json_data.append(payslip["json_data"]) + + return render( + request, + "payroll/payslip/generate_payslip_list.html", + { + "payslip_data": payslip_data, + "json_data": json_data, + "start_date": start_date, + "end_date": end_date, + }, + ) + + return render(request, "payroll/common/form.html", {"form": form}) + + +@login_required +@permission_required("payroll.add_payslip") +def create_payslip(request): + """ + Create a payslip for an employee. + + This method is used to create a payslip for an employee based on the provided form data. + + Args: + request: The HTTP request object. + + Returns: + A rendered HTML template for the payslip creation form. + """ + form = forms.PayslipForm() + if request.method == "POST": + form = forms.PayslipForm(request.POST) + if form.is_valid(): + employee = form.cleaned_data["employee_id"] + start_date = form.cleaned_data["start_date"] + end_date = form.cleaned_data["end_date"] + payslip = payroll.models.models.Payslip.objects.filter( + employee_id=employee, start_date=start_date, end_date=end_date + ).first() + + if form.is_valid(): + employee = form.cleaned_data["employee_id"] + start_date = form.cleaned_data["start_date"] + end_date = form.cleaned_data["end_date"] + contract = payroll.models.models.Contract.objects.filter( + employee_id=employee, contract_status="active" + ).first() + if start_date < contract.contract_start_date: + start_date = contract.contract_start_date + payslip_data = payroll_calculation(employee, start_date, end_date) + payslip_data["payslip"] = payslip + return render( + request, + "payroll/payslip/individual_payslip.html", + payslip_data, + ) + return render(request, "payroll/common/form.html", {"form": form}) + + +@login_required +@permission_required("payroll.add_attendance") +def validate_start_date(request): + """ + This method to validate the contract start date and the pay period start date + """ + print("hitting.....") + start_date = request.GET.get("start_date") + end_date = request.GET.get("end_date") + employee_id = request.GET.getlist("employee_id") + start_datetime = datetime.strptime(start_date, "%Y-%m-%d").date() + end_datetime = datetime.strptime(end_date, "%Y-%m-%d").date() + + error_message = "" + response = {"valid": True, "message": error_message} + for emp_id in employee_id: + contract = payroll.models.models.Contract.objects.filter( + employee_id__id=emp_id, contract_status="active" + ).first() + if start_datetime < contract.contract_start_date: + error_message = f"
  • The {contract.employee_id}'s \ + contract start date is smaller than pay period start date
" + response["message"] = error_message + response["valid"] = False + if start_datetime > end_datetime: + error_message = "
  • The end date must be greater than \ + or equal to the start date.
" + response["message"] = error_message + response["valid"] = False + print(end_datetime) + print(datetime.today()) + if end_datetime > datetime.today().date(): + error_message = ( + '
  • The end date cannot be in the future.
' + ) + response["message"] = error_message + response["valid"] = False + return JsonResponse(response) + + +@login_required +@permission_required("payroll.view_payslip") +def view_individual_payslip(request, employee_id, start_date, end_date): + """ + This method is used to render the template for viewing a payslip. + """ + + payslip_data = payroll_calculation(employee_id, start_date, end_date) + return render( + request, + "payroll/payslip/individual_payslip.html", + payslip_data, + ) + + +@login_required +def view_payslip(request): + """ + This method is used to render the template for viewing a payslip. + """ + if request.user.has_perm("payroll.view_payslip"): + payslips = payroll.models.models.Payslip.objects.all() + else: + payslips = payroll.models.models.Payslip.objects.filter( + employee_id__employee_user_id=request.user + ) + filter_form = PayslipFilter(request.GET) + individual_form = forms.PayslipForm() + bulk_form = forms.GeneratePayslipForm() + payslips = paginator_qry(payslips, request.GET.get("page")) + + return render( + request, + "payroll/payslip/view_payslips.html", + { + "payslips": payslips, + "f": filter_form, + "individual_form": individual_form, + "bulk_form": bulk_form, + }, + ) + + +@login_required +def filter_payslip(request): + """ + Filter and retrieve a list of payslips based on the provided query parameters. + """ + query_string = request.environ["QUERY_STRING"] + if request.user.has_perm("payroll.view_payslip"): + payslips = PayslipFilter(request.GET).qs + else: + payslips = payroll.models.models.Payslip.objects.filter( + employee_id__employee_user_id=request.user + ) + template = "payroll/payslip/list_payslips.html" + payslips = paginator_qry(payslips, request.GET.get("page")) + return render(request, template, {"payslips": payslips, "pd": query_string}) + + +@login_required +@permission_required("payroll.add_allowance") +def hx_create_allowance(request): + """ + This method is used to render htmx allowance form + """ + form = forms.AllowanceForm() + return render(request, "payroll/htmx/form.html", {"form": form}) diff --git a/payroll/views/tax_views.py b/payroll/views/tax_views.py new file mode 100644 index 000000000..bf268c2ac --- /dev/null +++ b/payroll/views/tax_views.py @@ -0,0 +1,257 @@ +""" +tax_views.py + +This module contains view functions for handling federal tax-related operations. + +The functions in this module handle various tasks related to payroll, including creating +filing status, managing tax brackets, calculating federal tax, and more. These functions +utilize the Django framework and make use of the render and redirect functions from the +django.shortcuts module. + +""" +import math +from django.shortcuts import render, redirect +from django.contrib import messages +from horilla.decorators import permission_required, login_required +from payroll.models.tax_models import ( + TaxBracket, +) +from payroll.forms.tax_forms import ( + FilingStatusForm, + TaxBracketForm, + FederalTaxForm, +) +from payroll.models.models import FilingStatus + + +@login_required +@permission_required("payroll.view_filingstatus") +def filing_status_view(request): + """ + Display the filing status view. + + This view retrieves all filing statuses from the database and renders the + 'payroll/tax/filing_status_view.html' template with the filing status data. + + """ + status = FilingStatus.objects.all() + context = {"status": status} + return render(request, "payroll/tax/filing_status_view.html", context) + + +@login_required +@permission_required("payroll.add_filingstatus") +def create_filing_status(request): + """ + Create a filing status record for tax bracket based on user input. + + If the request method is POST and the form data is valid, save the filing status form + and redirect to the create-filing-status page. + + """ + filing_status_form = FilingStatusForm() + if request.method == "POST": + filing_status_form = FilingStatusForm(request.POST) + if filing_status_form.is_valid(): + filing_status_form.save() + return redirect("create-filing-status") + return render( + request, + "payroll/tax/filing_status_creation.html", + { + "form": filing_status_form, + }, + ) + + +@login_required +@permission_required("payroll.change_filingstatus") +def update_filing_status(request, filing_status_id): + """ + Update an existing filing status record based on user input. + + If the request method is POST and the form data is valid, update the filing status form + and redirect to the update-filing-status page. + + :param tax_bracket_id: The ID of the filing status to update. + """ + filing_status = FilingStatus.objects.get(id=filing_status_id) + filing_status_form = FilingStatusForm(instance=filing_status) + if request.method == "POST": + tax_bracket_form = FilingStatusForm(request.POST, instance=filing_status) + if tax_bracket_form.is_valid(): + tax_bracket_form.save() + return redirect(update_filing_status, filing_status_id=filing_status_id) + return render( + request, + "payroll/tax/filing_status_edit.html", + { + "form": filing_status_form, + }, + ) + + +@login_required +@permission_required("payroll.delete_filingstatus") +def filing_status_delete(request, filing_status_id): + """ + Delete a filing status. + + This view deletes a filing status with the given `filing_status_id` from the + database and redirects to the filing status view. + + """ + + try: + filing_status = FilingStatus.objects.get(id=filing_status_id) + filing_status.delete() + messages.info(request, "Filing status successfully deleted.") + except: + messages.error(request, "This filing status assigned to employees") + + status = FilingStatus.objects.all() + context = {"status": status} + return render(request, "payroll/tax/filing_status_list.html", context) + + +@login_required +@permission_required("payroll.view_taxbracket") +def tax_bracket_list(request, filing_status_id): + """ + Display a list of tax brackets for a specific filing status. + + This view retrieves all tax brackets associated with the given `filing_status_id` + and renders them in the "tax_bracket_view.html" template. + + Args: + request: The HTTP request object. + filing_status_id: The ID of the filing status for which to display tax brackets. + + Returns: + The rendered "tax_bracket_view.html" template with the tax brackets for the + specified filing status. + """ + tax_brackets = TaxBracket.objects.filter( + filing_status_id=filing_status_id + ).order_by("max_income") + context = { + "tax_brackets": tax_brackets, + } + return render(request, "payroll/tax/tax_bracket_view.html", context) + + +@login_required +@permission_required("payroll.add_taxbracket") +def create_tax_bracket(request, filing_status_id): + """ + Create a tax bracket record for federal tax calculation based on user input. + + If the request method is POST and the form data is valid, save the tax bracket form + and redirect to the tax-bracket-create page. + + """ + tax_bracket_form = TaxBracketForm(initial={"filing_status_id": filing_status_id}) + context = { + "form": tax_bracket_form, + } + if request.method == "POST": + tax_bracket_form = TaxBracketForm( + request.POST, initial={"filing_status_id": filing_status_id} + ) + if tax_bracket_form.is_valid(): + max_income = tax_bracket_form.cleaned_data.get("max_income") + if not max_income: + messages.info(request, "The maximum income will be infinite") + tax_bracket_form.instance.max_income = math.inf + tax_bracket_form.save() + return redirect(create_tax_bracket, filing_status_id=filing_status_id) + + context["form"] = tax_bracket_form + + return render(request, "payroll/tax/tax_bracket_creation.html", context) + + +@login_required +@permission_required("payroll.change_taxbracket") +def update_tax_bracket(request, tax_bracket_id): + """ + Update an existing tax bracket record based on user input. + + If the request method is POST and the form data is valid, update the tax bracket form + and redirect to the tax-bracket-create page. + + :param tax_bracket_id: The ID of the tax bracket to update. + """ + tax_bracket = TaxBracket.objects.get(id=tax_bracket_id) + tax_bracket_form = TaxBracketForm(instance=tax_bracket) + + if request.method == "POST": + tax_bracket_form = TaxBracketForm(request.POST, instance=tax_bracket) + if tax_bracket_form.is_valid(): + max_income = tax_bracket_form.cleaned_data.get("max_income") + if not max_income: + messages.info(request, "The maximum income will be infinite") + tax_bracket_form.instance.max_income = math.inf + tax_bracket_form.save() + + context = { + "form": tax_bracket_form, + } + return render(request, "payroll/tax/tax_bracket_edit.html", context) + + +@login_required +@permission_required("payroll.delete_taxbracket") +def delete_tax_bracket(_request, tax_bracket_id): + """ + Delete an existing tax bracket record. + + Retrieve the tax bracket with the specified ID and delete it from the database. + Then, redirect to the tax-bracket-create page. + + :param tax_bracket_id: The ID of the tax bracket to delete. + """ + tax_bracket = TaxBracket.objects.get(id=tax_bracket_id) + tax_bracket.delete() + return redirect( + "tax-bracket-list", filing_status_id=tax_bracket.filing_status_id.id + ) + + +@login_required +@permission_required("payroll.") +def create_federal_tax(request): + """ + Create a federal tax record based on user input. + + If the request method is POST and the form data is valid, calculate the federal tax + based on the taxable amount and filing status. Save the federal tax record and redirect + to the create-federal-tax page. + + """ + federal_tax_form = FederalTaxForm() + context = {"form": federal_tax_form} + if request.method == "POST": + federal_tax_form = FederalTaxForm(request.POST) + if federal_tax_form.is_valid(): + taxable_amount = federal_tax_form.cleaned_data["taxable_gross"] + filing_status = federal_tax_form.cleaned_data["filing_status_id"] + tax_brackets = TaxBracket.objects.filter( + filing_status_id=filing_status + ).order_by("min_income") + federal_tax = 0 + remaining_income = taxable_amount + if tax_brackets.first().min_income <= taxable_amount: + for tax_bracket in tax_brackets: + min_income = tax_bracket.min_income + max_income = tax_bracket.max_income + tax_rate = tax_bracket.tax_rate + if remaining_income <= 0: + break + taxable_amount = min(remaining_income, max_income - min_income) + tax_amount = taxable_amount * tax_rate / 100 + federal_tax += tax_amount + remaining_income -= taxable_amount + federal_tax_form.save() + return redirect("create-federal-tax") + return render(request, "payroll/tax/federal_tax.html", context) diff --git a/payroll/views/views.py b/payroll/views/views.py new file mode 100644 index 000000000..69db6b4fd --- /dev/null +++ b/payroll/views/views.py @@ -0,0 +1,373 @@ +""" +views.py + +This module is used to define the method for the path in the urls +""" +import json +from datetime import datetime, timedelta +from django.utils import timezone +from django.shortcuts import render, redirect +from django.http import JsonResponse, HttpResponseRedirect +from django.contrib import messages +from horilla.decorators import login_required, permission_required +from employee.models import Employee, EmployeeWorkInformation +from payroll.models.models import Payslip, WorkRecord +from payroll.models.models import Contract +from payroll.forms.forms import ContractForm, WorkRecordForm +from payroll.models.tax_models import PayrollSettings +from payroll.forms.component_forms import PayrollSettingsForm +from django.utils.translation import gettext_lazy as _ +from payroll.filters import ContractFilter +from payroll.methods.methods import paginator_qry + +# Create your views here. + + +@login_required +@permission_required("payroll.view_dashboard") +def dashboard(request): + """ + Dashboard render views + """ + return render(request, "payroll/dashboard.html") + + +@login_required +@permission_required("payroll.add_contract") +def contract_create(request): + """ + Contract create view + """ + form = ContractForm() + if request.method == "POST": + form = ContractForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.success(request, _("Contract Created")) + return redirect(contract_view) + return render(request, "payroll/common/form.html", {"form": form}) + + +@login_required +@permission_required("payroll.change_contract") +def contract_update(request, contract_id): + """ + Update an existing contract. + + Args: + request: The HTTP request object. + contract_id: The ID of the contract to update. + + Returns: + If the request method is POST and the form is valid, redirects to the contract view. + Otherwise, renders the contract update form. + + """ + contract = Contract.objects.get(id=contract_id) + contract_form = ContractForm(instance=contract) + if request.method == "POST": + contract_form = ContractForm(request.POST, request.FILES, instance=contract) + if contract_form.is_valid(): + contract_form.save() + messages.success(request, _("Contract updated")) + return redirect(contract_view) + return render( + request, + "payroll/common/form.html", + { + "form": contract_form, + }, + ) + + +@login_required +@permission_required("payroll.delete_contract") +def contract_delete(request, contract_id): + """ + Delete a contract. + + Args: + contract_id: The ID of the contract to delete. + + Returns: + Redirects to the contract view after successfully deleting the contract. + + """ + Contract.objects.get(id=contract_id).delete() + messages.success(request, _("Contract deleted")) + return redirect(contract_view) + + +@login_required +@permission_required("payroll.view_contract") +def contract_view(request): + """ + Contract view method + """ + + contracts = Contract.objects.all() + contracts = paginator_qry(contracts, request.GET.get("page")) + filter_form = ContractFilter(request.GET) + context = {"contracts": contracts, "f": filter_form} + + return render(request, "payroll/contract/contract_view.html", context) + + +@login_required +@permission_required("payroll.view_contract") +def view_single_contract(request, contract_id): + """ + Renders a single contract view page. + + Parameters: + - request (HttpRequest): The HTTP request object. + - contract_id (int): The ID of the contract to view. + + Returns: + The rendered contract single view page. + + """ + contract = Contract.objects.get(id=contract_id) + context = {"contract": contract} + return render(request, "payroll/contract/contract_single_view.html", context) + + +@login_required +@permission_required("payroll.view_contract") +def contract_filter(request): + """ + Filter contracts based on the provided query parameters. + + Args: + request: The HTTP request object containing the query parameters. + + Returns: + Renders the contract list template with the filtered contracts. + + """ + query_string = request.environ["QUERY_STRING"] + contracts_filter = ContractFilter(request.GET) + template = "payroll/contract/contract_list.html" + contracts = contracts_filter.qs + contracts = paginator_qry(contracts, request.GET.get("page")) + return render(request, template, {"contracts": contracts, "pd": query_string}) + + +@login_required +@permission_required("payroll.view_workrecord") +def work_record_create(request): + """ + Work record create view + """ + form = WorkRecordForm() + + context = {"form": form} + if request.POST: + form = WorkRecordForm(request.POST) + if form.is_valid(): + form.save() + else: + context["form"] = form + return render(request, "payroll/work_record/work_record_create.html", context) + + +@login_required +@permission_required("payroll.view_workrecord") +def work_record_view(request): + """ + Work record view method + """ + contracts = WorkRecord.objects.all() + context = {"contracts": contracts} + return render(request, "payroll/work_record/work_record_view.html", context) + + +@login_required +@permission_required("payroll.workrecord") +def work_record_employee_view(request): + """ + Work record by employee view method + """ + current_month_start_date = datetime.now().replace(day=1) + next_month_start_date = current_month_start_date + timedelta(days=31) + current_month_records = WorkRecord.objects.filter( + start_datetime__gte=current_month_start_date, + start_datetime__lt=next_month_start_date, + ).order_by("start_datetime") + current_date = timezone.now().date() + current_month = current_date.strftime("%B") + start_of_month = current_date.replace(day=1) + employees = Employee.objects.all() + + current_month_dates_list = [ + datetime.now().replace(day=day).date() for day in range(1, 32) + ] + + context = { + "days": range(1, 32), + "employees": employees, + "current_date": current_date, + "current_month": current_month, + "start_of_month": start_of_month, + "current_month_dates_list": current_month_dates_list, + "work_records": current_month_records, + } + + return render( + request, "payroll/work_record/work_record_employees_view.html", context + ) + + +@login_required +@permission_required("payroll.view_settings") +def settings(request): + """ + This method is used to render settings template + """ + instance = PayrollSettings.objects.first() + form = PayrollSettingsForm(instance=instance) + if request.method == "POST": + form = PayrollSettingsForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, _("Payroll settings updated.")) + return render(request, "payroll/settings/payroll_settings.html", {"form": form}) + + +@login_required +@permission_required("payroll.change_payslip") +def update_payslip_status(request): + """ + This method is used to update the payslip confirmation status + """ + pay_data = json.loads(request.GET["data"]) + emp_id = pay_data["employee"] + employee = Employee.objects.get(id=emp_id) + start_date = pay_data["start_date"] + end_date = pay_data["end_date"] + status = pay_data["status"] + contract_wage = pay_data["contract_wage"] + basic_pay = pay_data["basic_pay"] + gross_pay = pay_data["gross_pay"] + deduction = pay_data["total_deductions"] + net_pay = pay_data["net_pay"] + + filtered_instance = Payslip.objects.filter( + employee_id=employee, + start_date=start_date, + end_date=end_date, + ).first() + instance = filtered_instance if filtered_instance is not None else Payslip() + instance.employee_id = employee + instance.start_date = start_date + instance.end_date = end_date + instance.status = status + instance.basic_pay = basic_pay + instance.contract_wage = contract_wage + instance.gross_pay = gross_pay + instance.deduction = deduction + instance.net_pay = net_pay + instance.pay_head_data = pay_data + instance.save() + return JsonResponse({"message": "success"}) + + +@login_required +@permission_required("payroll.change_payslip") +def bulk_update_payslip_status(request): + """ + This method is used to update payslip status when generating payslip through + generate payslip method + """ + json_data = request.GET["json_data"] + pay_data = json.loads(json_data) + status = request.GET["status"] + + for json_entry in pay_data: + data = json.loads(json_entry) + emp_id = data["employee"] + employee = Employee.objects.get(id=emp_id) + + payslip_kwargs = { + "employee_id": employee, + "start_date": data["start_date"], + "end_date": data["end_date"], + } + filtered_instance = Payslip.objects.filter(**payslip_kwargs).first() + instance = filtered_instance if filtered_instance is not None else Payslip() + + instance.employee_id = employee + instance.start_date = data["start_date"] + instance.end_date = data["end_date"] + instance.status = status + instance.basic_pay = data["basic_pay"] + instance.contract_wage = data["contract_wage"] + instance.gross_pay = data["gross_pay"] + instance.deduction = data["total_deductions"] + instance.net_pay = data["net_pay"] + instance.pay_head_data = data + instance.save() + + return JsonResponse({"message": "success"}) + + +@login_required +# @permission_required("payroll.view_payslip") +def view_created_payslip(request, payslip_id): + """ + This method is used to view the saved payslips + """ + payslip = Payslip.objects.get(id=payslip_id) + # the data must be dictionary in the payslip model for the json field + data = payslip.pay_head_data + data["employee"] = payslip.employee_id + data["payslip"] = payslip + data["json_data"] = data.copy() + data["json_data"]["employee"] = payslip.employee_id.id + data["json_data"]["payslip"] = payslip.id + + return render(request, "payroll/payslip/individual_payslip.html", data) + + +@login_required +@permission_required("payroll.delete_payslip") +def delete_payslip(request, payslip_id): + """ + This method is used to delete payslip instances + Args: + payslip_id (int): Payslip model instance id + """ + try: + Payslip.objects.get(id=payslip_id).delete() + messages.success(request, _("Payslip deleted")) + except: + messages.error(request, _("Something went wrong")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@permission_required("payroll.add_contract") +def contract_info_initial(request): + """ + This is an ajax method to return json response to auto fill the contract + form fields + """ + employee_id = request.GET["employee_id"] + work_info = EmployeeWorkInformation.objects.filter(employee_id=employee_id).first() + response_data = { + "department": work_info.department_id.id + if work_info.department_id is not None + else "", + "job_position": work_info.job_position_id.id + if work_info.job_position_id is not None + else "", + "job_role": work_info.job_role_id.id + if work_info.job_role_id is not None + else "", + "shift": work_info.shift_id.id if work_info.shift_id is not None else "", + "work_type": work_info.work_type_id.id + if work_info.work_type_id is not None + else "", + "wage": work_info.basic_salary, + } + return JsonResponse(response_data) diff --git a/payroll/widgets/__init__.py b/payroll/widgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payroll/widgets/component_widgets.py b/payroll/widgets/component_widgets.py new file mode 100644 index 000000000..f3afd7e71 --- /dev/null +++ b/payroll/widgets/component_widgets.py @@ -0,0 +1,125 @@ +""" +Custom form widgets for conditional visibility and styling. +""" +from django import forms +from django.utils.safestring import SafeText, mark_safe + + +class AllowanceConditionalVisibility(forms.Widget): + """ + A custom widget that loads conditional js to the form. + + Example: + class MyForm(forms.Form): + my_field = forms.CharField(widget=AllowanceConditionalVisibility, required=False) + + """ + + def render(self, name, value, attrs=None, renderer=None): + # Exclude the label from the rendered HTML + rendered_script = '' + additional_script = f""" + + """ + attrs = attrs or {} + attrs["required"] = False + return mark_safe(rendered_script + additional_script) + + +class DeductionConditionalVisibility(forms.Widget): + """ + A custom widget that loads conditional js to the form. + + Example: + class MyForm(forms.Form): + my_field = forms.CharField(widget=DeductionConditionalVisibility, required=False) + + """ + + def render(self, name, value, attrs, renderer) -> SafeText: + # Exclude the label from the rendered HTML + rendered_script = '' + additional_script = f""" + + """ + attrs = attrs or {} + attrs["required"] = False + return mark_safe(rendered_script + additional_script) + + +class StyleWidget(forms.Widget): + """ + A custom widget that enhances the styling and functionality of elements. + + Example: + class MyForm(forms.Form): + my_field = forms.CharField(widget=styleWidget, required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['style'].widget = widget.styleWidget(form=self) + + """ + + def __init__(self, *args, form=None, **kwargs): + if form is not None: + for _, field in form.fields.items(): + field.widget.attrs.update( + {"data-widget": "style-widget", "class": "style-widget"} + ) + super().__init__(*args, **kwargs) + + def render(self, name, value, attrs=None, renderer=None): + """ + Renders the widget as HTML, including the necessary scripts and styles for select2. + + Args: + name (str): The name of the form field. + value (Any): The current value of the form field. + attrs (dict, optional): Additional HTML attributes for the widget. + renderer: A custom renderer to use, if applicable. + + Returns: + str: The rendered HTML representation of the widget. + """ + rendered_script = '' + additional_script = f""" + + + """ + attrs = attrs or {} + attrs["required"] = False + return mark_safe(rendered_script + additional_script) diff --git a/templates/sidebar.html b/templates/sidebar.html index e432d005c..771d090d4 100755 --- a/templates/sidebar.html +++ b/templates/sidebar.html @@ -125,7 +125,7 @@
- {% comment %}
  • +
  • Dashboard @@ -160,7 +160,7 @@
    -
  • {% endcomment %} +