diff --git a/base/templates/base/audit_tag/history_tracking_fields.html b/base/templates/base/audit_tag/history_tracking_fields.html new file mode 100644 index 000000000..db8ac0e92 --- /dev/null +++ b/base/templates/base/audit_tag/history_tracking_fields.html @@ -0,0 +1,56 @@ +{% load i18n %} + + +
+ {% csrf_token %} +
+

+ {% trans "Employee History Tracking" %} +

+
+
+ + +
+
+
+ {{ history_fields_form.tracking_fields }} +
+ {% if perms.payroll.change_payrollsettings %} + + {% endif %} +
+ + +
diff --git a/base/templates/base/general_settings.html b/base/templates/base/general_settings.html index f5cf8618d..6f9b25606 100644 --- a/base/templates/base/general_settings.html +++ b/base/templates/base/general_settings.html @@ -21,5 +21,5 @@ {% if perms.payroll.change_encashmentgeneralsetting %} {% include "settings/encashment_settings.html" %} {% endif %} - +{% include "base/audit_tag/history_tracking_fields.html" %} {% endblock settings %} \ No newline at end of file diff --git a/base/urls.py b/base/urls.py index 4471e6ce3..c0d911ca1 100644 --- a/base/urls.py +++ b/base/urls.py @@ -570,6 +570,7 @@ urlpatterns = [ path("settings/get-date-format/", views.get_date_format, name="get-date-format"), path("settings/save-time/", views.save_time_format, name="save_time_format"), path("settings/get-time-format/", views.get_time_format, name="get-time-format"), + path("history-field-settings",views.history_field_settings,name="history-field-settings"), path( "settings/attendance-settings-view/", views.validation_condition_view, diff --git a/base/views.py b/base/views.py index ce1bc04d3..584e86f67 100644 --- a/base/views.py +++ b/base/views.py @@ -26,7 +26,8 @@ from attendance.models import AttendanceValidationCondition, GraceTime from django.views.decorators.csrf import csrf_exempt from employee.filters import EmployeeFilter from employee.forms import ActiontypeForm -from horilla_audit.models import AuditTag +from horilla_audit.forms import HistoryTrackingFieldsForm +from horilla_audit.models import AuditTag, HistoryTrackingFields from notifications.models import Notification from notifications.base.models import AbstractNotification from notifications.signals import notify @@ -3750,6 +3751,13 @@ def general_settings(request): form = AnnouncementExpireForm(instance=instance) encashment_instance = EncashmentGeneralSettings.objects.first() encashment_form = EncashmentGeneralSettingsForm(instance=encashment_instance) + history_tracking_instance = HistoryTrackingFields.objects.first() + history_fields_form_initial = {} + if history_tracking_instance: + history_fields_form_initial = { + "tracking_fields": history_tracking_instance.tracking_fields['tracking_fields'] + } + history_fields_form = HistoryTrackingFieldsForm(initial = history_fields_form_initial) if request.method == "POST": form = AnnouncementExpireForm(request.POST, instance=instance) if form.is_valid(): @@ -3762,6 +3770,7 @@ def general_settings(request): { "form": form, "encashment_form": encashment_form, + "history_fields_form":history_fields_form, }, ) @@ -3898,6 +3907,20 @@ def get_time_format(request): # Return the date format as JSON response return JsonResponse({"selected_format": time_format}) +@login_required +def history_field_settings(request): + if request.method == "POST": + fields = request.POST.getlist("tracking_fields") + history_object, created = HistoryTrackingFields.objects.get_or_create( + pk=1, defaults={"tracking_fields": {"tracking_fields": fields}} + ) + + if not created: + history_object.tracking_fields = {"tracking_fields": fields} + history_object.save() + + return redirect(general_settings) + @login_required @permission_required("attendance.view_attendancevalidationcondition") diff --git a/employee/models.py b/employee/models.py index 73028e63f..30db3b16e 100644 --- a/employee/models.py +++ b/employee/models.py @@ -434,10 +434,20 @@ class EmployeeWorkInformation(models.Model): null=True, verbose_name=_("Company"), ) - tags = models.ManyToManyField(EmployeeTag, blank=True, verbose_name=_("tags")) - location = models.CharField(max_length=50, blank=True) - email = models.EmailField(max_length=254, blank=True, null=True) - mobile = models.CharField(max_length=254, blank=True, null=True) + tags = models.ManyToManyField( + EmployeeTag, blank=True, verbose_name=_("Employee tag") + ) + location = models.CharField( + max_length=50, blank=True, verbose_name=_("Work Location") + ) + email = models.EmailField( + max_length=254, blank=True, null=True, verbose_name=_("Email") + ) + mobile = models.CharField( + max_length=254, + blank=True, + null=True, + ) shift_id = models.ForeignKey( EmployeeShift, on_delete=models.DO_NOTHING, @@ -445,10 +455,16 @@ class EmployeeWorkInformation(models.Model): blank=True, verbose_name=_("Shift"), ) - date_joining = models.DateField(null=True, blank=True) + date_joining = models.DateField( + null=True, blank=True, verbose_name=_("Joining Date") + ) contract_end_date = models.DateField(blank=True, null=True) - basic_salary = models.IntegerField(null=True, blank=True, default=0) - salary_hour = models.IntegerField(null=True, blank=True, default=0) + basic_salary = models.IntegerField( + null=True, blank=True, default=0, verbose_name=_("Basic Salary") + ) + salary_hour = models.IntegerField( + null=True, blank=True, default=0, verbose_name=_("Salary Per Hour") + ) additional_info = models.JSONField(null=True, blank=True) experience = models.FloatField(null=True, blank=True, default=0) history = HorillaAuditLog( diff --git a/employee/static/employee/importExport.js b/employee/static/employee/importExport.js index 7f180dec8..51b3e0dd3 100644 --- a/employee/static/employee/importExport.js +++ b/employee/static/employee/importExport.js @@ -7,19 +7,19 @@ var downloadMessages = { }; var importSuccess = { - ar: "نجح الاستيراد", - de: "Import erfolgreich", - es: "Importado con éxito", - en: "Imported Successfully!", - fr: "Importation réussie", + ar: "نجح الاستيراد", // Arabic + de: "Import erfolgreich", // German + es: "Importado con éxito", // Spanish + en: "Imported Successfully!", // English + fr: "Importation réussie", // French }; var uploadSuccess = { - ar: "تحميل كامل", - de: "Upload abgeschlossen", - es: "Carga completa", - en: "Upload Complete!", - fr: "Téléchargement terminé", + ar: "تحميل كامل", // Arabic + de: "Upload abgeschlossen", // German + es: "Carga completa", // Spanish + en: "Upload Complete!", // English + fr: "Téléchargement terminé", // French }; var uploadingMessage = { @@ -55,14 +55,28 @@ function getCookie(name) { } function getCurrentLanguageCode(callback) { - $.ajax({ - type: "GET", - url: "/employee/get-language-code/", - success: function (response) { - var languageCode = response.language_code; - callback(languageCode); // Pass the language code to the callback - }, - }); + var languageCode = $("#main-section-data").attr("data-lang"); + var allowedLanguageCodes = ["ar", "de", "es", "en", "fr"]; + if (allowedLanguageCodes.includes(languageCode)) { + callback(languageCode); + } else { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var ajaxLanguageCode = response.language_code; + $("#main-section-data").attr("data-lang", ajaxLanguageCode); + callback( + allowedLanguageCodes.includes(ajaxLanguageCode) + ? ajaxLanguageCode + : "en" + ); + }, + error: function () { + callback("en"); + }, + }); + } } // Get the form element @@ -112,55 +126,57 @@ form.addEventListener("submit", function (event) { $("#work-info-import").click(function (e) { e.preventDefault(); var languageCode = null; - languageCode = $("#main-section-data").attr("data-lang"); - var confirmMessage = - downloadMessages[languageCode] || - ((languageCode = "en"), downloadMessages[languageCode]); - Swal.fire({ - text: confirmMessage, - icon: "question", - showCancelButton: true, - confirmButtonColor: "#008000", - cancelButtonColor: "#d33", - confirmButtonText: "Confirm", - }).then(function (result) { - if (result.isConfirmed) { - $("#loading").show(); + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = downloadMessages[languageCode]; + Swal.fire({ + text: confirmMessage, + icon: "question", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + $("#loading").show(); - var xhr = new XMLHttpRequest(); - xhr.open("GET", "/employee/work-info-import", true); - xhr.responseType = "arraybuffer"; + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/employee/work-info-import", true); + xhr.responseType = "arraybuffer"; - xhr.upload.onprogress = function (e) { - if (e.lengthComputable) { - var percent = (e.loaded / e.total) * 100; - $(".progress-bar") - .width(percent + "%") - .attr("aria-valuenow", percent); - $("#progress-text").text("Uploading... " + percent.toFixed(2) + "%"); - } - }; + xhr.upload.onprogress = function (e) { + if (e.lengthComputable) { + var percent = (e.loaded / e.total) * 100; + $(".progress-bar") + .width(percent + "%") + .attr("aria-valuenow", percent); + $("#progress-text").text( + "Uploading... " + percent.toFixed(2) + "%" + ); + } + }; - xhr.onload = function (e) { - if (this.status == 200) { - const file = new Blob([this.response], { - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - }); - const url = URL.createObjectURL(file); - const link = document.createElement("a"); - link.href = url; - link.download = "work_info_template.xlsx"; - document.body.appendChild(link); - link.click(); - } - }; + xhr.onload = function (e) { + if (this.status == 200) { + const file = new Blob([this.response], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = "work_info_template.xlsx"; + document.body.appendChild(link); + link.click(); + } + }; - xhr.onerror = function (e) { - console.error("Error downloading file:", e); - }; + xhr.onerror = function (e) { + console.error("Error downloading file:", e); + }; - xhr.send(); - } + xhr.send(); + } + }); }); }); @@ -173,78 +189,72 @@ $(document).ajaxStop(function () { }); function simulateProgress() { - var languageCode = null; - languageCode = $("#main-section-data").attr("data-lang"); - var importMessage = - importSuccess[languageCode] || - ((languageCode = "en"), importSuccess[languageCode]); - var uploadMessage = - uploadSuccess[languageCode] || - ((languageCode = "en"), uploadSuccess[languageCode]); - var uploadingMessage = - uploadingMessage[languageCode] || - ((languageCode = "en"), uploadingMessage[languageCode]); - let progressBar = document.querySelector(".progress-bar"); - let progressText = document.getElementById("progress-text"); + getCurrentLanguageCode(function (code) { + languageCode = code; + var importMessage = importSuccess[languageCode]; + var uploadMessage = uploadSuccess[languageCode]; + let progressBar = document.querySelector(".progress-bar"); + let progressText = document.getElementById("progress-text"); - let width = 0; - let interval = setInterval(function () { - if (width >= 100) { - clearInterval(interval); - progressText.innerText = uploadMessage; - setTimeout(function () { - document.getElementById("loading").style.display = "none"; - }, 3000); - Swal.fire({ - text: importMessage, - icon: "success", - showConfirmButton: false, - timer: 2000, - timerProgressBar: true, - }); - setTimeout(function () { - $("#workInfoImport").removeClass("oh-modal--show"); - location.reload(true); - }, 2000); - } else { - width++; - progressBar.style.width = width + "%"; - progressBar.setAttribute("aria-valuenow", width); - progressText.innerText = uploadingMessage + width + "%"; - } - }, 20); + let width = 0; + let interval = setInterval(function () { + if (width >= 100) { + clearInterval(interval); + progressText.innerText = uploadMessage; + setTimeout(function () { + document.getElementById("loading").style.display = "none"; + }, 3000); + Swal.fire({ + text: importMessage, + icon: "success", + showConfirmButton: false, + timer: 2000, + timerProgressBar: true, + }); + setTimeout(function () { + $("#workInfoImport").removeClass("oh-modal--show"); + location.reload(true); + }, 2000); + } else { + width++; + progressBar.style.width = width + "%"; + progressBar.setAttribute("aria-valuenow", width); + progressText.innerText = uploadingMessage[languageCode] + width + "%"; + } + }, 20); + }); } document .getElementById("workInfoImportForm") .addEventListener("submit", function (event) { event.preventDefault(); - var languageCode = null; - languageCode = $("#main-section-data").attr("data-lang"); - var errorMessage = - validationMessage[languageCode] || - ((languageCode = "en"), validationMessage[languageCode]); - var fileInput = $("#workInfoImportFile").val(); - var allowedExtensions = /(\.xlsx)$/i; + getCurrentLanguageCode(function (code) { + languageCode = code; + var errorMessage = validationMessage[languageCode]; - if (!allowedExtensions.exec(fileInput)) { - var errorMessage = document.createElement("div"); - errorMessage.classList.add("error-message"); + var fileInput = $("#workInfoImportFile").val(); + var allowedExtensions = /(\.xlsx)$/i; - errorMessage.textContent = errorMessage; + if (!allowedExtensions.exec(fileInput)) { + var errorMessage = document.createElement("div"); + errorMessage.classList.add("error-message"); - document.getElementById("error-container").appendChild(errorMessage); + errorMessage.textContent = errorMessage; - fileInput.value = ""; + document.getElementById("error-container").appendChild(errorMessage); - setTimeout(function () { - errorMessage.remove(); - }, 2000); + fileInput.value = ""; - return false; - } else { - document.getElementById("loading").style.display = "block"; + setTimeout(function () { + errorMessage.remove(); + }, 2000); - simulateProgress(); - } + return false; + } else { + document.getElementById("loading").style.display = "block"; + + simulateProgress(); + } + }); }); diff --git a/employee/templates/employee_export_filter.html b/employee/templates/employee_export_filter.html index 6df201753..ab48537e1 100644 --- a/employee/templates/employee_export_filter.html +++ b/employee/templates/employee_export_filter.html @@ -7,8 +7,7 @@
diff --git a/horilla_audit/forms.py b/horilla_audit/forms.py index f30bc21d0..8c0ee9bfe 100644 --- a/horilla_audit/forms.py +++ b/horilla_audit/forms.py @@ -24,10 +24,11 @@ class HistoryForm(forms.Form): required=False, label=_("Updation description"), ) - history_highlight = forms.BooleanField(required=False,label=_("Updation highlight")) + history_highlight = forms.BooleanField( + required=False, label=_("Updation highlight") + ) history_tags = forms.ModelMultipleChoiceField( - queryset=AuditTag.objects.all(), required=False, - label = _("Updation tag") + queryset=AuditTag.objects.all(), required=False, label=_("Updation tag") ) def __init__(self, *args, **kwargs) -> None: @@ -50,3 +51,33 @@ class HistoryForm(forms.Form): context = {"form": self} table_html = render_to_string("horilla_audit/horilla_audit_log.html", context) return table_html + + +class HistoryTrackingFieldsForm(forms.Form): + excluded_fields = [ + "id", + "employee_id", + "objects", + "mobile", + "contract_end_date", + "additional_info", + "experience", + ] + def __init__(self, *args, **kwargs): + from employee.models import EmployeeWorkInformation as model + super(HistoryTrackingFieldsForm, self).__init__(*args, **kwargs) + field_choices = [ + (field.name, field.verbose_name) + for field in model._meta.get_fields() + if hasattr(field, "verbose_name") and field.name not in self.excluded_fields + ] + self.fields["tracking_fields"] = forms.MultipleChoiceField( + choices=field_choices, + required=False, + widget=forms.SelectMultiple( + attrs={ + "class": "oh-select oh-select-2 select2-hidden-accessible", + "style": "height:270px;", + } + ), + ) \ No newline at end of file diff --git a/horilla_audit/methods.py b/horilla_audit/methods.py index e23bd4eee..3246547aa 100644 --- a/horilla_audit/methods.py +++ b/horilla_audit/methods.py @@ -6,7 +6,6 @@ This module is used to write methods related to the history from django.db import models from django.contrib.auth.models import User - class Bot: def __init__(self) -> None: self.__str__() @@ -54,6 +53,21 @@ def get_field_label(model_class, field_name): return None +def filter_history(histories,track_fields): + filtered_histories = [] + for history in histories: + changes = history.get("changes", []) + filtered_changes = [ + change + for change in changes + if change.get("field_name", "") in track_fields + ] + if filtered_changes: + history["changes"] = filtered_changes + filtered_histories.append(history) + histories = filtered_histories + return histories + def get_diff(instance): """ This method is used to find the differences in the history @@ -117,4 +131,10 @@ def get_diff(instance): "updated_by": updated_by, } ) + from .models import HistoryTrackingFields + history_tracking_instance = HistoryTrackingFields.objects.first() + if history_tracking_instance: + track_fields = history_tracking_instance.tracking_fields["tracking_fields"] + if track_fields: + delta_changes = filter_history(delta_changes,track_fields) return delta_changes diff --git a/horilla_audit/models.py b/horilla_audit/models.py index e3a6e6b88..1d9940ae1 100644 --- a/horilla_audit/models.py +++ b/horilla_audit/models.py @@ -1,6 +1,7 @@ """ models.py """ + from collections.abc import Iterable from django.db import models from django.dispatch import receiver @@ -119,6 +120,10 @@ def post_create_horilla_audit_log(sender, instance, *_args, **kwargs): pass +class HistoryTrackingFields(models.Model): + tracking_fields = models.JSONField(null=True, blank=True, editable=False) + + # class HistoryComment(models.Model): # """ # HistoryComment model