Files
ihrm/employee/forms.py
elchimeneas 641a4d4842 i18n: improve translations across horilla (#1014)
- Added trans() / _() translations to models, forms and templates.
- Updated Spanish locale (django.po).
- Fixed missing verbose_name translations.

Known issues:
- "Leave Type" label in horilla/leave/forms.py not translating.
- "Performance" and "Mails automations" still pending.
2025-12-23 11:10:04 +05:30

808 lines
26 KiB
Python

"""
forms.py
This module contains the form classes used in the application.
Each form represents a specific functionality or data input in the
application. They are responsible for validating
and processing user input data.
Classes:
- YourForm: Represents a form for handling specific data input.
Usage:
from django import forms
class YourForm(forms.Form):
field_name = forms.CharField()
def clean_field_name(self):
# Custom validation logic goes here
pass
"""
import logging
import re
from datetime import date, datetime
from typing import Any
from django import forms
from django.contrib.auth.models import User
from django.db.models import Q
from django.forms import DateInput, TextInput
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as trans
from base.methods import eval_validate, reload_queryset
from employee.models import (
Actiontype,
BonusPoint,
DisciplinaryAction,
Employee,
EmployeeBankDetails,
EmployeeGeneralSetting,
EmployeeNote,
EmployeeTag,
EmployeeWorkInformation,
NoteFiles,
Policy,
PolicyMultipleFile,
)
from horilla import horilla_middlewares
from horilla_audit.models import AccountBlockUnblock
logger = logging.getLogger(__name__)
class ModelForm(forms.ModelForm):
"""
Override of Django ModelForm to add initial styling and defaults.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
reload_queryset(self.fields)
request = getattr(horilla_middlewares._thread_locals, "request", None)
today = date.today()
now = datetime.now()
default_input_class = "oh-input w-100"
select_class = "oh-select oh-select-2"
checkbox_class = "oh-switch__checkbox"
for field_name, field in self.fields.items():
widget = field.widget
label = _(field.label) if field.label else ""
# Date field
if isinstance(widget, forms.DateInput):
field.initial = today
widget.input_type = "date"
widget.format = "%Y-%m-%d"
field.input_formats = ["%Y-%m-%d"]
existing_class = widget.attrs.get("class", default_input_class)
widget.attrs.update(
{
"class": f"{existing_class} form-control",
"placeholder": label,
}
)
# Time field
elif isinstance(widget, forms.TimeInput):
field.initial = now.strftime("%H:%M")
widget.input_type = "time"
widget.format = "%H:%M"
field.input_formats = ["%H:%M"]
existing_class = widget.attrs.get("class", default_input_class)
widget.attrs.update(
{
"class": f"{existing_class} form-control",
"placeholder": label,
}
)
# Number, Email, Text, File, URL fields
elif isinstance(
widget,
(
forms.NumberInput,
forms.EmailInput,
forms.TextInput,
forms.FileInput,
forms.URLInput,
),
):
existing_class = widget.attrs.get("class", default_input_class)
widget.attrs.update(
{
"class": f"{existing_class} form-control",
"placeholder": _(field.label.title()) if field.label else "",
}
)
# Select fields
elif isinstance(widget, forms.Select):
if not isinstance(field, forms.ModelMultipleChoiceField):
field.empty_label = _("---Choose {label}---").format(label=label)
existing_class = widget.attrs.get("class", select_class)
widget.attrs.update({"class": existing_class})
# Textarea
elif isinstance(widget, forms.Textarea):
existing_class = widget.attrs.get("class", default_input_class)
widget.attrs.update(
{
"class": f"{existing_class} form-control",
"placeholder": label,
"rows": 2,
"cols": 40,
}
)
# Checkbox types
elif isinstance(
widget, (forms.CheckboxInput, forms.CheckboxSelectMultiple)
):
existing_class = widget.attrs.get("class", checkbox_class)
widget.attrs.update({"class": existing_class})
# Set employee_id and company_id once
if request:
employee = getattr(request.user, "employee_get", None)
if employee:
if "employee_id" in self.fields and self._meta.model.__name__ not in [
"DisciplinaryAction"
]:
self.fields["employee_id"].initial = employee
if "company_id" in self.fields:
company_field = self.fields["company_id"]
company = getattr(employee, "get_company", None)
if company:
queryset = company_field.queryset
company_field.initial = (
company if company in queryset else queryset.first()
)
class UserForm(ModelForm):
"""
Form for User model
"""
class Meta:
"""
Meta class to add the additional info
"""
fields = ("groups",)
model = User
class UserPermissionForm(ModelForm):
"""
Form for User model
"""
class Meta:
"""
Meta class to add the additional info
"""
fields = ("groups", "user_permissions")
model = User
class EmployeeForm(ModelForm):
"""
Form for Employee model
"""
class Meta:
"""
Meta class to add the additional info
"""
model = Employee
fields = "__all__"
exclude = (
"employee_user_id",
"additional_info",
"is_from_onboarding",
"is_directly_converted",
"is_active",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["email"].widget.attrs["autocomplete"] = "email"
self.fields["phone"].widget.attrs["autocomplete"] = "phone"
self.fields["address"].widget.attrs["autocomplete"] = "address"
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.dob is not None:
initial["dob"] = instance.dob.strftime("%H:%M")
kwargs["initial"] = initial
else:
self.initial = {"badge_id": self.get_next_badge_id()}
def as_p(self, *args, **kwargs):
context = {"form": self}
return render_to_string("employee/create_form/personal_info_as_p.html", context)
def clean(self):
super().clean()
email = self.cleaned_data["email"]
query = Employee.objects.entire().filter(email=email)
if self.instance and self.instance.id:
query = query.exclude(id=self.instance.id)
existing_employee = query.first()
if existing_employee:
company_id = None
if (
hasattr(existing_employee, "employee_work_info")
and existing_employee.employee_work_info
):
company_id = existing_employee.employee_work_info.company_id
if company_id:
error_message = _(
"An Employee with this Email already exists in company {}".format(
company_id
)
)
else:
error_message = _("An Employee with this Email already exists")
raise forms.ValidationError({"email": error_message})
def get_next_badge_id(self):
"""
This method is used to generate badge id
"""
from base.context_processors import get_initial_prefix
from employee.methods.methods import get_ordered_badge_ids
prefix = get_initial_prefix(None)["get_initial_prefix"]
data = get_ordered_badge_ids()
result = []
try:
for sublist in data:
for item in sublist:
if isinstance(item, str) and item.lower().startswith(
prefix.lower()
):
# Find the index of the item in the sublist
index = sublist.index(item)
# Check if there is a next item in the sublist
if index + 1 < len(sublist):
result = sublist[index + 1]
result = re.findall(r"[a-zA-Z]+|\d+|[^a-zA-Z\d\s]", result)
if result:
prefix = []
incremented = False
for item in reversed(result):
total_letters = len(item)
total_zero_leads = 0
for letter in item:
if letter == "0":
total_zero_leads = total_zero_leads + 1
continue
break
if total_zero_leads:
item = item[total_zero_leads:]
if isinstance(item, list):
item = item[-1]
if not incremented and isinstance(eval_validate(str(item)), int):
item = int(item) + 1
incremented = True
if isinstance(item, int):
item = "{:0{}d}".format(item, total_letters)
prefix.insert(0, str(item))
prefix = "".join(prefix)
except Exception as e:
logger.exception(e)
prefix = get_initial_prefix(None)["get_initial_prefix"]
return prefix
def clean_badge_id(self):
"""
This method is used to clean the badge id
"""
badge_id = self.cleaned_data["badge_id"]
if badge_id:
all_employees = Employee.objects.entire()
queryset = all_employees.filter(badge_id=badge_id).exclude(
pk=self.instance.pk if self.instance else None
)
if queryset.exists():
raise forms.ValidationError(trans("Badge ID must be unique."))
if not re.search(r"\d", badge_id):
raise forms.ValidationError(
trans("Badge ID must contain at least one digit.")
)
return badge_id
class EmployeeWorkInformationForm(ModelForm):
"""
Form for EmployeeWorkInformation model
"""
class Meta:
"""
Meta class to add the additional info
"""
model = EmployeeWorkInformation
fields = "__all__"
exclude = ("employee_id", "additional_info", "experience")
def __init__(self, *args, disable=False, **kwargs):
super().__init__(*args, **kwargs)
self.fields["email"].widget.attrs["autocomplete"] = "email"
self.fields["job_position_id"].widget.attrs.update(
{
"onchange": "jobChange($(this))",
}
)
for field in self.fields:
self.fields[field].widget.attrs["placeholder"] = self.fields[field].label
if disable:
self.fields[field].disabled = True
field_names = {
"Department": "department",
"Job Position": "job_position",
"Job Role": "job_role",
"Work Type": "work_type",
"Employee Type": "employee_type",
"Shift": "employee_shift",
}
urls = {
"Department": "#dynamicDept",
"Job Position": "#dynamicJobPosition",
"Job Role": "#dynamicJobRole",
"Work Type": "#dynamicWorkType",
"Employee Type": "#dynamicEmployeeType",
"Shift": "#dynamicShift",
}
for label, field in self.fields.items():
if isinstance(field, forms.ModelChoiceField) and field.label in field_names:
if field.label is not None:
field_name = field_names.get(field.label)
if field.queryset.model != Employee and field_name:
translated_label = _(field.label)
empty_label = _("---Choose {label}---").format(
label=translated_label
)
self.fields[label] = forms.ChoiceField(
choices=[("", empty_label)]
+ list(field.queryset.values_list("id", f"{field_name}")),
required=field.required,
label=translated_label,
initial=field.initial,
widget=forms.Select(
attrs={
"class": "oh-select oh-select-2",
"onchange": f'onDynamicCreate(this.value,"{urls.get(field.label)}");',
}
),
)
self.fields[label].choices += [
("create", _("Create New {} ").format(translated_label))
]
def clean(self):
cleaned_data = super().clean()
if "employee_id" in self.errors:
del self.errors["employee_id"]
return cleaned_data
def as_p(self, *args, **kwargs):
context = {"form": self}
return render_to_string("employee/create_form/personal_info_as_p.html", context)
class EmployeeWorkInformationUpdateForm(ModelForm):
"""
Form for EmployeeWorkInformation model
"""
class Meta:
"""
Meta class to add the additional info
"""
model = EmployeeWorkInformation
fields = "__all__"
exclude = ("employee_id",)
def as_p(self, *args, **kwargs):
context = {"form": self}
return render_to_string("employee/create_form/personal_info_as_p.html", context)
class EmployeeBankDetailsForm(ModelForm):
"""
Form for EmployeeBankDetails model
"""
address = forms.CharField(widget=forms.Textarea(attrs={"rows": 2, "cols": 40}))
class Meta:
"""
Meta class to add the additional info
"""
model = EmployeeBankDetails
fields = (
"bank_name",
"account_number",
"branch",
"any_other_code1",
"address",
"country",
"state",
"city",
"any_other_code2",
)
exclude = ["employee_id", "is_active", "additional_info"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["address"].widget.attrs["autocomplete"] = "address"
for visible in self.visible_fields():
visible.field.widget.attrs["class"] = "oh-input w-100"
def as_p(self, *args, **kwargs):
context = {"form": self}
return render_to_string("employee/update_form/bank_info_as_p.html", context)
class EmployeeBankDetailsUpdateForm(ModelForm):
"""
Form for EmployeeBankDetails model
"""
class Meta:
"""
Meta class to add the additional info
"""
model = EmployeeBankDetails
fields = "__all__"
exclude = ["employee_id", "is_active", "additional_info"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for visible in self.visible_fields():
visible.field.widget.attrs["class"] = "oh-input w-100"
for field in self.fields:
self.fields[field].widget.attrs["placeholder"] = self.fields[field].label
def as_p(self, *args, **kwargs):
context = {"form": self}
return render_to_string("employee/update_form/bank_info_as_p.html", context)
excel_columns = [
("badge_id", trans("Badge ID")),
("employee_first_name", trans("First Name")),
("employee_last_name", trans("Last Name")),
("email", trans("Email")),
("phone", trans("Phone")),
("experience", trans("Experience")),
("gender", trans("Gender")),
("dob", trans("Date of Birth")),
("country", trans("Country")),
("state", trans("State")),
("city", trans("City")),
("address", trans("Address")),
("zip", trans("Zip Code")),
("marital_status", trans("Marital Status")),
("children", trans("Children")),
("is_active", trans("Is active")),
("emergency_contact", trans("Emergency Contact")),
("emergency_contact_name", trans("Emergency Contact Name")),
("emergency_contact_relation", trans("Emergency Contact Relation")),
("employee_work_info__email", trans("Work Email")),
("employee_work_info__mobile", trans("Work Phone")),
("employee_work_info__department_id", trans("Department")),
("employee_work_info__job_position_id", trans("Job Position")),
("employee_work_info__job_role_id", trans("Job Role")),
("employee_work_info__shift_id", trans("Shift")),
("employee_work_info__work_type_id", trans("Work Type")),
("employee_work_info__reporting_manager_id", trans("Reporting Manager")),
("employee_work_info__employee_type_id", trans("Employee Type")),
("employee_work_info__location", trans("Location")),
("employee_work_info__date_joining", trans("Date Joining")),
("employee_work_info__basic_salary", trans("Basic Salary")),
("employee_work_info__salary_hour", trans("Salary Hour")),
("employee_work_info__contract_end_date", trans("Contract End Date")),
("employee_work_info__company_id", trans("Company")),
("employee_bank_details__bank_name", trans("Bank Name")),
("employee_bank_details__branch", trans("Branch")),
("employee_bank_details__account_number", trans("Account Number")),
("employee_bank_details__any_other_code1", trans("Bank Code #1")),
("employee_bank_details__any_other_code2", trans("Bank Code #2")),
("employee_bank_details__country", trans("Bank Country")),
("employee_bank_details__state", trans("Bank State")),
("employee_bank_details__city", trans("Bank City")),
]
fields_to_remove = [
"badge_id",
"employee_first_name",
"employee_last_name",
"is_active",
"email",
"phone",
"employee_bank_details__account_number",
]
class EmployeeExportExcelForm(forms.Form):
selected_fields = forms.MultipleChoiceField(
choices=excel_columns,
widget=forms.CheckboxSelectMultiple,
initial=[
"badge_id",
"employee_first_name",
"employee_last_name",
"email",
"phone",
"gender",
"employee_work_info__department_id",
"employee_work_info__job_position_id",
"employee_work_info__job_role_id",
"employee_work_info__shift_id",
"employee_work_info__work_type_id",
"employee_work_info__reporting_manager_id",
"employee_work_info__employee_type_id",
"employee_work_info__location",
"employee_work_info__date_joining",
"employee_work_info__basic_salary",
"employee_work_info__salary_hour",
"employee_work_info__contract_end_date",
"employee_work_info__company_id",
],
)
class BulkUpdateFieldForm(forms.Form):
update_fields = forms.MultipleChoiceField(
choices=excel_columns, label=_("Select Fields to Update")
)
bulk_employee_ids = forms.CharField(widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
updated_choices = [
(value, label)
for value, label in self.fields["update_fields"].choices
if value not in fields_to_remove
]
self.fields["update_fields"].choices = updated_choices
for visible in self.visible_fields():
visible.field.widget.attrs["class"] = "oh-select oh-select-2 oh-input w-100"
class EmployeeNoteForm(ModelForm):
"""
Form for EmployeeNote model
"""
class Meta:
"""
Meta class to add the additional info
"""
model = EmployeeNote
fields = ("description",)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["note_files"] = MultipleFileField(label="files")
self.fields["note_files"].required = False
def save(self, commit: bool = ...) -> Any:
attachement = []
multiple_attachment_ids = []
attachements = None
if self.files.getlist("note_files"):
attachements = self.files.getlist("note_files")
self.instance.attachement = attachements[0]
multiple_attachment_ids = []
for attachement in attachements:
file_instance = NoteFiles()
file_instance.files = attachement
file_instance.save()
multiple_attachment_ids.append(file_instance.pk)
instance = super().save(commit)
if commit:
instance.note_files.add(*multiple_attachment_ids)
return instance, multiple_attachment_ids
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = [single_file_clean(data, initial)]
if len(result) == 0:
result = [[]]
return result[0]
class PolicyForm(ModelForm):
"""
PolicyForm
"""
class Meta:
model = Policy
fields = "__all__"
exclude = ["attachments", "is_active"]
widgets = {
"body": forms.Textarea(
attrs={"data-summernote": "", "style": "display:none;"}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["attachment"] = MultipleFileField(
label=trans("Attachments"), required=False
)
def save(self, *args, commit=True, **kwargs):
attachemnt = []
multiple_attachment_ids = []
attachemnts = None
if self.files.getlist("attachment"):
attachemnts = self.files.getlist("attachment")
multiple_attachment_ids = []
for attachemnt in attachemnts:
file_instance = PolicyMultipleFile()
file_instance.attachment = attachemnt
file_instance.save()
multiple_attachment_ids.append(file_instance.pk)
instance = super().save(commit)
if commit:
instance.attachments.add(*multiple_attachment_ids)
return instance, attachemnts
class BonusPointAddForm(ModelForm):
class Meta:
model = BonusPoint
fields = ["points", "reason"]
widgets = {
"reason": forms.TextInput(attrs={"required": "required"}),
}
class BonusPointRedeemForm(ModelForm):
class Meta:
model = BonusPoint
fields = ["points"]
def clean(self):
cleaned_data = super().clean()
available_points = BonusPoint.objects.filter(
employee_id=self.instance.employee_id
).first()
if not available_points or available_points.points < cleaned_data["points"]:
raise forms.ValidationError({"points": "Not enough bonus points to redeem"})
if cleaned_data["points"] <= 0:
raise forms.ValidationError(
{"points": "Points must be greater than zero to redeem."}
)
class DisciplinaryActionForm(ModelForm):
class Meta:
model = DisciplinaryAction
fields = "__all__"
exclude = ["objects", "is_active"]
action = forms.ModelChoiceField(
queryset=Actiontype.objects.all(),
label=_("Action"),
widget=forms.Select(
attrs={
"class": "oh-select oh-select-2",
"onchange": "actionTypeChange($(this))",
}
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
action_choices = [("", _("---Choose Action---"))] + list(
self.fields["action"].queryset.values_list("id", "title")
)
self.fields["action"].choices = action_choices
if self.instance.pk is None:
self.fields["action"].choices += [("create", _("Create new action type "))]
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 ActiontypeForm(ModelForm):
class Meta:
model = Actiontype
fields = "__all__"
exclude = ["is_active"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["action_type"].widget.attrs.update(
{
"onchange": "actionChange($(this))",
}
)
class EmployeeTagForm(ModelForm):
"""
Employee Tags form
"""
class Meta:
"""
Meta class for additional options
"""
model = EmployeeTag
fields = "__all__"
exclude = ["is_active"]
widgets = {"color": TextInput(attrs={"type": "color", "style": "height:50px"})}
class EmployeeGeneralSettingPrefixForm(forms.ModelForm):
class Meta:
model = EmployeeGeneralSetting
exclude = ["objects"]
widgets = {
"badge_id_prefix": forms.TextInput(attrs={"class": "oh-input w-100"}),
"company_id": forms.Select(attrs={"class": "oh-select oh-select-2 w-100"}),
}