[ADD] PAYROLL: Python codeble federal tax feature

This commit is contained in:
Horilla
2024-08-14 12:16:09 +05:30
parent 91e76ddcdf
commit fe5626ab0f
10 changed files with 303 additions and 54 deletions

View File

@@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
from base.methods import reload_queryset
from horilla import horilla_middlewares
from payroll.methods import federal_tax
from payroll.models.models import FilingStatus
from payroll.models.tax_models import TaxBracket
@@ -95,6 +96,25 @@ class FilingStatusForm(ModelForm):
fields = "__all__"
exclude = ["is_active"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
attrs: dict = self.fields["use_py"].widget.attrs
attrs[
"onchange"
] = """
if($(this).is(':checked')){
$('#oc-editor').show();
}else{
$('#oc-editor').hide();
}
"""
if self.instance.pk is None:
self.instance.python_code = federal_tax.CODE
else:
del self.fields["use_py"]
del self.fields["python_code"]
class TaxBracketForm(ModelForm):
"""Form for creating and updating tax bracket."""

View File

@@ -0,0 +1,101 @@
"""
federal_tax.py
"""
CODE = '''
"""
federal_tax.py
"""
YEARLY_TAXABLE_INCOME = 189000.52
def calcluate_federal_tax(yearly_income: int, **kwargs) -> float:
"""
Federal Tax calculation method
yearly_income: The early converted 'based on' amount
eg: yearly_income-> 189000 then taxable_amount-> 39312.0 (yearly)
"""
def filter_brackets(brackets: list) -> list:
"""
This method to filter out the actual brackets/brackets range
"""
# brackets that contains actual bracket range, calculated_rate, and diff amount
filterd_brackets = []
for bracket in brackets:
if bracket["max"] > bracket["min"]:
# bracket: {'rate': 12, 'min': 11000, 'max': 44725}
# finding diff amount and adding to the bracket
bracket["diff"] = bracket["max"] - bracket["min"]
# find bracket rate from the difference and adding to bracket
bracket["calculated_rate"] = (bracket["rate"] / 100) * bracket["diff"]
# bracket: {'rate': 12, 'min': 11000, 'max': 44725, 'diff': 33725, 'calculated_rate': 4047.0}
filterd_brackets.append(bracket)
continue
# returning valid filtered brackets
return filterd_brackets
# returning valid filtered brackets
return filterd_brackets
# filter_brackets method/function will sort out the brackets
# for example for the 189000 yearly income come in the 32% group,
# so the final the max considered as min(231250,189000) which is 189000
brackets = [
{"rate": 10, "min": 0, "max": min(11000, yearly_income)},
{"rate": 12, "min": 11000, "max": min(44725, yearly_income)},
{"rate": 22, "min": 44725, "max": min(95375, yearly_income)},
{"rate": 24, "min": 95375, "max": min(182100, yearly_income)},
{"rate": 32, "min": 182100, "max": min(231250, yearly_income)},
{"rate": 35, "min": 231250, "max": min(578125, yearly_income)},
{"rate": 37, "min": 578125, "max": max(578125, yearly_income)},
]
# filtering the brackets to actual range
brackets = filter_brackets(brackets=brackets)
# finding yearly taxable amount
taxable_amount = sum(bracket["calculated_rate"] for bracket in brackets)
"""
use formated_result method to print the table
"""
# formated_result(brackets=brackets, taxable_amount=taxable_amount)
# returning the taxable amount later on the yearly taxable amount-
# is converted to daily and calculate federal tax for the total days between the
# Payslip period
return taxable_amount
def formated_result(brackets: dict, taxable_amount: float) -> None:
"""
It will print the brackets such a formated way
"""
col_width = 7
print("----------------------Brackets----------------------")
print(
f"|{'Rate':<{col_width}} |{'Min':<{col_width}} |{'Max':<{col_width}} |{'Taxable':<{col_width}} |{'Bracket Tax':<{col_width}} |"
)
for bracket in brackets:
print(
f"|{bracket['rate']:<{col_width}}% |{bracket['min']:<{col_width}} | {bracket['max']:<{col_width}} | {bracket['diff']:<{col_width}} | {round(bracket['calculated_rate'],2):<{col_width + 3}} |"
)
print(f"| YEARLY TAXABLE INCOME | {taxable_amount} |")
print("----------------------------------------------------")
month_taxable = calcluate_federal_tax(YEARLY_TAXABLE_INCOME)
print("YEARLY TAXABLE AMOUNT", month_taxable)
'''

View File

@@ -6,6 +6,7 @@ based on their contract details and income information.
"""
import datetime
import logging
from payroll.methods.payslip_calc import (
calculate_gross_pay,
@@ -14,6 +15,8 @@ from payroll.methods.payslip_calc import (
from payroll.models.models import Contract
from payroll.models.tax_models import TaxBracket
logger = logging.getLogger(__name__)
def calculate_taxable_amount(**kwargs):
"""Calculate the taxable amount for a given employee within a specific period.
@@ -39,52 +42,63 @@ def calculate_taxable_amount(**kwargs):
).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
<= tax_brackets.first().max_income
):
tax_rate = tax_brackets.first().tax_rate
tax_amount = yearly_income * tax_rate / 100
federal_tax += tax_amount
elif 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
tax_brackets = TaxBracket.objects.filter(filing_status_id=filing).order_by(
"min_income"
)
num_days = (end_date - start_date).days + 1
calculation_functions = {
"taxable_gross_pay": calculate_taxable_gross_pay,
"gross_pay": calculate_gross_pay,
}
based = filing.based_on
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)
federal_tax = 0
if filing is not None and not filing.use_py:
brackets = [
{
"rate": item["tax_rate"],
"min": item["min_income"],
"max": min(item["max_income"], yearly_income),
}
for item in tax_brackets.values("tax_rate", "min_income", "max_income")
]
filterd_brackets = []
for bracket in brackets:
if bracket["max"] > bracket["min"]:
bracket["diff"] = bracket["max"] - bracket["min"]
bracket["calculated_rate"] = (bracket["rate"] / 100) * bracket["diff"]
filterd_brackets.append(bracket)
continue
break
print(filterd_brackets)
federal_tax = sum(bracket["calculated_rate"] for bracket in filterd_brackets)
elif filing.use_py:
code = filing.python_code
code = code.replace("print(", "pass#print(")
code = code.replace(" formated_result(", "# formated_result(")
local_vars = {}
exec(code, {}, local_vars)
try:
federal_tax = local_vars["calcluate_federal_tax"](yearly_income)
except Exception as e:
logger.error(e)
federal_tax_for_period = 0
if federal_tax and (tax_brackets.exists() or filing.use_py):
daily_federal_tax = federal_tax / total_days
federal_tax_for_period = daily_federal_tax * num_days
return federal_tax_for_period

