diff --git a/attendance/forms.py b/attendance/forms.py index ac3fb9716..d41ec9029 100644 --- a/attendance/forms.py +++ b/attendance/forms.py @@ -253,6 +253,14 @@ class AttendanceUpdateForm(ModelForm): ) ): del self.fields["attendance_overtime_approve"] + self.fields["batch_attendance_id"].choices = list( + self.fields["batch_attendance_id"].choices + ) + [("dynamic_create", "Dynamic create")] + self.fields["batch_attendance_id"].widget.attrs.update( + { + "onchange": "dynamicBatchAttendance($(this))", + } + ) def as_p(self, *args, **kwargs): """ @@ -388,6 +396,14 @@ class AttendanceForm(ModelForm): } ) self.fields["work_type_id"].widget.attrs.update({"id": str(uuid.uuid4())}) + self.fields["batch_attendance_id"].choices = list( + self.fields["batch_attendance_id"].choices + ) + [("dynamic_create", "Dynamic create")] + self.fields["batch_attendance_id"].widget.attrs.update( + { + "onchange": "dynamicBatchAttendance($(this))", + } + ) def save(self, commit=True): instance = super().save(commit=False) @@ -424,9 +440,12 @@ class AttendanceForm(ModelForm): attendance_date=self.data["attendance_date"] ).filter(employee_id__id__in=employee_ids) if existing_attendance.exists(): + employee_names = [ + attendance.employee_id.__str__() for attendance in existing_attendance + ] raise ValidationError( { - "employee_id": f"""Already attendance exists for{list(existing_attendance.values_list("employee_id__employee_first_name",flat=True))} employees""" + "employee_id": f"Already attendance exists for {', '.join(employee_names)} employees" } ) @@ -617,7 +636,7 @@ class AttendanceRequestForm(ModelForm): "hx-swap": "outerHTML", "hx-select": "#id_attendance_worked_hour_parent_div", "hx-get": "/attendance/update-worked-hour-field", - "hx-trigger": "change delay:300ms", # Delay added here for 500ms + "hx-trigger": "change delay:300ms", # Delay added here for 300ms } ) @@ -665,6 +684,14 @@ class AttendanceRequestForm(ModelForm): } ) self.fields["work_type_id"].widget.attrs.update({"id": str(uuid.uuid4())}) + self.fields["batch_attendance_id"].choices = list( + self.fields["batch_attendance_id"].choices + ) + [("dynamic_create", "Dynamic create")] + self.fields["batch_attendance_id"].widget.attrs.update( + { + "onchange": "dynamicBatchAttendance($(this))", + } + ) class Meta: """ @@ -683,6 +710,7 @@ class AttendanceRequestForm(ModelForm): "attendance_worked_hour", "minimum_hour", "request_description", + "batch_attendance_id", ] def as_p(self, *args, **kwargs): @@ -1100,19 +1128,11 @@ class BulkAttendanceRequestForm(ModelForm): label=_("To Date"), widget=forms.DateInput(attrs={"type": "date", "class": "form-control"}), ) - create_batch = forms.BooleanField( + batch_attendance_id = forms.ModelChoiceField( + queryset=BatchAttendance.objects.all(), required=False, - initial=True, - label=_("Create Batch Request"), - help_text=_("Check this for create requests as a batch"), - widget=forms.CheckboxInput( - attrs={"class": "oh-checkbox", "onchange": "toggleBatchTitle()"} - ), - ) - batch_title = forms.CharField( - max_length=100, - required=False, - label="Batch name", + label="Batch", + widget=forms.Select(attrs={"onchange": "dynamicBatchAttendance($(this))"}), ) class Meta: @@ -1174,6 +1194,9 @@ class BulkAttendanceRequestForm(ModelForm): self.fields["employee_id"].queryset = Employee.objects.filter( employee_user_id=request.user ) + self.fields["batch_attendance_id"].choices = list( + self.fields["batch_attendance_id"].choices + ) + [("dynamic_create", "Dynamic create")] def clean(self): cleaned_data = self.cleaned_data @@ -1208,10 +1231,6 @@ class BulkAttendanceRequestForm(ModelForm): "There is no valid date to create attendance request between this date range" ) ) - if cleaned_data.get("create_batch") and ( - not cleaned_data.get("batch_title") or cleaned_data.get("batch_title") == "" - ): - raise ValidationError(_("Please enter a batch name")) return cleaned_data def save(self, commit=True): @@ -1228,8 +1247,10 @@ class BulkAttendanceRequestForm(ModelForm): minimum_hour = cleaned_data.get("minimum_hour") work_type_id = employee_id.employee_work_info.work_type_id date_list = get_date_list(employee_id, from_date, to_date) - batch_title = ( - cleaned_data.get("batch_title") if cleaned_data.get("batch_title") else None + batch = ( + cleaned_data.get("batch_attendance_id") + if cleaned_data.get("batch_attendance_id") + else None ) # Prepare initial data for the form initial_data = { @@ -1243,9 +1264,6 @@ class BulkAttendanceRequestForm(ModelForm): "minimum_hour": minimum_hour, "request_description": request_description, } - # Iterate over the dates and create attendance requests - if batch_title: - batch = BatchAttendance.objects.create(title=batch_title) for date in date_list: initial_data.update( { @@ -1261,7 +1279,7 @@ class BulkAttendanceRequestForm(ModelForm): instance.employee_id = employee_id instance.request_type = "create_request" instance.is_bulk_request = True - if batch_title: + if batch: instance.batch_attendance_id = batch instance.save() else: @@ -1285,3 +1303,34 @@ class WorkRecordsForm(ModelForm): fields = "__all__" model = WorkRecords + + +class BatchAttendanceForm(ModelForm): + """ + WorkRecordForm + """ + + verbose_name = _("Create attendance batch") + + class Meta: + """ + Meta class for additional options + """ + + fields = "__all__" + model = BatchAttendance + exclude = ["is_active"] + + def as_p(self, *args, **kwargs): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + context = {"form": self} + form_html = render_to_string("common_form.html", context) + return form_html + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.instance.pk: + self.verbose_name = _("Update batch attendance") diff --git a/attendance/models.py b/attendance/models.py index 604336a57..3d39ad164 100644 --- a/attendance/models.py +++ b/attendance/models.py @@ -154,13 +154,6 @@ class Attendance(HorillaModel): null=True, verbose_name=_("Attendance day"), ) - batch_attendance_id = models.ForeignKey( - BatchAttendance, - null=True, - blank=True, - on_delete=models.CASCADE, - verbose_name=_("Batch Attendance"), - ) attendance_clock_in_date = models.DateField( null=True, verbose_name=_("Check-In Date") ) @@ -186,6 +179,13 @@ class Attendance(HorillaModel): validators=[validate_time_format], verbose_name=_("Minimum hour"), ) + batch_attendance_id = models.ForeignKey( + BatchAttendance, + null=True, + blank=True, + on_delete=models.PROTECT, + verbose_name=_("Batch Attendance"), + ) attendance_overtime = models.CharField( default="00:00", validators=[validate_time_format], @@ -435,6 +435,9 @@ class Attendance(HorillaModel): "work_type_id": self.work_type_id.id if self.work_type_id else "", "attendance_worked_hour": self.attendance_worked_hour, "minimum_hour": self.minimum_hour, + "batch_attendance_id": ( + self.batch_attendance_id.id if self.batch_attendance_id else "" + ), # Add other fields you want to store } return serialized_data diff --git a/attendance/templates/attendance/attendance/attendance_add_batch.html b/attendance/templates/attendance/attendance/attendance_add_batch.html index ca1945e92..3894a2632 100644 --- a/attendance/templates/attendance/attendance/attendance_add_batch.html +++ b/attendance/templates/attendance/attendance/attendance_add_batch.html @@ -7,7 +7,7 @@ +
@@ -18,12 +18,11 @@
@@ -33,6 +32,5 @@ - + - diff --git a/attendance/templates/attendance/attendance/attendance_nav.html b/attendance/templates/attendance/attendance/attendance_nav.html index 13b35928e..6595bda13 100644 --- a/attendance/templates/attendance/attendance/attendance_nav.html +++ b/attendance/templates/attendance/attendance/attendance_nav.html @@ -233,6 +233,12 @@ data-target="#objectDetailsModal" hx-get="{% url 'attendance-add-to-batch' %}" hx-target="#objectDetailsModalTarget" hx-vals=""> +
  • + {% trans "Batches" %} +
  • {% if perms.attendance.delete_attendance %}
  • {% trans "Delete" %} diff --git a/attendance/templates/attendance/attendance/attendance_request_one.html b/attendance/templates/attendance/attendance/attendance_request_one.html index 1fc2b8407..8d88527c4 100644 --- a/attendance/templates/attendance/attendance/attendance_request_one.html +++ b/attendance/templates/attendance/attendance/attendance_request_one.html @@ -105,13 +105,18 @@ {{at_work}} -
    {% trans "Overtime" %} {{over_time}}
    -
    +
    + {% trans "Batch" %} + {{attendance_request.batch_attendance_id}} +
    +
    +
    +
    {% trans "Activities" %} diff --git a/attendance/templates/attendance/attendance/attendance_view.html b/attendance/templates/attendance/attendance/attendance_view.html index 4c525d14c..245500dd8 100644 --- a/attendance/templates/attendance/attendance/attendance_view.html +++ b/attendance/templates/attendance/attendance/attendance_view.html @@ -52,6 +52,15 @@
    + + {% endblock content %} diff --git a/attendance/templates/attendance/attendance/batch_attendance_form.html b/attendance/templates/attendance/attendance/batch_attendance_form.html new file mode 100644 index 000000000..eb181bdc0 --- /dev/null +++ b/attendance/templates/attendance/attendance/batch_attendance_form.html @@ -0,0 +1,35 @@ +{% load widget_tweaks %} +{% load i18n %} +{% if messages %} + + {% if previous_url %} + + {% endif %} + +{% endif %} +
    +
    + +
    +
    +
    + {{form.as_p}} +
    +
    +
    \ No newline at end of file diff --git a/attendance/templates/attendance/attendance/batches_list.html b/attendance/templates/attendance/attendance/batches_list.html new file mode 100644 index 000000000..d4954b3be --- /dev/null +++ b/attendance/templates/attendance/attendance/batches_list.html @@ -0,0 +1,108 @@ +{% load static %}{% load i18n %} + +
    +
    + + +
    {% trans "Attendance Batches" %}
    +
    +
    + {% if batches %} +
    +
    +
    +
    +
    +
    + {% trans "Batch" %} +
    +
    + {% trans "No of Attendances" %} +
    + {% if perms.attendance.delete_batchattendance %} +
    + {% trans "Action" %} +
    + {% endif %} +
    +
    +
    + {% for batch in batches %} +
    +
    + {% if batch.created_by == request.user %} + + {% else %} + {{batch.title}} + {% endif %} +
    +
    {{batch.attendance_set.all.count}}
    + +
    +
    + {% if perms.attendance.delete_batchattendance %} +
    + +
    + {% endif %} +
    +
    +
    + {% endfor %} +
    +
    +
    +
    + {% else %} +
    +
    + +

    + {% trans "There are no batches at the moment." %} +

    +
    +
    + {% endif %} +
    \ No newline at end of file diff --git a/attendance/templates/attendance/attendance/form.html b/attendance/templates/attendance/attendance/form.html index d1736a2ae..c9c0d70ce 100644 --- a/attendance/templates/attendance/attendance/form.html +++ b/attendance/templates/attendance/attendance/form.html @@ -1,5 +1,7 @@ {% load i18n %} -
    + {{form.as_p}}
    diff --git a/attendance/templates/requests/attendance/update_form.html b/attendance/templates/requests/attendance/update_form.html index 0b9dfd615..74be4a8db 100644 --- a/attendance/templates/requests/attendance/update_form.html +++ b/attendance/templates/requests/attendance/update_form.html @@ -1,4 +1,9 @@
    {% csrf_token %} {{form.as_p}}
    +{% comment %} {% endcomment %} diff --git a/attendance/templates/requests/attendance/view-requests.html b/attendance/templates/requests/attendance/view-requests.html index 98eb18bde..4b020e28a 100644 --- a/attendance/templates/requests/attendance/view-requests.html +++ b/attendance/templates/requests/attendance/view-requests.html @@ -57,7 +57,7 @@ - +
    @@ -145,8 +145,32 @@ $("#unselectAllInstances").click(function () { unselectAllReqAttendance(); }); + }); - }) + // Dynamic batch attendance create + function dynamicBatchAttendance(element){ + batch = element.val() + if (batch === 'dynamic_create'){ + var parentForm = element.parents().closest("form"); + previous_url = parentForm.attr('data-url'); + // clear hx-vals + $('#dynamicCreateBatchAttendanceSpan').attr("hx-vals",`{"previous_url":"${previous_url}"}`) + $('#dynamicCreateBatchAttendanceSpan').click(); + // unselect the choosen value + element.val('').change(); + console.log('element',element.val()) + + } + } + + // Batch title change + function batchTitleChange(element){ + console.log(element) + title = $(element).val() + batchId = $(element).data('id') + $('#updateTitleSpan').attr("hx-vals",`{"batch_id":"${batchId}","title":"${title}"}`) + $('#updateTitleSpan').click() + } diff --git a/attendance/urls.py b/attendance/urls.py index 148c47f7f..a27943e1f 100644 --- a/attendance/urls.py +++ b/attendance/urls.py @@ -300,6 +300,18 @@ urlpatterns = [ attendance.views.requests.request_new, name="request-new-attendance", ), + path( + "create-batch-attendance", + attendance.views.requests.create_batch_attendance, + name="create-batch-attendance", + ), + path("get-batches", attendance.views.requests.get_batches, name="get-batches"), + path("update-title", attendance.views.requests.update_title, name="update-title"), + path( + "delete-batch/", + attendance.views.requests.delete_batch, + name="delete-batch", + ), path( "employee-widget-filter", attendance.views.search.widget_filter, diff --git a/attendance/views/requests.py b/attendance/views/requests.py index fda63f42d..7547c2389 100644 --- a/attendance/views/requests.py +++ b/attendance/views/requests.py @@ -10,8 +10,9 @@ from datetime import date, datetime, time from urllib.parse import parse_qs from django.contrib import messages +from django.db.models import ProtectedError from django.http import HttpResponse, HttpResponseRedirect, JsonResponse -from django.shortcuts import render +from django.shortcuts import redirect, render from django.template.loader import render_to_string from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -19,6 +20,7 @@ from django.utils.translation import gettext_lazy as _ from attendance.filters import AttendanceFilters, AttendanceRequestReGroup from attendance.forms import ( AttendanceRequestForm, + BatchAttendanceForm, BulkAttendanceRequestForm, NewRequestForm, ) @@ -28,7 +30,12 @@ from attendance.methods.utils import ( paginator_qry, shift_schedule_today, ) -from attendance.models import Attendance, AttendanceActivity, AttendanceLateComeEarlyOut +from attendance.models import ( + Attendance, + AttendanceActivity, + AttendanceLateComeEarlyOut, + BatchAttendance, +) from attendance.views.clock_in_out import early_out, late_come from base.methods import ( choosesubordinates, @@ -40,7 +47,12 @@ from base.methods import ( ) from base.models import EmployeeShift, EmployeeShiftDay from employee.models import Employee -from horilla.decorators import hx_request_required, login_required, manager_can_enter +from horilla.decorators import ( + hx_request_required, + login_required, + manager_can_enter, + permission_required, +) from notifications.signals import notify @@ -49,7 +61,10 @@ def request_attendance(request): """ This method is used to render template to register new attendance for a normal user """ - form = AttendanceRequestForm() + if request.GET.get("previous_url"): + form = AttendanceRequestForm(initial=request.GET.dict()) + else: + form = AttendanceRequestForm() if request.method == "POST": form = AttendanceRequestForm(request.POST) if form.is_valid(): @@ -134,7 +149,10 @@ def request_new(request): if request.GET.get("bulk") and eval_validate(request.GET.get("bulk")): employee = request.user.employee_get - form = BulkAttendanceRequestForm(initial={"employee_id": employee}) + if request.GET.get("employee_id"): + form = BulkAttendanceRequestForm(initial=request.GET) + else: + form = BulkAttendanceRequestForm(initial={"employee_id": employee}) if request.method == "POST": form = BulkAttendanceRequestForm(request.POST) form.instance.attendance_clock_in_date = request.POST.get("from_date") @@ -155,7 +173,10 @@ def request_new(request): "requests/attendance/request_new_form.html", {"form": form, "bulk": True}, ) - form = NewRequestForm() + if request.GET.get("employee_id"): + form = NewRequestForm(initial=request.GET.dict()) + else: + form = NewRequestForm() form = choosesubordinates(request, form, "attendance.change_attendance") form.fields["employee_id"].queryset = form.fields[ "employee_id" @@ -195,25 +216,113 @@ def request_new(request): ) +@login_required +def create_batch_attendance(request): + form = BatchAttendanceForm() + previous_form_data = request.GET.urlencode() + previous_url = request.GET.get("previous_url") + # Split the string at "?" and extract the first part, then reattach the "?" + previous_url = previous_url.split("?")[0] + "?" + if "attendance-update" in previous_url: + hx_target = "#updateAttendanceModalBody" + elif "edit-validate-attendance" in previous_url: + hx_target = "#editValidateAttendanceRequestModalBody" + elif "request-attendance" in previous_url: + hx_target = "#objectUpdateModalTarget" + elif "attendance-create" in previous_url: + hx_target = "#addAttendanceModalBody" + else: + hx_target = "#objectCreateModalTarget" + if request.method == "POST": + form = BatchAttendanceForm(request.POST) + if form.is_valid(): + batch = form.save() + messages.success(request, _("Attendance batch created successfully.")) + previous_form_data += f"&batch_attendance_id={batch.id}" + return render( + request, + "attendance/attendance/batch_attendance_form.html", + { + "form": form, + "previous_form_data": previous_form_data, + "previous_url": previous_url, + "hx_target": hx_target, + }, + ) + + +@login_required +def get_batches(request): + batches = BatchAttendance.objects.all() + return render( + request, "attendance/attendance/batches_list.html", {"batches": batches} + ) + + +@login_required +def update_title(request): + batch_id = request.POST.get("batch_id") + try: + batch = BatchAttendance.objects.filter(id=batch_id).first() + if batch.created_by == request.user: + title = request.POST.get("title") + batch.title = title + batch.save() + messages.success(request, _("Batch attendance title updated sucessfully.")) + else: + messages.info(request, _("You don't have permission.")) + except: + messages.error(request, _("Something went wrong.")) + return redirect(reverse("get-batches")) + + +@login_required +@permission_required("attendance.delete_batchattendance") +def delete_batch(request, batch_id): + try: + batch_name = BatchAttendance.objects.filter(id=batch_id).first().__str__() + BatchAttendance.objects.filter(id=batch_id).first().delete() + messages.success( + request, _(f"{batch_name} - batch has been deleted sucessfully") + ) + except ProtectedError as e: + model_verbose_names_set = set() + for obj in e.protected_objects: + # Convert the lazy translation proxy to a string. + model_verbose_names_set.add(str(_(obj._meta.verbose_name.capitalize()))) + model_names_str = ", ".join(model_verbose_names_set) + messages.error( + request, + _("This {} is already in use for {}.").format(batch_name, model_names_str), + ), + except: + messages.error(request, _("Something went wrong.")) + + return redirect(reverse("get-batches")) + + @login_required def attendance_request_changes(request, attendance_id): """ This method is used to store the requested changes to the instance """ attendance = Attendance.objects.get(id=attendance_id) - form = AttendanceRequestForm(instance=attendance) - form.fields["work_type_id"].widget.attrs.update( - { - "class": "w-100", - "style": "height:50px;border-radius:0;border:1px solid hsl(213deg,22%,84%)", - } - ) - form.fields["shift_id"].widget.attrs.update( - { - "class": "w-100", - "style": "height:50px;border-radius:0;border:1px solid hsl(213deg,22%,84%)", - } - ) + if request.GET.get("previous_url"): + form = AttendanceRequestForm(initial=request.GET.dict()) + else: + form = AttendanceRequestForm(instance=attendance) + # form.fields["work_type_id"].widget.attrs.update( + # { + # "class": "w-100", + # "style": "height:50px;border-radius:0;border:1px solid hsl(213deg,22%,84%)", + # } + # ) + # form.fields["shift_id"].widget.attrs.update( + # { + # "class": "w-100", + # "style": "height:50px;border-radius:0;border:1px solid hsl(213deg,22%,84%)", + # } + # ) if request.method == "POST": form = AttendanceRequestForm(request.POST, instance=copy.copy(attendance)) form.fields["work_type_id"].widget.attrs.update( @@ -277,11 +386,17 @@ def attendance_request_changes(request, attendance_id): ) return HttpResponse( render( - request, "requests/attendance/form.html", {"form": form} + request, + "requests/attendance/form.html", + {"form": form, "attendance_id": attendance_id}, ).content.decode("utf-8") + "" ) - return render(request, "requests/attendance/form.html", {"form": form}) + return render( + request, + "requests/attendance/form.html", + {"form": form, "attendance_id": attendance_id}, + ) @login_required @@ -303,6 +418,7 @@ def validate_attendance_request(request, attendance_id): "shift_id": None, "work_type_id": None, "attendance_worked_hour": None, + "batch_attendance_id": None, } if attendance.request_type == "create_request": other_dict = first_dict @@ -701,9 +817,12 @@ def edit_validate_attendance(request, attendance_id): """ attendance = Attendance.objects.get(id=attendance_id) initial = attendance.serialize() - if attendance.request_type != "create_request": - initial = json.loads(attendance.requested_data) - initial["request_description"] = attendance.request_description + if request.GET.get("previous_url"): + initial = request.GET.dict() + else: + if attendance.request_type != "create_request": + initial = json.loads(attendance.requested_data) + initial["request_description"] = attendance.request_description form = AttendanceRequestForm(initial=initial) form.instance.id = attendance.id hx_target = request.META.get("HTTP_HX_TARGET")