From a34aa73d50f97b46b3a4834b73f2d7e98d634410 Mon Sep 17 00:00:00 2001 From: Horilla Date: Wed, 31 Jan 2024 16:20:15 +0530 Subject: [PATCH] [UPDT] PAYROLL: Add contribution report --- payroll/context_processors.py | 20 ++ payroll/forms/component_forms.py | 9 +- payroll/forms/forms.py | 20 +- payroll/settings.py | 6 + payroll/templates/contract_form.html | 5 + payroll/templates/payroll/common/form.html | 186 +++++++++--------- payroll/templates/payroll/dashboard.html | 34 ++++ .../payroll/dashboard/contribution.html | 39 ++++ payroll/urls/component_urls.py | 1 + payroll/urls/urls.py | 13 +- payroll/views/component_views.py | 67 ++++++- payroll/views/views.py | 136 ++++++++----- 12 files changed, 393 insertions(+), 143 deletions(-) create mode 100644 payroll/templates/payroll/dashboard/contribution.html diff --git a/payroll/context_processors.py b/payroll/context_processors.py index 69c4ec8d1..3a8fbfdff 100644 --- a/payroll/context_processors.py +++ b/payroll/context_processors.py @@ -3,7 +3,9 @@ context_processor.py This module is used to register context processor` """ +from employee.models import Employee from payroll.models import tax_models as models +from payroll.models.models import Deduction def default_currency(request): @@ -24,3 +26,21 @@ def host(request): """ protocol = "https" if request.is_secure() else "http" return {"host": request.get_host(), "protocol": protocol} + + +def get_deductions(request): + """ + This method used to return the deduction + """ + deductions = Deduction.objects.filter( + only_show_under_employee=False, employer_rate__gt=0 + ) + return {"get_deductions": deductions} + + +def get_active_employees(request): + """ + This method used to return the deduction + """ + employees = Employee.objects.filter(is_active=True) + return {"get_active_employees": employees} diff --git a/payroll/forms/component_forms.py b/payroll/forms/component_forms.py index 3f824ecc9..652129084 100644 --- a/payroll/forms/component_forms.py +++ b/payroll/forms/component_forms.py @@ -204,6 +204,9 @@ class PayslipForm(ModelForm): for contract in active_contracts if contract.employee_id.is_active ] + if self.instance.pk is None: + self.initial["start_date"] = datetime.date.today().replace(day=1) + self.initial["end_date"] = datetime.date.today() class Meta: """ @@ -281,6 +284,8 @@ class GeneratePayslipForm(HorillaForm): self.fields["start_date"].widget.attrs.update({"class": "oh-input w-100"}) self.fields["group_name"].widget.attrs.update({"class": "oh-input w-100"}) self.fields["end_date"].widget.attrs.update({"class": "oh-input w-100"}) + self.initial["start_date"] = datetime.date.today().replace(day=1) + self.initial["end_date"] = datetime.date.today() class Meta: """ @@ -420,7 +425,9 @@ class PayslipDeductionForm(ModelForm): """ Bonus Creating Form """ + verbose_name = _("Deduction") + class Meta: model = Deduction fields = [ @@ -450,7 +457,7 @@ class PayslipDeductionForm(ModelForm): context = {"form": self} table_html = render_to_string("one_time_deduction.html", context) return table_html - + class LoanAccountForm(ModelForm): """ diff --git a/payroll/forms/forms.py b/payroll/forms/forms.py index 200a2f5c3..db96dc537 100644 --- a/payroll/forms/forms.py +++ b/payroll/forms/forms.py @@ -1,6 +1,7 @@ """ forms.py """ + from django import forms from django.forms import widgets from django.utils.translation import gettext_lazy as trans @@ -65,7 +66,7 @@ class ContractForm(ModelForm): verbose_name = trans("Contract") contract_start_date = forms.DateField() - contract_end_date = forms.DateField() + contract_end_date = forms.DateField(required=False) class Meta: """ @@ -97,6 +98,20 @@ class ContractForm(ModelForm): "placeholder": "Select a date", } ) + self.fields["contract_status"].widget.attrs.update( + { + "class": "oh-select", + } + ) + if self.instance and self.instance.pk: + dynamic_url = self.get_dynamic_hx_post_url(self.instance) + self.fields["contract_status"].widget.attrs.update( + { + "hx-target": "#contractFormTarget", + "hx-post": dynamic_url, + "hx-swap": "outerHTML", + } + ) first = PayrollGeneralSetting.objects.first() if first and self.instance.pk is None: self.initial["notice_period_in_month"] = first.notice_period @@ -109,6 +124,9 @@ class ContractForm(ModelForm): table_html = render_to_string("contract_form.html", context) return table_html + def get_dynamic_hx_post_url(self, instance): + return f"/payroll/update-contract-status/{instance.pk}" + class WorkRecordForm(ModelForm): """ diff --git a/payroll/settings.py b/payroll/settings.py index 5f7c90d0b..2952898e5 100644 --- a/payroll/settings.py +++ b/payroll/settings.py @@ -9,6 +9,12 @@ from horilla.settings import TEMPLATES TEMPLATES[0]["OPTIONS"]["context_processors"].append( "payroll.context_processors.default_currency", ) +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "payroll.context_processors.get_deductions", +) +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "payroll.context_processors.get_active_employees", +) TEMPLATES[0]["OPTIONS"]["context_processors"].append( "payroll.context_processors.host", ) diff --git a/payroll/templates/contract_form.html b/payroll/templates/contract_form.html index 3358ff2c8..07ec4ac7c 100644 --- a/payroll/templates/contract_form.html +++ b/payroll/templates/contract_form.html @@ -8,6 +8,9 @@ {{ form.verbose_name }} +
+ {{ form.contract_status }} +
@@ -16,6 +19,7 @@
{% for field in form.visible_fields %} + {% if field.name != 'contract_status' %}
@@ -31,6 +35,7 @@ {% else %} {{ field|add_class:"form-control" }} {% endif %} {{field.errors}}
+ {% endif %} {% endfor %}
diff --git a/payroll/templates/payroll/common/form.html b/payroll/templates/payroll/common/form.html index beb2015c3..acc07a6eb 100644 --- a/payroll/templates/payroll/common/form.html +++ b/payroll/templates/payroll/common/form.html @@ -1,99 +1,99 @@ {% extends 'index.html' %} {% block content %} {% load static %} +
+
+
+ {% csrf_token %} {{form.as_p}} +
+
+ -{% endblock content %} \ No newline at end of file + ` + ); + $("#conditionContainer").prepend(conditionSet) + {% endfor %} + function initialData () { + $.each($("[name=other_fields],[name=other_conditions]"), function (indexInArray, valueOfElement) { + $(valueOfElement).val($(valueOfElement).attr("data-initial-value")).change() + }); + } + initialData() + {% endif %} + +
+{% endblock content %} diff --git a/payroll/templates/payroll/dashboard.html b/payroll/templates/payroll/dashboard.html index d4d5151f0..51205ffdb 100644 --- a/payroll/templates/payroll/dashboard.html +++ b/payroll/templates/payroll/dashboard.html @@ -171,6 +171,40 @@
+
+
+
+ {% comment %} {% endcomment %} + {% trans "Employer Contributions" %} +
+ + + + {% comment %} + + {% endcomment %} +
+ {% comment %} {% endcomment %} + +
+
+
+