View File

@@ -30,7 +30,6 @@ from base.models import (
from employee.methods.duration_methods import strtime_seconds
from employee.models import BonusPoint, Employee, EmployeeWorkInformation
from horilla import horilla_middlewares
from horilla.methods import get_horilla_model_class
from horilla.models import HorillaModel
from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog
@@ -96,6 +95,8 @@ class FilingStatus(HorillaModel):
default="taxable_gross_pay",
verbose_name=_("Based on"),
)
use_py = models.BooleanField(verbose_name="Python Code", default=False)
python_code = models.TextField(null=True)
description = models.TextField(
blank=True,
verbose_name=_("Description"),

View File

@@ -46,3 +46,49 @@
</div>
</form>
</div>
<iframe
style="display: none;"
frameBorder="0"
height="450px"
id="oc-editor"
src="https://onecompiler.com/embed/python?codeChangeEvent=true&hideNew=true&hideNewFileOption=true&hideStdin=true&hideTitle=true&listenToEvents=true&availableLanguages=false&theme=dark"
width="100%"
>
</iframe>
<script>
$("#objectCreateModal #objectCreateModalTarget").css("max-width","90%")
console.log($("#objectCreateModal #objectCreateModalTarget"))
var iFrame = document.getElementById('oc-editor');
// Make sure the iframe is fully loaded before sending the postMessage
iFrame.onload = function() {
// Populate the code in the OneCompiler editor
iFrame.contentWindow.postMessage({
eventType: 'populateCode',
language: 'python',
files: [
{
"name": "federal_tax.py",
"content": `{{form.instance.python_code|safe}}`
}
]
}, "*");
// Trigger the code to run
iFrame.contentWindow.postMessage({
eventType: 'triggerRun'
}, "*");
};
window.onmessage = function (e) {
if (e.data && e.data.language) {
code = e.data.files[0].content
$("[name=python_code]").html(code);
// handle the e.data which contains the code object
}
};
</script>
<script>
$("[name=python_code]").parent().hide();
$("[name=use_py]").change();
</script>

View File

@@ -44,7 +44,7 @@
style="display: none"
>
<ul class="oh-dropdown__items">
{% if perms.payroll.add_taxbracket %}
{% if perms.payroll.add_taxbracket and not filing_status.use_py %}
<li class="oh-dropdown__item">
<a
class="oh-dropdown__link oh-dropdown__link"

View File

@@ -1,4 +1,5 @@
{% extends 'index.html' %} {% block content %} {% load static %} {% load i18n %}
<main :class="sidebarOpen ? 'oh-main__sidebar-visible' : ''">
<section class="oh-wrapper oh-main__topbar" x-data="{searchShow: false}">
<div class="oh-main__titlebar oh-main__titlebar--left">

View File

@@ -5,6 +5,7 @@
{% endfor %}
</div>
{% endif %}
{% if not filing_status.use_py %}
<div class="oh-sticky-table__table">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
@@ -75,3 +76,54 @@
{% endfor %}
</div>
</div>
{% else %}
<iframe
frameBorder="0"
height="450px"
id="oc-editor{{filing_status.pk}}"
src="https://onecompiler.com/embed/python?codeChangeEvent=true&hideNew=true&hideNewFileOption=true&hideStdin=true&hideTitle=true&listenToEvents=true&availableLanguages=false&theme=dark"
width="100%"
>
</iframe>
<script>
var iFrame = document.getElementById('oc-editor{{filing_status.pk}}');
// Make sure the iframe is fully loaded before sending the postMessage
iFrame.onload = function() {
// Populate the code in the OneCompiler editor
iFrame.contentWindow.postMessage({
eventType: 'populateCode',
language: 'python',
files: [
{
"name": "federal_tax.py",
"content": `{{filing_status.python_code|safe}}`
}
]
}, "*");
// Trigger the code to run
iFrame.contentWindow.postMessage({
eventType: 'triggerRun'
}, "*");
};
window.onmessage = function (e) {
if (e.data && e.data.language) {
code = e.data.files[0].content
$.ajax({
type: "post",
url: "{% url 'update-py-code' filing_status.pk %}",
data: {
csrfmiddlewaretoken: getCookie("csrftoken"),
code:code,
},
success: function (response) {
}
});
// handle the e.data which contains the code object
}
};
</script>{% endif %}

View File

@@ -50,4 +50,5 @@ urlpatterns = [
tax_views.delete_tax_bracket,
name="tax-bracket-delete",
),
path("update-py-code/<int:pk>/", tax_views.update_py_code, name="update-py-code"),
]

View File

@@ -14,7 +14,7 @@ import math
from urllib.parse import parse_qs
from django.contrib import messages
from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect, render
from django.utils.translation import gettext_lazy as _
@@ -166,12 +166,11 @@ def tax_bracket_list(request, filing_status_id):
The rendered "tax_bracket_view.html" template with the tax brackets for the
specified filing status.
"""
filing_status = FilingStatus.objects.get(id=filing_status_id)
tax_brackets = TaxBracket.objects.filter(
filing_status_id=filing_status_id
).order_by("max_income")
context = {
"tax_brackets": tax_brackets,
}
context = {"tax_brackets": tax_brackets, "filing_status": filing_status}
return render(request, "payroll/tax/tax_bracket_view.html", context)
@@ -273,3 +272,17 @@ def delete_tax_bracket(request, tax_bracket_id):
if filing_status_id
else HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
)
@login_required
@permission_required("payroll.change_taxbracket")
def update_py_code(request, pk):
"""
Ajax method to update python code of filing status
"""
code = request.POST["code"]
filing = FilingStatus.objects.get(pk=pk)
if not filing.python_code == code:
filing.python_code = code
filing.save()
return JsonResponse({"message": "success"})