{% trans "Contracts ending " %}

diff --git a/payroll/templates/payroll/dashboard/contribution.html b/payroll/templates/payroll/dashboard/contribution.html new file mode 100644 index 000000000..73303270d --- /dev/null +++ b/payroll/templates/payroll/dashboard/contribution.html @@ -0,0 +1,39 @@ +{% load i18n static %} +
+
+
+
+
+ {% trans 'Deduction' %} +
+
+ {% trans 'Employee Contribution' %} +
+
+ {% trans 'Employer Contribution' %} +
+
+
+
+ {% for deduction in contribution_deductions %} +
+
+
+
+ +
+ {{ deduction.title }} +
+
+
{{ currency }} {{ deduction.employee_contribution }}
+
{{ currency }} {{ deduction.employer_contribution }}
+
+ {% endfor %} + {% if not contribution_deductions %} +
+ +
+ {% endif %} +
+
+
diff --git a/payroll/urls/component_urls.py b/payroll/urls/component_urls.py index f41c998a3..a5a9ec6e5 100644 --- a/payroll/urls/component_urls.py +++ b/payroll/urls/component_urls.py @@ -127,4 +127,5 @@ urlpatterns = [ component_views.delete_attachments, name="delete-attachments", ), + path("get-contribution-report",component_views.get_contribution_report,name="get-contribution-report") ] diff --git a/payroll/urls/urls.py b/payroll/urls/urls.py index 4912bfcb4..c04e3529c 100644 --- a/payroll/urls/urls.py +++ b/payroll/urls/urls.py @@ -3,6 +3,7 @@ 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 from payroll.models.models import Contract, Payslip @@ -18,6 +19,11 @@ urlpatterns = [ name="update-contract", kwargs={"model": Contract}, ), + path( + "update-contract-status/", + views.contract_status_update, + name="update-contract-status", + ), path( "delete-contract/", views.contract_delete, @@ -126,7 +132,6 @@ urlpatterns = [ views.payslip_select_filter, name="payslip-select-filter", ), - path( "payroll-request-add-comment//", views.create_payrollrequest_comment, @@ -142,5 +147,9 @@ urlpatterns = [ views.delete_payrollrequest_comment, name="payroll-request-delete-comment", ), - path("initial-notice-period",views.initial_notice_period,name="initial-notice-period") + path( + "initial-notice-period", + views.initial_notice_period, + name="initial-notice-period", + ), ] diff --git a/payroll/views/component_views.py b/payroll/views/component_views.py index 295f37471..17fb54da4 100644 --- a/payroll/views/component_views.py +++ b/payroll/views/component_views.py @@ -4,6 +4,7 @@ component_views.py This module is used to write methods to the component_urls patterns respectively """ from collections import defaultdict +from itertools import groupby import json import operator from datetime import date, datetime @@ -930,7 +931,6 @@ def add_deduction(request): initial={"employee_id": employee_id, "one_time_date": instance.start_date}, ) if form.is_valid(): - # Save the form to create the Deduction instance deduction_instance = form.save(commit=False) deduction_instance.only_show_under_employee = True @@ -940,7 +940,7 @@ def add_deduction(request): deduction_instance.specific_employees.set([employee_id]) deduction_instance.include_active_employees = False deduction_instance.save() - + # Now create new payslip by deleting existing payslip new_post_data = QueryDict(mutable=True) new_post_data.update( @@ -1269,3 +1269,66 @@ def delete_attachments(request, _reimbursement_id): ReimbursementMultipleAttachment.objects.filter(id__in=ids).delete() messages.success(request, "Attachment deleted") return redirect(view_reimbursement) + + +@login_required +@permission_required("payroll.view_payslip") +def get_contribution_report(request): + """ + This method is used to get the contribution report + """ + employee_id = request.GET["employee_id"] + deudction_id = request.GET.get("deduction_id") + pay_heads = Payslip.objects.filter(employee_id__id=employee_id).values_list( + "pay_head_data", flat=True + ) + contribution_deductions = [] + deductions = [] + for head in pay_heads: + for deduction in head["gross_pay_deductions"]: + if deduction.get("deduction_id"): + deductions.append(deduction) + for deduction in head["basic_pay_deductions"]: + if deduction.get("deduction_id"): + deductions.append(deduction) + for deduction in head["pretax_deductions"]: + if deduction.get("deduction_id"): + deductions.append(deduction) + for deduction in head["post_tax_deductions"]: + if deduction.get("deduction_id"): + deductions.append(deduction) + for deduction in head["tax_deductions"]: + if deduction.get("deduction_id"): + deductions.append(deduction) + for deduction in head["net_deductions"]: + deductions.append(deduction) + + deductions.sort(key=lambda x: x["deduction_id"]) + grouped_deductions = { + key: list(group) + for key, group in groupby(deductions, key=lambda x: x["deduction_id"]) + } + + for deduction_id, group in grouped_deductions.items(): + title = group[0]["title"] + employee_contribution = sum(item["amount"] for item in group) + employer_contribution = sum( + item["employer_contribution_amount"] for item in group + ) + total_contribution = employee_contribution + employer_contribution + + contribution_deductions.append( + { + "deduction_id": deduction_id, + "title": title, + "employee_contribution": employee_contribution, + "employer_contribution": employer_contribution, + "total_contribution": total_contribution, + } + ) + + return render( + request, + "payroll/dashboard/contribution.html", + {"contribution_deductions": contribution_deductions}, + ) diff --git a/payroll/views/views.py b/payroll/views/views.py index 3c1739bc9..302b1a7a8 100644 --- a/payroll/views/views.py +++ b/payroll/views/views.py @@ -3,6 +3,7 @@ views.py This module is used to define the method for the path in the urls """ + from collections import defaultdict from urllib.parse import parse_qs import pandas as pd @@ -20,8 +21,19 @@ from base.methods import export_data, generate_colors, get_key_instances from employee.models import Employee, EmployeeWorkInformation from base.methods import closest_numbers from base.methods import generate_pdf -from payroll.models.models import PayrollGeneralSetting, Payslip, Reimbursement, ReimbursementrequestComment, WorkRecord, Contract -from payroll.forms.forms import ContractForm, ReimbursementrequestCommentForm, WorkRecordForm +from payroll.models.models import ( + PayrollGeneralSetting, + Payslip, + Reimbursement, + ReimbursementrequestComment, + WorkRecord, + Contract, +) +from payroll.forms.forms import ( + ContractForm, + ReimbursementrequestCommentForm, + WorkRecordForm, +) from payroll.models.tax_models import PayrollSettings from payroll.forms.component_forms import ContractExportFieldForm, PayrollSettingsForm from payroll.methods.methods import save_payslip @@ -94,6 +106,20 @@ def contract_update(request, contract_id, **kwargs): ) +def contract_status_update(request, contract_id): + if request.method == "POST": + contract = Contract.objects.get(id=contract_id) + contract_form = ContractForm(request.POST, request.FILES, instance=contract) + if contract_form.is_valid(): + contract_form.save() + messages.success(request, _("Contract status updated")) + else: + for errors in contract_form.errors.values(): + for error in errors: + messages.error(request, error) + return HttpResponse("") + + @login_required @permission_required("payroll.delete_contract") def contract_delete(request, contract_id): @@ -424,24 +450,26 @@ def contract_info_initial(request): 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 "", + "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 "", + "work_type": ( + work_info.work_type_id.id if work_info.work_type_id is not None else "" + ), "wage": work_info.basic_salary, "contract_start_date": work_info.date_joining if work_info.date_joining else "", - "contract_end_date": work_info.contract_end_date - if work_info.contract_end_date - else "", + "contract_end_date": ( + work_info.contract_end_date if work_info.contract_end_date else "" + ), } return JsonResponse(response_data) @@ -641,15 +669,19 @@ def contract_ending(request): date = request.GET.get("period") month = date.split("-")[1] year = date.split("-")[0] - - if request.GET.get("initialLoad") == 'true': - if month == '12': - month =0 + + if request.GET.get("initialLoad") == "true": + if month == "12": + month = 0 year = int(year) + 1 - contract_end = Contract.objects.filter(contract_end_date__month=int(month)+1,contract_end_date__year=int(year)) + contract_end = Contract.objects.filter( + contract_end_date__month=int(month) + 1, contract_end_date__year=int(year) + ) else: - contract_end = Contract.objects.filter(contract_end_date__month=int(month),contract_end_date__year=int(year)) + contract_end = Contract.objects.filter( + contract_end_date__month=int(month), contract_end_date__year=int(year) + ) ending_contract = [] for contract in contract_end: @@ -851,9 +883,11 @@ def payslip_export(request): df_table3 = df_table3.rename( columns={ - "contract_ending": f"Contract Ending {start_date} to {end_date}" - if start_date and end_date - else f"Contract Ending", + "contract_ending": ( + f"Contract Ending {start_date} to {end_date}" + if start_date and end_date + else f"Contract Ending" + ), } ) @@ -918,9 +952,11 @@ def payslip_export(request): 0, 0, max_columns - 1, - f"Payroll details {start_date} to {end_date}" - if start_date and end_date - else f"Payroll details", + ( + f"Payroll details {start_date} to {end_date}" + if start_date and end_date + else f"Payroll details" + ), heading_format, ) @@ -1202,19 +1238,25 @@ def create_payrollrequest_comment(request, payroll_id): """ payroll = Reimbursement.objects.filter(id=payroll_id).first() emp = request.user.employee_get - form = ReimbursementrequestCommentForm(initial={'employee_id':emp.id, 'request_id':payroll_id}) + form = ReimbursementrequestCommentForm( + initial={"employee_id": emp.id, "request_id": payroll_id} + ) if request.method == "POST": - form = ReimbursementrequestCommentForm(request.POST ) + form = ReimbursementrequestCommentForm(request.POST) if form.is_valid(): form.instance.employee_id = emp form.instance.request_id = payroll form.save() - form = ReimbursementrequestCommentForm(initial={'employee_id':emp.id, 'request_id':payroll_id}) + form = ReimbursementrequestCommentForm( + initial={"employee_id": emp.id, "request_id": payroll_id} + ) messages.success(request, _("Comment added successfully!")) - + if request.user.employee_get.id == payroll.employee_id.id: - rec = payroll.employee_id.employee_work_info.reporting_manager_id.employee_user_id + rec = ( + payroll.employee_id.employee_work_info.reporting_manager_id.employee_user_id + ) notify.send( request.user.employee_get, recipient=rec, @@ -1226,7 +1268,10 @@ def create_payrollrequest_comment(request, payroll_id): redirect="/payroll/view-reimbursement", icon="chatbox-ellipses", ) - elif request.user.employee_get.id == payroll.employee_id.employee_work_info.reporting_manager_id.id: + elif ( + request.user.employee_get.id + == payroll.employee_id.employee_work_info.reporting_manager_id.id + ): rec = payroll.employee_id.employee_user_id notify.send( request.user.employee_get, @@ -1240,7 +1285,10 @@ def create_payrollrequest_comment(request, payroll_id): icon="chatbox-ellipses", ) else: - rec = [payroll.employee_id.employee_user_id, payroll.employee_id.employee_work_info.reporting_manager_id.employee_user_id] + rec = [ + payroll.employee_id.employee_user_id, + payroll.employee_id.employee_work_info.reporting_manager_id.employee_user_id, + ] notify.send( request.user.employee_get, recipient=rec, @@ -1252,14 +1300,12 @@ def create_payrollrequest_comment(request, payroll_id): redirect="/payroll/view-reimbursement", icon="chatbox-ellipses", ) - + return HttpResponse("") return render( request, "payroll/reimbursement/reimbursement_request_comment_form.html", - { - "form": form, "request_id":payroll_id - }, + {"form": form, "request_id": payroll_id}, ) @@ -1268,7 +1314,9 @@ def view_payrollrequest_comment(request, payroll_id): """ This method is used to show Reimbursement request comments """ - comments = ReimbursementrequestComment.objects.filter(request_id=payroll_id).order_by('-created_at') + comments = ReimbursementrequestComment.objects.filter( + request_id=payroll_id + ).order_by("-created_at") no_comments = False if not comments.exists(): no_comments = True @@ -1276,7 +1324,7 @@ def view_payrollrequest_comment(request, payroll_id): return render( request, "payroll/reimbursement/comment_view.html", - {"comments": comments, 'no_comments': no_comments } + {"comments": comments, "no_comments": no_comments}, ) @@ -1300,7 +1348,7 @@ def initial_notice_period(request): notice_period = eval(request.GET["notice_period"]) settings = PayrollGeneralSetting.objects.first() settings = settings if settings else PayrollGeneralSetting() - settings.notice_period = max(notice_period,0) + settings.notice_period = max(notice_period, 0) settings.save() - messages.success(request,"Initial notice period updated") + messages.success(request, "Initial notice period updated") return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))