From cd1cd2e83af85379b599b711d22f13d576e5d4f412dda3b841d15201edc49261 Mon Sep 17 00:00:00 2001 From: nestict Date: Fri, 16 Jan 2026 12:47:27 +0100 Subject: [PATCH] Upload files to "asset" Signed-off-by: nestict --- asset/__init__.py | 1 + asset/admin.py | 34 + asset/apps.py | 33 + asset/filters.py | 375 +++++++++ asset/forms.py | 384 +++++++++ asset/models.py | 331 ++++++++ asset/resources.py | 21 + asset/scheduler.py | 102 +++ asset/sidebar.py | 59 ++ asset/tests.py | 5 + asset/urls.py | 207 +++++ asset/views.py | 1878 ++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 3430 insertions(+) create mode 100644 asset/__init__.py create mode 100644 asset/admin.py create mode 100644 asset/apps.py create mode 100644 asset/filters.py create mode 100644 asset/forms.py create mode 100644 asset/models.py create mode 100644 asset/resources.py create mode 100644 asset/scheduler.py create mode 100644 asset/sidebar.py create mode 100644 asset/tests.py create mode 100644 asset/urls.py create mode 100644 asset/views.py diff --git a/asset/__init__.py b/asset/__init__.py new file mode 100644 index 0000000..4d4ebe9 --- /dev/null +++ b/asset/__init__.py @@ -0,0 +1 @@ +from asset import scheduler diff --git a/asset/admin.py b/asset/admin.py new file mode 100644 index 0000000..2b9d153 --- /dev/null +++ b/asset/admin.py @@ -0,0 +1,34 @@ +""" +Module: admin.py +Description: This module is responsible for registering models + to be managed through the Django admin interface. +Models Registered: +- Asset: Represents a physical asset with relevant details. +- AssetCategory: Categorizes assets for better organization. +- AssetRequest: Manages requests for acquiring assets. +- AssetAssignment: Tracks the assets assigned to employees. +- AssetLot: Represents a collection of assets under a lot number. +""" + +from django.contrib import admin + +from .models import ( + Asset, + AssetAssignment, + AssetCategory, + AssetDocuments, + AssetLot, + AssetReport, + AssetRequest, +) + +# Register your models here. + + +admin.site.register(Asset) +admin.site.register(AssetRequest) +admin.site.register(AssetCategory) +admin.site.register(AssetAssignment) +admin.site.register(AssetLot) +admin.site.register(AssetReport) +admin.site.register(AssetDocuments) diff --git a/asset/apps.py b/asset/apps.py new file mode 100644 index 0000000..21959e2 --- /dev/null +++ b/asset/apps.py @@ -0,0 +1,33 @@ +""" +Module: apps.py +Description: Configuration for the 'asset' app. +""" + +from django.apps import AppConfig + + +class AssetConfig(AppConfig): + """ + Class: AssetConfig + Description: Configuration class for the 'asset' app. + + Attributes: + default_auto_field (str): Default auto-generated field type for primary keys. + name (str): Name of the app. + """ + + default_auto_field = "django.db.models.BigAutoField" + name = "asset" + + def ready(self): + from django.urls import include, path + + from horilla.horilla_settings import APP_URLS, APPS + from horilla.urls import urlpatterns + + APPS.append("asset") + urlpatterns.append( + path("asset/", include("asset.urls")), + ) + APP_URLS.append("asset.urls") + super().ready() diff --git a/asset/filters.py b/asset/filters.py new file mode 100644 index 0000000..63fe625 --- /dev/null +++ b/asset/filters.py @@ -0,0 +1,375 @@ +""" +Module containing custom filter classes for various models. +""" + +import uuid + +import django_filters +from django import forms +from django.db.models import Q +from django_filters import FilterSet + +from base.methods import reload_queryset + +from .models import Asset, AssetAssignment, AssetCategory, AssetRequest + + +class CustomFilterSet(FilterSet): + """ + Custom FilterSet class that applies specific CSS classes to filter + widgets. + + The class applies CSS classes to different types of filter widgets, + such as NumberInput, EmailInput, TextInput, Select, Textarea, + CheckboxInput, CheckboxSelectMultiple, and ModelChoiceField. The + CSS classes are applied to enhance the styling and behavior of the + filter widgets. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.form.fields) + for field_name, field in self.form.fields.items(): + filter_widget = self.filters[field_name] + widget = filter_widget.field.widget + if isinstance( + widget, (forms.NumberInput, forms.EmailInput, forms.TextInput) + ): + field.widget.attrs.update({"class": "oh-input w-100"}) + elif isinstance(widget, (forms.Select,)): + field.widget.attrs.update( + { + "class": "oh-select oh-select-2", + } + ) + elif isinstance(widget, (forms.Textarea)): + field.widget.attrs.update({"class": "oh-input w-100"}) + elif isinstance( + widget, + ( + forms.CheckboxInput, + forms.CheckboxSelectMultiple, + ), + ): + filter_widget.field.widget.attrs.update( + {"class": "oh-switch__checkbox"} + ) + elif isinstance(widget, (forms.ModelChoiceField)): + field.widget.attrs.update( + { + "class": "oh-select oh-select-2 ", + } + ) + elif isinstance(widget, (forms.DateField)): + field.widget.attrs.update({"type": "date", "class": "oh-input w-100"}) + if isinstance(field, django_filters.CharFilter): + field.lookup_expr = "icontains" + + +class AssetExportFilter(CustomFilterSet): + """ + Custom filter class for exporting filtered Asset data. + """ + + class Meta: + """ + A nested class that specifies the configuration for the filter. + model(class): The Asset model is used to filter. + fields (str): A special value "__all__" to include all fields + of the model in the filter. + """ + + model = Asset + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form.fields["asset_purchase_date"].widget.attrs.update({"type": "date"}) + + +class AssetFilter(CustomFilterSet): + """ + Custom filter set for Asset instances. + """ + + search = django_filters.CharFilter(method="search_method") + category = django_filters.CharFilter(field_name="asset_category_id") + + class Meta: + """ + A nested class that specifies the configuration for the filter. + model(class): The Asset model is used to filter. + fields (str): A special value "__all__" to include all fields + of the model in the filter. + """ + + model = Asset + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for visible in self.form.visible_fields(): + visible.field.widget.attrs["id"] = str(uuid.uuid4()) + + def search_method(self, queryset, _, value): + """ + Search method + """ + return ( + queryset.filter(asset_name__icontains=value) + | queryset.filter(asset_category_id__asset_category_name__icontains=value) + ).distinct() + + +class CustomAssetFilter(CustomFilterSet): + """ + Custom filter set for asset assigned to employees instances. + """ + + asset_id__asset_name = django_filters.CharFilter(lookup_expr="icontains") + + class Meta: + """ + Specifies the model and fields to be used for filtering AssetAssignment instances. + + Attributes: + model (class): The model class AssetAssignment to be filtered. + fields (list): The fields to include in the filter, referring to + related AssetAssignment fields. + """ + + model = AssetAssignment + fields = [ + "asset_id__asset_name", + "asset_id__asset_status", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for visible in self.form.visible_fields(): + visible.field.widget.attrs["id"] = str(uuid.uuid4()) + + +class AssetRequestFilter(CustomFilterSet): + """ + Custom filter set for AssetRequest instances. + """ + + search = django_filters.CharFilter(method="search_method") + + def search_method(self, queryset, _, value: str): + """ + This method is used to search employees + """ + values = value.split(" ") + empty = queryset.model.objects.none() + for split in values: + empty = empty | ( + queryset.filter( + requested_employee_id__employee_first_name__icontains=split + ) + | queryset.filter( + requested_employee_id__employee_last_name__icontains=split + ) + ) + return empty.distinct() + + class Meta: + """ + Specifies the model and fields to be used for filtering AssetRequest instances. + + Attributes: + model (class): The model class AssetRequest to be filtered. + fields (str): A special value "__all__" to include all fields of the model in the filter. + """ + + model = AssetRequest + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for visible in self.form.visible_fields(): + visible.field.widget.attrs["id"] = str(uuid.uuid4()) + + +class AssetAllocationFilter(CustomFilterSet): + """ + Custom filter set for AssetAllocation instances. + """ + + search = django_filters.CharFilter(method="search_method") + + def search_method(self, queryset, _, value: str): + """ + This method is used to search employees + """ + values = value.split(" ") + empty = queryset.model.objects.none() + for split in values: + empty = empty | ( + queryset.filter( + assigned_to_employee_id__employee_first_name__icontains=split + ) + | queryset.filter( + assigned_to_employee_id__employee_last_name__icontains=split + ) + ) + return empty.distinct() + + class Meta: + """ + Specifies the model and fields to be used for filtering AssetAllocation instances. + + Attributes: + model (class): The model class AssetAssignment to be filtered. + fields (str): A special value "__all__" to include all fields + of the model in the filter. + """ + + model = AssetAssignment + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for visible in self.form.visible_fields(): + visible.field.widget.attrs["id"] = str(uuid.uuid4()) + + +class AssetCategoryFilter(CustomFilterSet): + """ + Custom filter set for AssetCategory instances. + """ + + search = django_filters.CharFilter(method="search_method") + + class Meta: + model = AssetCategory + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for visible in self.form.visible_fields(): + visible.field.widget.attrs["id"] = str(uuid.uuid4()) + + def search_method(self, queryset, name, value): + """ + Search method to filter by asset category name or related asset name. + """ + if not value: + return queryset # Return unfiltered queryset if no search term is provided + + return queryset.filter( + Q(asset_category_name__icontains=value) + | Q(asset__asset_name__icontains=value) + ).distinct() + + def filter_queryset(self, queryset): + """ + Filters queryset and applies AssetFilter if necessary. + """ + # Get the base filtered queryset + queryset = super().filter_queryset(queryset) + + # Filter by assets if asset data is present in the GET request + if self.data and "asset__pk" in self.data: + assets = AssetFilter(data=self.data).qs + queryset = queryset.filter( + asset__pk__in=assets.values_list("pk", flat=True) + ) + + return queryset.distinct() + + +class AssetRequestReGroup: + """ + Class to keep the field name for group by option + """ + + fields = [ + ("", "Select"), + ("requested_employee_id", "Employee"), + ("asset_category_id", "Asset Category"), + ("asset_request_date", "Request Date"), + ("asset_request_status", "Status"), + ] + + +class AssetAllocationReGroup: + """ + Class to keep the field name for group by option + """ + + fields = [ + ("", "Select"), + ("assigned_to_employee_id", "Employee"), + ("assigned_date", "Assigned Date"), + ("return_date", "Return Date"), + ] + + +class AssetHistoryFilter(CustomFilterSet): + """ + Custom filter set for AssetAssignment instances for filtering in asset history view. + """ + + search = django_filters.CharFilter( + field_name="asset_id__asset_name", lookup_expr="icontains" + ) + returned_assets = django_filters.CharFilter( + field_name="return_status", method="exclude_none" + ) + return_date_gte = django_filters.DateFilter( + field_name="return_date", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + return_date_lte = django_filters.DateFilter( + field_name="return_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + assigned_date_gte = django_filters.DateFilter( + field_name="assigned_date", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + assigned_date_lte = django_filters.DateFilter( + field_name="assigned_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + def exclude_none(self, queryset, name, value): + """ + Exclude objects with a null return_status from the queryset if value is "True" + """ + if value == "True": + queryset = queryset.filter(return_status__isnull=False) + return queryset + + class Meta: + """ + Specifies the model and fields to be used for filtering AssetAllocation instances. + + Attributes: + model (class): The model class AssetAssignment to be filtered. + fields (str): A special value "__all__" to include all fields + of the model in the filter. + """ + + model = AssetAssignment + fields = "__all__" + + +class AssetHistoryReGroup: + """ + Class to keep the field name for group by option + """ + + fields = [ + ("", "Select"), + ("asset_id", "Asset"), + ("assigned_to_employee_id", "Employee"), + ("assigned_date", "Assigned Date"), + ("return_date", "Return Date"), + ] diff --git a/asset/forms.py b/asset/forms.py new file mode 100644 index 0000000..ed3f913 --- /dev/null +++ b/asset/forms.py @@ -0,0 +1,384 @@ +""" +forms.py +Asset Management Forms + +This module contains Django ModelForms for handling various aspects of asset management, +including asset creation, allocation, return, category assignment, and batch handling. +""" + +import uuid +from datetime import date + +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from asset.models import ( + Asset, + AssetAssignment, + AssetCategory, + AssetDocuments, + AssetLot, + AssetReport, + AssetRequest, +) +from base.forms import ModelForm +from base.methods import reload_queryset +from employee.forms import MultipleFileField +from employee.models import Employee +from horilla.horilla_middlewares import _thread_locals + + +def set_date_field_initial(instance): + """this is used to update change the date value format""" + initial = {} + if instance.asset_purchase_date is not None: + initial["asset_purchase_date"] = instance.asset_purchase_date.strftime( + "%Y-%m-%d" + ) + + return initial + + +class AssetForm(ModelForm): + """ + A ModelForm for creating and updating asset information. + """ + + class Meta: + model = Asset + fields = "__all__" + exclude = ["is_active"] + widgets = { + "asset_lot_number_id": forms.Select( + attrs={"onchange": "batchNoChange($(this))"} + ), + } + + def __init__(self, *args, **kwargs): + request = getattr(_thread_locals, "request", None) + instance = kwargs.get("instance") + + if instance: + kwargs.setdefault("initial", set_date_field_initial(instance)) + + super().__init__(*args, **kwargs) + + uuid_map = { + field: str(uuid.uuid4()) + for field in ["asset_category_id", "asset_lot_number_id", "asset_status"] + } + for field, uuid_value in uuid_map.items(): + self.fields[field].widget.attrs["id"] = uuid_value + + if request and request.user.has_perm("asset.add_assetlot"): + batch_no_choices = list( + self.fields["asset_lot_number_id"].queryset.values_list( + "id", "lot_number" + ) + ) + batch_no_choices.insert(0, ("", _("---Choose Batch No.---"))) + + if not self.instance.pk: + batch_no_choices.append(("create", _("Create new batch number"))) + + self.fields["asset_lot_number_id"].choices = batch_no_choices + + def clean(self): + instance = self.instance + prev_instance = Asset.objects.filter(id=instance.pk).first() + if instance.pk: + if ( + self.cleaned_data.get("asset_status", None) + and self.cleaned_data.get("asset_status", None) + != prev_instance.asset_status + ): + if instance.assetassignment_set.filter( + return_status__isnull=True + ).exists(): + raise ValidationError( + {"asset_status": 'Asset in use you can"t change the status'} + ) + if ( + Asset.objects.filter(asset_tracking_id=self.data["asset_tracking_id"]) + .exclude(id=instance.pk) + .exists() + ): + raise ValidationError( + {"asset_tracking_id": "Already asset with this tracking id exists."} + ) + + +class DocumentForm(forms.ModelForm): + """ + Form for uploading documents related to an asset. + + Attributes: + - file: A FileField with a TextInput widget for file upload, allowing multiple files. + """ + + file = forms.FileField( + widget=forms.TextInput( + attrs={ + "name": "file", + "type": "File", + "class": "form-control", + "multiple": "True", + "accept": ".jpeg, .jpg, .png, .pdf", + } + ) + ) + + class Meta: + """ + Metadata options for the DocumentForm. + + Attributes: + - model: The model associated with this form (AssetDocuments). + - fields: Fields to include in the form ('file'). + - exclude: Fields to exclude from the form ('is_active'). + """ + + model = AssetDocuments + fields = [ + "file", + ] + exclude = ["is_active"] + + +class AssetReportForm(ModelForm): + """ + Form for creating and updating asset reports. + + Metadata: + - model: The model associated with this form (AssetReport). + - fields: Fields to include in the form ('title', 'asset_id'). + - exclude: Fields to exclude from the form ('is_active'). + + Methods: + - __init__: Initializes the form, disabling the 'asset_id' field. + """ + + class Meta: + """ + Metadata options for the AssetReportForm. + + Attributes: + - model: The model associated with this form (AssetReport). + - fields: Fields to include in the form ('title', 'asset_id'). + - exclude: Fields to exclude from the form ('is_active'). + """ + + model = AssetReport + fields = [ + "title", + "asset_id", + ] + exclude = ["is_active"] + + def __init__(self, *args, **kwargs): + """ + Initialize the AssetReportForm, disabling the 'asset_id' field. + + Args: + - *args: Variable length argument list. + - **kwargs: Arbitrary keyword arguments. + """ + super().__init__(*args, **kwargs) + self.fields["asset_id"].widget.attrs["disabled"] = "disabled" + + +class AssetCategoryForm(ModelForm): + """ + A form for creating and updating AssetCategory instances. + """ + + class Meta: + """ + Specifies the model and fields to be used for the AssetForm. + Attributes: + model (class): The model class AssetCategory to be used for the form. + fields (str): A special value "__all__" to include all fields + of the model in the form. + """ + + model = AssetCategory + fields = "__all__" + exclude = ["is_active"] + + +class AssetRequestForm(ModelForm): + """ + A Django ModelForm for creating and updating AssetRequest instances. + """ + + class Meta: + """ + Specifies the model and fields to be used for the AssetRequestForm. + Attributes: + model (class): The model class AssetRequest to be used for the form. + fields (str): A special value "__all__" to include all fields + of the model in the form. + widgets (dict): A dictionary containing widget configurations for + specific form fields. + """ + + model = AssetRequest + fields = "__all__" + exclude = ["is_active"] + + def __init__(self, *args, **kwargs): + user = kwargs.pop("user", None) + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + if user is not None and user.has_perm("asset.add_assetrequest"): + self.fields["requested_employee_id"].queryset = Employee.objects.all() + self.fields["requested_employee_id"].initial = Employee.objects.filter( + id=user.employee_get.id + ).first() + else: + self.fields["requested_employee_id"].queryset = Employee.objects.filter( + employee_user_id=user + ) + self.fields["requested_employee_id"].initial = user.employee_get + + self.fields["asset_category_id"].widget.attrs.update({"id": str(uuid.uuid4())}) + + +class AssetAllocationForm(ModelForm): + """ + A Django ModelForm for creating and updating AssetAssignment instances. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + reload_queryset(self.fields) + self.fields["asset_id"].queryset = Asset.objects.filter( + asset_status="Available" + ) + + self.fields["assign_images"] = MultipleFileField( + label=_("Assign Condition Images") + ) + self.fields["assign_images"].required = True + + class Meta: + """ + Specifies the model and fields to be used for the AssetAllocationForm. + Attributes: + model (class): The model class AssetAssignment to be used for the form. + fields (str): A special value "__all__" to include all fields + of the model in the form. + widgets (dict): A dictionary containing widget configurations for + specific form fields. + """ + + model = AssetAssignment + fields = "__all__" + exclude = [ + "return_date", + "return_condition", + "assigned_date", + "return_images", + "is_active", + ] + widgets = { + "asset_id": forms.Select(attrs={"class": "oh-select oh-select-2 "}), + "assigned_to_employee_id": forms.Select( + attrs={"class": "oh-select oh-select-2 "} + ), + "assigned_by_employee_id": forms.Select( + attrs={ + "class": "oh-select oh-select-2 ", + }, + ), + } + + # def clean(self): + # cleaned_data = super.clean() + + +class AssetReturnForm(ModelForm): + """ + A Django ModelForm for updating AssetAssignment instances during asset return. + """ + + class Meta: + """ + Specifies the model and fields to be used for the AssetReturnForm. + Attributes: + model (class): The model class AssetAssignment to be used for the form. + fields (list): The fields to include in the form, referring to + related AssetAssignment fields. + widgets (dict): A dictionary containing widget configurations for + specific form fields. + """ + + model = AssetAssignment + fields = ["return_date", "return_condition", "return_status", "return_images"] + widgets = { + "return_condition": forms.Textarea( + attrs={ + "class": "oh-input oh-input--textarea oh-input--block", + "rows": 3, + "cols": 40, + "placeholder": _( + "on returns the laptop. However, it has suffered minor damage." + ), + } + ), + "return_status": forms.Select( + attrs={"class": "oh-select oh-select-2", "required": "true"}, + ), + } + + def __init__(self, *args, **kwargs): + """ + Initializes the AssetReturnForm with initial values and custom field settings. + """ + super().__init__(*args, **kwargs) + self.fields["return_date"].widget.attrs.update({"required": "true"}) + self.fields["return_images"] = MultipleFileField( + label=_("Return Condition Images") + ) + self.fields["return_images"].required = True + + def clean_return_date(self): + """ + Validates the 'return_date' field. + + Ensures that the return date is not in the future. If the return date is in the future, + a ValidationError is raised. + + Returns: + - The cleaned return date. + + Raises: + - forms.ValidationError: If the return date is in the future. + """ + return_date = self.cleaned_data.get("return_date") + + if return_date and return_date > date.today(): + raise forms.ValidationError(_("Return date cannot be in the future.")) + + return return_date + + +class AssetBatchForm(ModelForm): + """ + A Django ModelForm for creating or updating AssetLot instances. + """ + + class Meta: + """ + Specifies the model and fields to be used for the AssetBatchForm. + Attributes: + model (class): The model class AssetLot to be used for the form. + fields (str): A special value "__all__" to include all fields + of the model in the form. + widgets (dict): A dictionary containing widget configurations for + specific form fields. + """ + + model = AssetLot + fields = "__all__" diff --git a/asset/models.py b/asset/models.py new file mode 100644 index 0000000..be03531 --- /dev/null +++ b/asset/models.py @@ -0,0 +1,331 @@ +""" +Models for Asset Management System + +This module defines Django models to manage assets, their categories, assigning, and requests +within an Asset Management System. +""" + +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from base.horilla_company_manager import HorillaCompanyManager +from base.models import Company +from employee.models import Employee +from horilla.models import HorillaModel, upload_path + + +class AssetCategory(HorillaModel): + """ + Represents a category for different types of assets. + """ + + asset_category_name = models.CharField( + max_length=255, unique=True, verbose_name=_("Name") + ) + asset_category_description = models.TextField( + max_length=255, verbose_name=_("Description") + ) + objects = models.Manager() + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + objects = HorillaCompanyManager("company_id") + + class Meta: + """ + Meta class to add additional options + """ + + verbose_name = _("Asset Category") + verbose_name_plural = _("Asset Categories") + + def __str__(self): + return f"{self.asset_category_name}" + + +class AssetLot(HorillaModel): + """ + Represents a lot associated with a collection of assets. + """ + + lot_number = models.CharField( + max_length=30, + null=False, + blank=False, + unique=True, + verbose_name=_("Batch Number"), + ) + lot_description = models.TextField( + null=True, blank=True, max_length=255, verbose_name=_("Description") + ) + company_id = models.ManyToManyField(Company, blank=True, verbose_name=_("Company")) + objects = HorillaCompanyManager() + + class Meta: + """ + Meta class to add additional options + """ + + ordering = ["-created_at"] + verbose_name = _("Asset Batch") + verbose_name_plural = _("Asset Batches") + + def __str__(self): + return f"{self.lot_number}" + + +class Asset(HorillaModel): + """ + Represents a asset with various attributes. + """ + + ASSET_STATUS = [ + ("In use", _("In Use")), + ("Available", _("Available")), + ("Not-Available", _("Not-Available")), + ] + asset_name = models.CharField(max_length=255, verbose_name=_("Asset Name")) + owner = models.ForeignKey( + Employee, + on_delete=models.PROTECT, + null=True, + blank=True, + verbose_name=_("Current User"), + ) + asset_description = models.TextField( + null=True, blank=True, max_length=255, verbose_name=_("Description") + ) + asset_tracking_id = models.CharField( + max_length=30, null=False, unique=True, verbose_name=_("Tracking Id") + ) + asset_purchase_date = models.DateField(verbose_name=_("Purchase Date")) + asset_purchase_cost = models.DecimalField( + max_digits=10, decimal_places=2, verbose_name=_("Cost") + ) + asset_category_id = models.ForeignKey( + AssetCategory, on_delete=models.PROTECT, verbose_name=_("Category") + ) + asset_status = models.CharField( + choices=ASSET_STATUS, + default="Available", + max_length=40, + verbose_name=_("Status"), + ) + asset_lot_number_id = models.ForeignKey( + AssetLot, + on_delete=models.PROTECT, + null=True, + blank=True, + verbose_name=_("Batch No"), + ) + expiry_date = models.DateField(null=True, blank=True, verbose_name=_("Expiry Date")) + notify_before = models.IntegerField( + default=1, null=True, verbose_name=_("Notify Before (days)") + ) + objects = HorillaCompanyManager("asset_category_id__company_id") + + class Meta: + ordering = ["-created_at"] + verbose_name = _("Asset") + verbose_name_plural = _("Assets") + + def __str__(self): + return f"{self.asset_name}-{self.asset_tracking_id}" + + def clean(self): + existing_asset = Asset.objects.filter( + asset_tracking_id=self.asset_tracking_id + ).exclude( + id=self.pk + ) # Exclude the current instance if updating + if existing_asset.exists(): + raise ValidationError( + { + "asset_description": _( + "An asset with this tracking ID already exists." + ) + } + ) + return super().clean() + + +class AssetReport(HorillaModel): + """ + Model representing a report for an asset. + + Attributes: + - title: A CharField for the title of the report (optional). + - asset_id: A ForeignKey to the Asset model, linking the report to a specific asset. + """ + + title = models.CharField(max_length=255, blank=True, null=True) + asset_id = models.ForeignKey( + Asset, related_name="asset_report", on_delete=models.CASCADE + ) + + def __str__(self): + """ + Returns a string representation of the AssetReport instance. + If a title is present, it returns "asset_id - title". + Otherwise, it returns "report for asset_id". + """ + return ( + f"{self.asset_id} - {self.title}" + if self.title + else f"report for {self.asset_id}" + ) + + +class AssetDocuments(HorillaModel): + """ + Model representing documents associated with an asset report. + + Attributes: + - asset_report: A ForeignKey to the AssetReport model, linking the document to + a specific asset report. + - file: A FileField for uploading the document file (optional). + """ + + asset_report = models.ForeignKey( + "AssetReport", related_name="documents", on_delete=models.CASCADE + ) + file = models.FileField(upload_to=upload_path, blank=True, null=True) + objects = models.Manager() + + class Meta: + verbose_name = _("Asset Document") + verbose_name_plural = _("Asset Documents") + + def __str__(self): + return f"document for {self.asset_report}" + + +class ReturnImages(HorillaModel): + """ + Model representing images associated with a returned asset. + + Attributes: + - image: A FileField for uploading the image file (optional). + """ + + image = models.FileField(upload_to=upload_path, blank=True, null=True) + + +class AssetAssignment(HorillaModel): + """ + Represents the allocation and return of assets to and from employees. + """ + + STATUS = [ + ("Minor damage", _("Minor damage")), + ("Major damage", _("Major damage")), + ("Healthy", _("Healthy")), + ] + asset_id = models.ForeignKey( + Asset, on_delete=models.PROTECT, verbose_name=_("Asset") + ) + assigned_to_employee_id = models.ForeignKey( + Employee, + on_delete=models.PROTECT, + related_name="allocated_employee", + verbose_name=_("Assigned To"), + ) + assigned_date = models.DateField(auto_now_add=True) + assigned_by_employee_id = models.ForeignKey( + Employee, + on_delete=models.PROTECT, + related_name="assigned_by", + verbose_name=_("Assigned By"), + ) + return_date = models.DateField(null=True, blank=True, verbose_name=_("Return Date")) + return_condition = models.TextField( + null=True, blank=True, max_length=255, verbose_name=_("Return Condition") + ) + return_status = models.CharField( + choices=STATUS, + max_length=30, + null=True, + blank=True, + verbose_name=_("Return Status"), + ) + return_request = models.BooleanField(default=False) + objects = HorillaCompanyManager("asset_id__asset_lot_number_id__company_id") + return_images = models.ManyToManyField( + ReturnImages, blank=True, related_name="return_images" + ) + assign_images = models.ManyToManyField( + ReturnImages, + blank=True, + related_name="assign_images", + verbose_name=_("Assign Condition Images"), + ) + objects = HorillaCompanyManager( + "assigned_to_employee_id__employee_work_info__company_id" + ) + + class Meta: + """Meta class for AssetAssignment model""" + + ordering = ["-id"] + verbose_name = _("Asset Allocation") + verbose_name_plural = _("Asset Allocations") + + def __str__(self): + return f"{self.assigned_to_employee_id} --- {self.asset_id} --- {self.return_status}" + + +class AssetRequest(HorillaModel): + """ + Represents a request for assets made by employees. + """ + + STATUS = [ + ("Requested", _("Requested")), + ("Approved", _("Approved")), + ("Rejected", _("Rejected")), + ] + requested_employee_id = models.ForeignKey( + Employee, + on_delete=models.PROTECT, + related_name="requested_employee", + null=False, + blank=False, + verbose_name=_("Requesting User"), + ) + asset_category_id = models.ForeignKey( + AssetCategory, on_delete=models.PROTECT, verbose_name=_("Asset Category") + ) + asset_request_date = models.DateField(auto_now_add=True) + description = models.TextField( + null=True, blank=True, max_length=255, verbose_name=_("Description") + ) + asset_request_status = models.CharField( + max_length=30, choices=STATUS, default="Requested", null=True, blank=True + ) + objects = HorillaCompanyManager( + "requested_employee_id__employee_work_info__company_id" + ) + + class Meta: + """Meta class for AssetRequest model""" + + ordering = ["-id"] + verbose_name = _("Asset Request") + verbose_name_plural = _("Asset Requests") + + def status_html_class(self): + COLOR_CLASS = { + "Approved": "oh-dot--success", + "Requested": "oh-dot--info", + "Rejected": "oh-dot--danger", + } + + LINK_CLASS = { + "Approved": "link-success", + "Requested": "link-info", + "Rejected": "link-danger", + } + status = self.asset_request_status + return { + "color": COLOR_CLASS.get(status), + "link": LINK_CLASS.get(status), + } diff --git a/asset/resources.py b/asset/resources.py new file mode 100644 index 0000000..4464e9e --- /dev/null +++ b/asset/resources.py @@ -0,0 +1,21 @@ +""" +Module: resources.py +This module defines classes for handling resources related to assets. +""" + +from import_export import resources + +from .models import Asset + + +class AssetResource(resources.ModelResource): + """ + This class is used to import and export Asset data using the import_export library. + """ + + class Meta: + """ + Specifies the model to be used for import and export. + """ + + model = Asset diff --git a/asset/scheduler.py b/asset/scheduler.py new file mode 100644 index 0000000..abaf63d --- /dev/null +++ b/asset/scheduler.py @@ -0,0 +1,102 @@ +""" +scheduler.py + +This module is used to register scheduled tasks +""" + +import sys +from datetime import date, timedelta + +from apscheduler.schedulers.background import BackgroundScheduler +from django.urls import reverse + +from notifications.signals import notify + + +def notify_expiring_assets(): + """ + Finds all Expiring Assets and send a notification on the notify_before date. + """ + from django.contrib.auth.models import User + + from asset.models import Asset + + today = date.today() + assets = Asset.objects.all() + + # Cache bot & superuser once + bot = User.objects.filter(username="Horilla Bot").only("id").first() + superuser = User.objects.filter(is_superuser=True).only("id").first() + + # Query only assets that are expiring today + assets = Asset.objects.filter( + expiry_date__isnull=False, + expiry_date__gte=today, + ) + + for asset in assets: + if asset.expiry_date: + expiry_date = asset.expiry_date + notify_date = expiry_date - timedelta(days=asset.notify_before) + recipient = getattr(asset.owner, "employee_user_id", None) or superuser + if notify_date == today and recipient: + notify.send( + bot, + recipient=recipient, + verb=f"The Asset '{asset.asset_name}' expires in {asset.notify_before} days", + verb_ar=f"تنتهي صلاحية الأصل '{asset.asset_name}' خلال {asset.notify_before} من الأيام", + verb_de=f"Das Asset {asset.asset_name} läuft in {asset.notify_before} Tagen ab.", + verb_es=f"El activo {asset.asset_name} caduca en {asset.notify_before} días.", + verb_fr=f"L'actif {asset.asset_name} expire dans {asset.notify_before} jours.", + redirect=reverse("asset-category-view"), + label="System", + icon="information", + ) + + +def notify_expiring_documents(): + """ + Finds all Expiring Documents and send a notification on the notify_before date. + """ + from django.contrib.auth.models import User + + from horilla_documents.models import Document + + today = date.today() + documents = Document.objects.all() + bot = User.objects.filter(username="Horilla Bot").first() + for document in documents: + if document.expiry_date: + expiry_date = document.expiry_date + notify_date = expiry_date - timedelta(days=document.notify_before) + + if notify_date == today: + notify.send( + bot, + recipient=document.employee_id.employee_user_id, + verb=f"The document ' {document.title} ' expires in {document.notify_before}\ + days", + verb_ar=f"تنتهي صلاحية المستند '{document.title}' خلال {document.notify_before}\ + يوم", + verb_de=f"Das Dokument '{document.title}' läuft in {document.notify_before}\ + Tagen ab.", + verb_es=f"El documento '{document.title}' caduca en {document.notify_before}\ + días", + verb_fr=f"Le document '{document.title}' expire dans {document.notify_before}\ + jours", + redirect=reverse("asset-category-view"), + label="System", + icon="information", + ) + if today >= expiry_date: + document.is_active = False + + +if not any( + cmd in sys.argv + for cmd in ["makemigrations", "migrate", "compilemessages", "flush", "shell"] +): + scheduler = BackgroundScheduler() + scheduler.add_job(notify_expiring_assets, "interval", days=1) + scheduler.add_job(notify_expiring_documents, "interval", hours=4) + scheduler.start() diff --git a/asset/sidebar.py b/asset/sidebar.py new file mode 100644 index 0000000..3d4016f --- /dev/null +++ b/asset/sidebar.py @@ -0,0 +1,59 @@ +""" +assets/sidebar.py +""" + +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +MENU = _("Assets") +IMG_SRC = "images/ui/assets.svg" + +SUBMENUS = [ + { + "menu": _("Dashboard"), + "redirect": reverse("asset-dashboard"), + "accessibility": "asset.sidebar.dashboard_accessibility", + }, + { + "menu": _("Asset View"), + "redirect": reverse("asset-category-view"), + "accessibility": "asset.sidebar.dashboard_accessibility", + }, + { + "menu": _("Asset Batches"), + "redirect": reverse("asset-batch-view"), + "accessibility": "asset.sidebar.lot_accessibility", + }, + { + "menu": _("Request and Allocation"), + "redirect": reverse("asset-request-allocation-view"), + }, + { + "menu": _("Asset History"), + "redirect": reverse("asset-history"), + "accessibility": "asset.sidebar.history_accessibility", + }, +] + + +def dashboard_accessibility(request, submenu, user_perms, *args, **kwargs): + """ + Determine if the user has the necessary permissions to access the + dashboard and asset category view. + """ + return request.user.has_perm("asset.view_assetcategory") + + +def history_accessibility(request, submenu, user_perms, *args, **kwargs): + """ + Determine if the user has the necessary permissions to access the + dashboard and asset category view. + """ + return request.user.has_perm("asset.view_assetassignment") + + +def lot_accessibility(request, subment, user_perms, *args, **kwargs): + """ + Asset batch sidebar accessibility method + """ + return request.user.has_perm("asset.view_assetlot") diff --git a/asset/tests.py b/asset/tests.py new file mode 100644 index 0000000..b0ff325 --- /dev/null +++ b/asset/tests.py @@ -0,0 +1,5 @@ +""" +This module contains test cases for the assets application. +""" + +from django.test import TestCase diff --git a/asset/urls.py b/asset/urls.py new file mode 100644 index 0000000..1ad955c --- /dev/null +++ b/asset/urls.py @@ -0,0 +1,207 @@ +""" +URL configuration for asset-related views. +""" + +from django import views +from django.urls import path + +from asset.forms import AssetCategoryForm, AssetForm +from asset.models import Asset, AssetCategory +from base.views import object_duplicate + +from . import views + +urlpatterns = [ + path( + "asset-creation//", + views.asset_creation, + name="asset-creation", + ), + path("asset-list/", views.asset_list, name="asset-list"), + path("asset-update//", views.asset_update, name="asset-update"), + path( + "duplicate-asset//", + object_duplicate, + name="duplicate-asset", + kwargs={ + "model": Asset, + "form": AssetForm, + "form_name": "asset_creation_form", + "template": "asset/asset_creation.html", + }, + ), + path("asset-delete//", views.asset_delete, name="asset-delete"), + path( + "asset-information//", + views.asset_information, + name="asset-information", + ), + path("asset-category-view/", views.asset_category_view, name="asset-category-view"), + path( + "asset-category-view-search-filter", + views.asset_category_view_search_filter, + name="asset-category-view-search-filter", + ), + path( + "asset-category-duplicate//", + object_duplicate, + name="asset-category-duplicate", + kwargs={ + "model": AssetCategory, + "form": AssetCategoryForm, + "form_name": "form", + "template": "category/asset_category_form.html", + }, + ), + path( + "asset-category-creation", + views.asset_category_creation, + name="asset-category-creation", + ), + path( + "asset-category-update/", + views.asset_category_update, + name="asset-category-update", + ), + path( + "asset-category-delete/", + views.delete_asset_category, + name="asset-category-delete", + ), + path( + "asset-request-creation", + views.asset_request_creation, + name="asset-request-creation", + ), + path( + "asset-request-allocation-view/", + views.asset_request_allocation_view, + name="asset-request-allocation-view", + ), + path( + "asset-request-individual-view/", + views.asset_request_individual_view, + name="asset-request-individual-view", + ), + path( + "own-asset-individual-view/", + views.own_asset_individual_view, + name="own-asset-individual-view", + ), + path( + "asset-allocation-individual-view/", + views.asset_allocation_individual_view, + name="asset-allocation-individual-view", + ), + path( + "asset-request-allocation-view-search-filter", + views.asset_request_alloaction_view_search_filter, + name="asset-request-allocation-view-search-filter", + ), + path( + "asset-request-approve//", + views.asset_request_approve, + name="asset-request-approve", + ), + path( + "asset-request-reject//", + views.asset_request_reject, + name="asset-request-reject", + ), + path( + "asset-allocate-creation", + views.asset_allocate_creation, + name="asset-allocate-creation", + ), + path( + "asset-allocate-return//", + views.asset_allocate_return, + name="asset-allocate-return", + ), + path( + "asset-allocate-return-request//", + views.asset_allocate_return_request, + name="asset-allocate-return-request", + ), + path("asset-excel", views.asset_excel, name="asset-excel"), + path("asset-import", views.asset_import, name="asset-import"), + path("asset-export-excel", views.asset_export_excel, name="asset-export-excel"), + path( + "asset-batch-number-creation", + views.asset_batch_number_creation, + name="asset-batch-number-creation", + ), + path("asset-batch-view", views.asset_batch_view, name="asset-batch-view"), + path( + "asset-batch-number-search", + views.asset_batch_number_search, + name="asset-batch-number-search", + ), + path( + "asset-batch-update/", + views.asset_batch_update, + name="asset-batch-update", + ), + path( + "asset-batch-number-delete/", + views.asset_batch_number_delete, + name="asset-batch-number-delete", + ), + path("asset-count-update", views.asset_count_update, name="asset-count-update"), + path("add-asset-report/", views.add_asset_report, name="add-asset-report"), + path( + "add-asset-report/", + views.add_asset_report, + name="add-asset-report", + ), + path("dashboard/", views.asset_dashboard, name="asset-dashboard"), + path( + "asset-dashboard-requests/", + views.asset_dashboard_requests, + name="asset-dashboard-requests", + ), + path( + "asset-dashboard-allocates/", + views.asset_dashboard_allocates, + name="asset-dashboard-allocates", + ), + path( + "asset-available-chart/", + views.asset_available_chart, + name="asset-available-chart", + ), + path( + "asset-category-chart/", views.asset_category_chart, name="asset-category-chart" + ), + path( + "asset-history", + views.asset_history, + name="asset-history", + ), + path( + "asset-history-single-view/", + views.asset_history_single_view, + name="asset-history-single-view", + ), + path( + "asset-history-search", + views.asset_history_search, + name="asset-history-search", + ), + path("asset-tab/", views.asset_tab, name="asset-tab"), + path( + "profile-asset-tab/", + views.profile_asset_tab, + name="profile-asset-tab", + ), + path( + "asset-request-tab/", + views.asset_request_tab, + name="asset-request-tab", + ), + path( + "main-dashboard-asset-requests", + views.asset_dashboard_requests, + name="main-dashboard-asset-requests", + ), +] diff --git a/asset/views.py b/asset/views.py new file mode 100644 index 0000000..77d2d04 --- /dev/null +++ b/asset/views.py @@ -0,0 +1,1878 @@ +""" " +asset.py + +This module is used to""" + +import csv +import json +import os +from datetime import date, datetime +from urllib.parse import parse_qs + +import pandas as pd +from django.contrib import messages +from django.core.files.base import ContentFile +from django.core.files.storage import FileSystemStorage +from django.core.paginator import Paginator +from django.db.models import ProtectedError +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from asset.filters import ( + AssetAllocationFilter, + AssetAllocationReGroup, + AssetCategoryFilter, + AssetExportFilter, + AssetFilter, + AssetHistoryFilter, + AssetHistoryReGroup, + AssetRequestFilter, + AssetRequestReGroup, + CustomAssetFilter, +) +from asset.forms import ( + AssetAllocationForm, + AssetBatchForm, + AssetCategoryForm, + AssetForm, + AssetReportForm, + AssetRequestForm, + AssetReturnForm, +) +from asset.models import ( + Asset, + AssetAssignment, + AssetCategory, + AssetDocuments, + AssetLot, + AssetRequest, + ReturnImages, +) +from base.methods import ( + closest_numbers, + eval_validate, + filtersubordinates, + get_key_instances, + get_pagination, + paginator_qry, + sortby, +) +from base.models import Company +from employee.models import Employee, EmployeeWorkInformation +from horilla import settings +from horilla.decorators import ( + hx_request_required, + login_required, + manager_can_enter, + owner_can_enter, + permission_required, +) +from horilla.group_by import group_by_queryset +from horilla.horilla_settings import HORILLA_DATE_FORMATS +from horilla.methods import horilla_users_with_perms +from notifications.signals import notify + + +def asset_del(request, asset): + """ + Handle the deletion of an asset and provide message to the user. + """ + try: + asset.delete() + messages.success(request, _("Asset deleted successfully")) + except ProtectedError: + messages.error(request, _("You cannot delete this asset.")) + + +@login_required +@hx_request_required +@permission_required("asset.add_asset") +def asset_creation(request, asset_category_id): + """ + View function for creating a new asset object. + Args: + request (HttpRequest): A Django HttpRequest object that contains information + about the current request. + asset_category_id (int): An integer representing the ID of the asset category for which + the asset is being created. + + Returns: + If the request method is 'POST' and the form is valid, the function saves the + new asset object to the database + and redirects to the asset creation page with a success message. + If the form is not valid, the function returns the asset creation page with the + form containing the invalid data. + If the request method is not 'POST', the function renders the asset creation + page with the form initialized with + the ID of the asset category for which the asset is being created. + Raises: + None + """ + asset_category = AssetCategory.find(asset_category_id) + if not asset_category: + messages.error(request, _("Asset category not found")) + return HttpResponse(status=204, headers={"HX-Refresh": "true"}) + + initial_data = {"asset_category_id": asset_category_id} + # Use request.GET to pre-fill the form with dynamic create batch number data if available + form = ( + AssetForm(initial={**initial_data, **request.GET.dict()}) + if request.GET.get("csrfmiddlewaretoken") + else AssetForm(initial=initial_data) + ) + if request.method == "POST": + form = AssetForm(request.POST, initial=initial_data) + if form.is_valid(): + form.save() + messages.success(request, _("Asset created successfully")) + return redirect("asset-creation", asset_category_id=asset_category_id) + context = {"asset_creation_form": form} + return render(request, "asset/asset_creation.html", context) + + +@login_required +def add_asset_report(request, asset_id=None): + """ + Function for adding asset report to the asset + """ + asset_report_form = AssetReportForm() + if asset_id: + asset = Asset.objects.get(id=asset_id) + asset_report_form = AssetReportForm(initial={"asset_id": asset}) + if not request.GET.get("asset_list"): + if request.user.employee_get == AssetAssignment.objects.get( + asset_id=asset_id, return_date__isnull=True + ).assigned_to_employee_id or request.user.has_perm("asset.change_asset"): + pass + else: + return redirect(asset_request_allocation_view) + + if request.method == "POST": + asset_report_form = AssetReportForm( + request.POST, request.FILES, initial={"asset_id": asset_id} + ) + + if asset_report_form.is_valid(): + asset_report = asset_report_form.save() + messages.success(request, _("Report added successfully.")) + + if asset_report_form.is_valid() and request.FILES: + for file in request.FILES.getlist("file"): + AssetDocuments.objects.create(asset_report=asset_report, file=file) + + return render( + request, + "asset/asset_report_form.html", + {"asset_report_form": asset_report_form, "asset_id": asset_id}, + ) + + +@login_required +@hx_request_required +@permission_required("asset.change_asset") +def asset_update(request, asset_id): + """ + Updates an asset with the given ID. + If the request method is GET, it displays the form to update the asset. If the + request method is POST and the form is valid, it updates the asset and + redirects to the asset list view for the asset's category. + Args: + - request: the HTTP request object + - id (int): the ID of the asset to be updated + Returns: + - If the request method is GET, the rendered 'asset_update.html' template + with the form to update the asset. + - If the request method is POST and the form is valid, a redirect to the asset + list view for the asset's category. + """ + + if request.method == "GET": + # modal form get + asset_under = request.GET.get("asset_under") + elif request.method == "POST": + # modal form post + asset_under = request.POST.get("asset_under") + + if not asset_under: + # if asset there is no asset_under data that means the request is form the category list + asset_under = "asset_category" + instance = Asset.objects.get(id=asset_id) + asset_form = AssetForm(instance=instance) + previous_data = request.GET.urlencode() + + if request.method == "POST": + asset_form = AssetForm(request.POST, instance=instance) + if asset_form.is_valid(): + asset_form.save() + messages.success(request, _("Asset Updated")) + context = { + "instance": instance, + "asset_form": asset_form, + "asset_under": asset_under, + "pg": previous_data, + "asset_cat_id": instance.asset_category_id.id, + } + requests_ids_json = request.GET.get("requests_ids") + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + request_copy = request.GET.copy() + request_copy.pop("requests_ids", None) + previous_data = request_copy.urlencode() + context["requests_ids"] = requests_ids + context["pd"] = previous_data + return render(request, "asset/asset_update.html", context=context) + + +@login_required +@hx_request_required +def asset_information(request, asset_id): + """ + Display information about a specific Asset object. + Args: + request: the HTTP request object + asset_id (int): the ID of the Asset object to retrieve + Returns: + A rendered HTML template displaying the information about the requested Asset object. + """ + + asset = Asset.objects.get(id=asset_id) + context = {"asset": asset} + requests_ids_json = request.GET.get("requests_ids") + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + previous_id, next_id = closest_numbers(requests_ids, asset_id) + context["requests_ids"] = requests_ids_json + context["previous"] = previous_id + context["next"] = next_id + return render(request, "asset/asset_information.html", context) + + +@login_required +@permission_required(perm="asset.delete_asset") +def asset_delete(request, asset_id): + """Delete the asset with the given id. + If the asset is currently in use, display an info message and + redirect to the asset list. + Otherwise, delete the asset and display a success message. + Args: + request: HttpRequest object representing the current request. + asset_id: int representing the id of the asset to be deleted. + Returns: + If the asset is currently in use or the asset list filter is + applied, render the asset list template + with the corresponding context. + Otherwise, redirect to the asset list view for the asset + category of the deleted asset. + """ + + request_copy = request.GET.copy() + request_copy.pop("requests_ids", None) + previous_data = request_copy.urlencode() + try: + asset = Asset.objects.get(id=asset_id) + except Asset.DoesNotExist: + messages.error(request, _("Asset not found")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + asset_cat_id = asset.asset_category_id.id + status = asset.asset_status + asset_list_filter = request.GET.get("asset_list") + asset_allocation = AssetAssignment.objects.filter(asset_id=asset).first() + if asset_list_filter: + # if the asset deleted is from the filtered list of asset + asset_under = "asset_filter" + assets = Asset.objects.all() + previous_data = request.GET.urlencode() + asset_filtered = AssetFilter(request.GET, queryset=assets) + asset_list = asset_filtered.qs + paginator = Paginator(asset_list, get_pagination()) + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) + context = { + "assets": page_obj, + "pg": previous_data, + "asset_category_id": asset.asset_category_id.id, + "asset_under": asset_under, + } + if status == "In use": + messages.info(request, _("Asset is in use")) + elif asset_allocation: + messages.error(request, _("Asset is used in allocation!.")) + else: + asset_del(request, asset) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + instances_ids = request.GET.get("requests_ids", "[]") + instances_list = eval_validate(instances_ids) + if status == "In use": + messages.info(request, _("Asset is in use")) + return redirect( + f"/asset/asset-information/{asset.id}/?{previous_data}&requests_ids={instances_list}&asset_info=true" + ) + elif asset_allocation: + messages.error(request, _("Asset is used in allocation!.")) + return redirect( + f"/asset/asset-information/{asset.id}/?{previous_data}&requests_ids={instances_list}&asset_info=true" + ) + else: + asset_del(request, asset) + if len(eval_validate(instances_ids)) <= 1: + return HttpResponse("") + + if Asset.find(asset.id): + return redirect( + f"/asset/asset-information/{asset.id}/?{previous_data}&requests_ids={instances_list}&asset_info=true" + ) + else: + instances_ids = request.GET.get("requests_ids") + instances_list = json.loads(instances_ids) + if asset_id in instances_list: + instances_list.remove(asset_id) + previous_instance, next_instance = closest_numbers( + json.loads(instances_ids), asset_id + ) + return redirect( + f"/asset/asset-information/{next_instance}/?{previous_data}&requests_ids={instances_list}&asset_info=true" + ) + + +@login_required +@hx_request_required +def asset_list(request, cat_id): + """ + View function is used as asset list inside a category and also in + filter asset list + Args: + request (HttpRequest): A Django HttpRequest object that contains + information about the current request. + cat_id (int): An integer representing the id of the asset category + to list assets for. + Returns: + A rendered HTML template that displays a paginated list of assets in the given + asset category. + Raises: + None + """ + context = {} + asset_under = "" + asset_filtered = AssetFilter(request.GET) + asset_list = asset_filtered.qs.filter(asset_category_id=cat_id) + + paginator = Paginator(asset_list, get_pagination()) + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) + + requests_ids = json.dumps([instance.id for instance in page_obj.object_list]) + previous_data = request.GET.urlencode() + data_dict = parse_qs(previous_data) + get_key_instances(Asset, data_dict) + context = { + "assets": page_obj, + "pg": previous_data, + "asset_category_id": cat_id, + "asset_under": asset_under, + "asset_count": len(asset_list) or None, + "filter_dict": data_dict, + "requests_ids": requests_ids, + } + return render(request, "asset/asset_list.html", context) + + +@login_required +@hx_request_required +@permission_required(perm="asset.add_assetcategory") +def asset_category_creation(request): + """ + Allow a user to create a new AssetCategory object using a form. + Args: + request: the HTTP request object + Returns: + A rendered HTML template displaying the AssetCategory creation form. + """ + form = AssetCategoryForm() + + if request.method == "POST": + form = AssetCategoryForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Asset category created successfully")) + form = AssetCategoryForm() + if AssetCategory.objects.filter().count() == 1: + if AssetCategory.objects.count() == 1: + return HttpResponse(status=204, headers={"HX-Refresh": "true"}) + context = {"form": form} + return render(request, "category/asset_category_form.html", context) + + +@login_required +@hx_request_required +@permission_required(perm="asset.change_assetcategory") +def asset_category_update(request, cat_id): + """ + This view is used to update an existing asset category. + Args: + request: HttpRequest object. + id: int value representing the id of the asset category to update. + Returns: + Rendered HTML template. + """ + + previous_data = request.GET.urlencode() + asset_category = AssetCategory.find(cat_id) + if not asset_category: + messages.error(request, _("Asset category not found")) + return HttpResponse(status=204, headers={"HX-Refresh": "true"}) + + form = AssetCategoryForm(instance=asset_category) + context = {"form": form, "pg": previous_data} + if request.method == "POST": + form = AssetCategoryForm(request.POST, instance=asset_category) + if form.is_valid(): + form.save() + messages.success(request, _("Asset category updated successfully")) + else: + context["form"] = form + + return render(request, "category/asset_category_form.html", context) + + +@login_required +@permission_required(perm="asset.delete_assetcategory") +def delete_asset_category(request, cat_id): + """ + This method is used to delete asset category + """ + previous_data = request.GET.urlencode() + + asset_category = AssetCategory.find(cat_id) + if not asset_category: + messages.error(request, _("Asset category not found")) + return redirect(f"/asset/asset-category-view-search-filter?{previous_data}") + + try: + asset_category.delete() + messages.success(request, _("Asset category deleted.")) + except Exception: + messages.error(request, _("Assets are located within this category.")) + + if not AssetCategory.objects.exists(): + return HttpResponse(status=204, headers={"HX-Refresh": "true"}) + + return redirect(f"/asset/asset-category-view-search-filter?{previous_data}") + + +def filter_pagination_asset_category(request): + """ + This view is used for pagination and filtering asset categories + """ + search = request.GET.get("search", "") + + previous_data = request.GET.urlencode() + + asset_category_queryset = AssetCategory.objects.all() + + if request.GET: + asset_category_filtered = AssetCategoryFilter( + request.GET, queryset=asset_category_queryset + ) + asset_category_queryset = ( + asset_category_filtered.qs + ) # Filter the queryset based on the GET params + asset_category_filtered_form = asset_category_filtered.form # Show filter form + else: + asset_category_filtered_form = None + + # Pagination + asset_category_paginator = Paginator(asset_category_queryset, get_pagination()) + page_number = request.GET.get("page") + asset_categories = asset_category_paginator.get_page(page_number) + + data_dict = parse_qs(previous_data) + get_key_instances(Asset, data_dict) # 882 + + asset_creation_form = AssetForm() + if data_dict.get("type"): + del data_dict["type"] + asset_category_form = AssetCategoryForm() + asset_filter_form = AssetFilter() + return { + "asset_creation_form": asset_creation_form, + "asset_category_form": asset_category_form, + "asset_categories": asset_categories, + "asset_category_filter_form": asset_category_filtered_form, + "asset_filter_form": asset_filter_form.form, + "pg": previous_data, + "filter_dict": data_dict, + "dashboard": request.GET.get("dashboard"), + "model": AssetCategory, + } + + +@login_required +@permission_required(perm="asset.view_assetcategory") +def asset_category_view(request): + """ + View function for rendering a paginated list of asset categories. + Args: + request (HttpRequest): A Django HttpRequest object that contains information + about the current request. + Returns: + A rendered HTML template that displays a paginated list of asset categories. + Raises: + None + """ + + queryset = AssetCategory.objects.all() + if queryset.exists(): + template = "category/asset_category_view.html" + else: + template = "category/asset_empty.html" + context = filter_pagination_asset_category(request) + return render(request, template, context) + + +@login_required +@permission_required(perm="asset.view_assetcategory") +def asset_category_view_search_filter(request): + """ + View function for rendering a paginated list of asset categories with search and filter options. + Args: + request (HttpRequest): A Django HttpRequest object that contains information + about the current request. + Returns: + A rendered HTML template that displays a paginated list of asset + categories with search and filter options. + Raises: + None + """ + context = filter_pagination_asset_category(request) + return render(request, "category/asset_category.html", context) + + +def request_creation_hx_returns(referer, user): + """ + Determines the hx_url and hx_target based on the referer path + for asset request creation + """ + referer = "/" + "/".join(referer.split("/")[3:]) + # Map referer paths to corresponding URLs and targets + hx_map = { + "/": ("asset-dashboard-requests", "dashboardAssetRequests"), + "/asset/dashboard/": ("asset-dashboard-requests", "dashboardAssetRequests"), + "/asset/asset-request-allocation-view/": ( + "asset-request-allocation-view-search-filter", + "asset_request_allocation_list", + ), + "/employee/employee-profile/": ( + "profile-asset-tab", + "asset_target", + ), + } + + hx_url, hx_target = hx_map.get( + referer, (None, None) + ) # Default to None if not in map + + if hx_url == "profile-asset-tab": + hx_url = reverse(hx_url, kwargs={"emp_id": user.employee_get.id}) + else: + hx_url = reverse(hx_url) if hx_url else None + + return hx_url, hx_target + + +@login_required +@hx_request_required +def asset_request_creation(request): + """ + Creates a new AssetRequest object and saves it to the database. + Renders the asset_request_creation.html template if the request method is GET. + If the request method is POST and the form data is valid, the new + AssetRequest is saved to the database and + the user is redirected to the asset_request_view_search_filter view. + If the form data is invalid, or if the request method is POST but the + form data is not present, the user is + presented with the asset_request_creation.html template with error + messages displayed. + """ + # intitial = {'requested_employee_id':request.user.employee_get} + + referer = request.META.get("HTTP_REFERER", "/") + hx_url, hx_target = request_creation_hx_returns(referer, request.user) + form = AssetRequestForm(user=request.user) + context = {"asset_request_form": form, "hx_url": hx_url, "hx_target": hx_target} + if request.method == "POST": + form = AssetRequestForm(request.POST, user=request.user) + if form.is_valid(): + form.save() + messages.success(request, _("Asset request created!")) + context["asset_request_form"] = form + + return render(request, "request_allocation/asset_request_creation.html", context) + + +@login_required +@hx_request_required +@permission_required(perm="asset.add_assetassignment") +def asset_request_approve(request, req_id): + """ + Approves an asset request with the given ID and updates the corresponding asset record + to mark it as allocated. + """ + asset_request = AssetRequest.find(req_id) + homepage_url = request.build_absolute_uri("/") + error_response = ( + f"" + ) + if not asset_request: + messages.error(request, _("Asset request does not exist.")) + return HttpResponse(error_response) + + assets = asset_request.asset_category_id.asset_set.filter(asset_status="Available") + if request.method == "POST": + post_data = request.POST.copy() + post_data["assigned_to_employee_id"] = asset_request.requested_employee_id + post_data["assigned_by_employee_id"] = request.user.employee_get + + form = AssetAllocationForm(post_data, request.FILES) + if form.is_valid(): + try: + asset = form.cleaned_data["asset_id"] + asset.asset_status = "In use" + asset.save() + + allocation = form.save(commit=False) + allocation.assigned_by_employee_id = request.user.employee_get + allocation.save() + + asset_request.asset_request_status = "Approved" + asset_request.save() + + notify.send( + request.user.employee_get, + recipient=allocation.assigned_to_employee_id.employee_user_id, + verb=_("Your asset request has been approved!"), + redirect=reverse("asset-request-allocation-view") + + f"?asset_request_date={asset_request.asset_request_date}&" + f"asset_request_status={asset_request.asset_request_status}", + icon="bag-check", + ) + + messages.success(request, _("Asset request approved successfully!")) + return HttpResponse("") + except Exception as e: + messages.error(request, _("An error occurred: ") + str(e)) + return HttpResponse(error_response) + else: + form = AssetAllocationForm() + form.fields["asset_id"].queryset = assets + + context = {"asset_allocation_form": form, "id": req_id} + return render(request, "request_allocation/asset_approve.html", context) + + +def reject_request_return(request, asset_request, req_id): + if not request.META.get("HTTP_HX_REQUEST"): + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + hx_target = request.META.get("HTTP_HX_TARGET") + if hx_target == "objectDetailsModalW25Target": + try: + requests_ids = json.loads(request.GET.get("requests_ids", "[]")) + except json.JSONDecodeError: + requests_ids = [] + return redirect( + reverse( + "asset-request-individual-view", kwargs={"asset_request_id": req_id} + ) + + f"?requests_ids={requests_ids}" + ) + + referrer = request.META.get("HTTP_REFERER", "") + referrer = "/" + "/".join(referrer.split("/")[3:]) + if referrer.startswith("/employee/employee-view/"): + return redirect( + f"/asset/asset-request-tab/{asset_request.requested_employee_id.id}" + ) + + if referrer.endswith("/asset/dashboard/") or referrer == "/": + return redirect(reverse("asset-dashboard-requests")) + + return redirect( + f"{reverse('asset-request-allocation-view-search-filter')}?{request.GET.urlencode()}" + ) + + +@login_required +@permission_required(perm="asset.add_assetassignment") +def asset_request_reject(request, req_id): + """ + View function to reject an asset request. + Parameters: + request (HttpRequest): the request object sent by the client + req_id (int): the id of the AssetRequest object to reject + + Returns: + HttpResponse: a redirect to the asset request list view with a success + message if the asset request is rejected successfully, or a redirect to the + asset request detail view with an error message if the asset request is not + found or already rejected + """ + asset_request = AssetRequest.objects.get(id=req_id) + asset_request.asset_request_status = "Rejected" + asset_request.save() + messages.info(request, _("Asset request has been rejected.")) + notify.send( + request.user.employee_get, + recipient=asset_request.requested_employee_id.employee_user_id, + verb="Your asset request rejected!.", + verb_ar="تم رفض طلب الأصول الخاص بك!", + verb_de="Ihr Antragsantrag wurde abgelehnt!", + verb_es="¡Se ha rechazado su solicitud de activo!", + verb_fr="Votre demande d'actif a été rejetée !", + redirect=reverse("asset-request-allocation-view") + + f"?asset_request_date={asset_request.asset_request_date}\ + &asset_request_status={asset_request.asset_request_status}", + icon="bag-check", + ) + return reject_request_return(request, asset_request, req_id) + + +@login_required +@permission_required(perm="asset.add_assetassignment") +def asset_allocate_creation(request): + """ + View function to create asset allocation. + Returns: + - to allocated view. + """ + + form = AssetAllocationForm( + initial={"assigned_by_employee_id": request.user.employee_get} + ) + context = {"asset_allocation_form": form} + if request.method == "POST": + form = AssetAllocationForm(request.POST) + if form.is_valid(): + asset = form.instance.asset_id.id + asset = Asset.objects.filter(id=asset).first() + asset.asset_status = "In use" + asset.save() + instance = form.save() + files = request.FILES.getlist("assign_images") + attachments = [] + if request.FILES: + for file in files: + attachment = ReturnImages() + attachment.image = file + attachment.save() + attachments.append(attachment) + instance.assign_images.add(*attachments) + form = AssetAllocationForm( + initial={"assigned_by_employee_id": request.user.employee_get} + ) + messages.success(request, _("Asset allocated successfully!.")) + context["asset_allocation_form"] = form + return render(request, "request_allocation/asset_allocation_creation.html", context) + + +@login_required +def asset_allocate_return_request(request, asset_id): + """ + Handle the initiation of a return request for an allocated asset. + """ + previous_data = request.GET.urlencode() + asset_assign = AssetAssignment.objects.get(id=asset_id) + asset_assign.return_request = True + asset_assign.save() + message = _("Return request for {} initiated.").format(asset_assign.asset_id) + messages.success(request, message) + permed_users = horilla_users_with_perms("asset.change_assetassignment") + notify.send( + request.user.employee_get, + recipient=permed_users, + verb=f"Return request for {asset_assign.asset_id} initiated from\ + {asset_assign.assigned_to_employee_id}", + verb_ar=f"تم بدء طلب الإرجاع للمورد {asset_assign.asset_id}\ + من الموظف {asset_assign.assigned_to_employee_id}", + verb_de=f"Rückgabewunsch für {asset_assign.asset_id} vom Mitarbeiter\ + {asset_assign.assigned_to_employee_id} initiiert", + verb_es=f"Solicitud de devolución para {asset_assign.asset_id}\ + iniciada por el empleado {asset_assign.assigned_to_employee_id}", + verb_fr=f"Demande de retour pour {asset_assign.asset_id}\ + initiée par l'employé {asset_assign.assigned_to_employee_id}", + redirect=reverse("asset-request-allocation-view") + + f"?assigned_to_employee_id={asset_assign.assigned_to_employee_id}&\ + asset_id={asset_assign.asset_id}&assigned_date={asset_assign.assigned_date}", + icon="bag-check", + ) + if request.META.get("HTTP_HX_REQUEST") == "true": + url = reverse("asset-request-allocation-view-search-filter") + return redirect(f"{url}?{previous_data}") + + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +@login_required +@permission_required(perm="asset.change_assetassignment") +def asset_allocate_return(request, asset_id): + """ + View function to return asset. + Args: + - asset_id: integer value representing the ID of the asset + Returns: + - message of the return + """ + + asset_return_form = AssetReturnForm() + asset_allocation = AssetAssignment.objects.filter( + asset_id=asset_id, return_status__isnull=True + ).first() + if request.method == "POST": + asset_return_form = AssetReturnForm(request.POST, request.FILES) + + if asset_return_form.is_valid(): + asset = Asset.objects.filter(id=asset_id).first() + asset_return_status = request.POST.get("return_status") + asset_return_date = request.POST.get("return_date") + asset_return_condition = request.POST.get("return_condition") + files = request.FILES.getlist("return_images") + attachments = [] + context = {"asset_return_form": asset_return_form, "asset_id": asset_id} + response = render(request, "asset/asset_return_form.html", context) + if asset_return_status == "Healthy": + asset_allocation = AssetAssignment.objects.filter( + asset_id=asset_id, return_status__isnull=True + ).first() + asset_allocation.return_date = asset_return_date + asset_allocation.return_status = asset_return_status + asset_allocation.return_condition = asset_return_condition + asset_allocation.return_request = False + asset_allocation.save() + if request.FILES: + for file in files: + attachment = ReturnImages() + attachment.image = file + attachment.save() + attachments.append(attachment) + asset_allocation.return_images.add(*attachments) + asset.asset_status = "Available" + asset.save() + messages.info(request, _("Asset Return Successful !.")) + return HttpResponse( + response.content.decode("utf-8") + + "" + ) + asset.asset_status = "Not-Available" + asset.save() + asset_allocation = AssetAssignment.objects.filter( + asset_id=asset_id, return_status__isnull=True + ).first() + asset_allocation.return_date = asset_return_date + asset_allocation.return_status = asset_return_status + asset_allocation.return_condition = asset_return_condition + asset_allocation.save() + if request.FILES: + for file in files: + attachment = ReturnImages() + attachment.image = file + attachment.save() + attachments.append(attachment) + asset_allocation.return_images.add(*attachments) + messages.info(request, _("Asset Return Successful!.")) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + + context = {"asset_return_form": asset_return_form, "asset_id": asset_id} + context["asset_alocation"] = asset_allocation + return render(request, "asset/asset_return_form.html", context) + + +def filter_pagination_asset_request_allocation(request): + """ + Filter and paginate asset request and allocation data based on search criteria and sort options. + + This function handles the retrieval, filtering, and pagination of asset request and allocation + data.It processes GET parameters to search, sort, and filter asset requests and allocations, + and returns a context dictionary with the filtered data and associated forms for rendering in + a template. + """ + asset_request_allocation_search = request.GET.get("search") + request_field = request.GET.get("request_field") + allocation_field = request.GET.get("allocation_field") + if asset_request_allocation_search is None: + asset_request_allocation_search = "" + employee = request.user.employee_get + asset_assignment = AssetAssignment.objects.all() + asset_request = filtersubordinates( + request=request, + perm="asset.view_assetrequest", + queryset=AssetRequest.objects.all(), + field="requested_employee_id", + ) | AssetRequest.objects.filter(requested_employee_id=request.user.employee_get) + asset_request = asset_request.distinct() + if request.GET.get("assign_sortby"): + asset_assignment = sortby(request, asset_assignment, "assign_sortby") + if request.GET.get("request_sortby"): + asset_request = sortby(request, asset_request, "request_sortby") + + assets = ( + asset_assignment.filter(assigned_to_employee_id=employee) + .exclude(return_status__isnull=False) + .filter(asset_id__asset_name__icontains=asset_request_allocation_search) + ) + + previous_data = request.GET.urlencode() + assets_filtered = CustomAssetFilter(request.GET, queryset=assets) + asset_request_filtered = AssetRequestFilter(request.GET, queryset=asset_request).qs + if request_field != "" and request_field is not None: + asset_request_filtered = group_by_queryset( + asset_request_filtered, request_field, request.GET.get("page"), "page" + ) + list_values = [entry["list"] for entry in asset_request_filtered] + id_list = [] + for value in list_values: + for instance in value.object_list: + id_list.append(instance.id) + + requests_ids = json.dumps(list(id_list)) + + else: + asset_request_filtered = paginator_qry( + asset_request_filtered, request.GET.get("page") + ) + requests_ids = json.dumps( + [instance.id for instance in asset_request_filtered.object_list] + ) + + asset_allocation_filtered = AssetAllocationFilter( + request.GET, queryset=asset_assignment + ).qs + + if allocation_field != "" and allocation_field is not None: + asset_allocation_filtered = group_by_queryset( + asset_allocation_filtered, allocation_field, request.GET.get("page"), "page" + ) + list_values = [entry["list"] for entry in asset_allocation_filtered] + id_list = [] + for value in list_values: + for instance in value.object_list: + id_list.append(instance.id) + + allocations_ids = json.dumps(list(id_list)) + + else: + asset_allocation_filtered = paginator_qry( + asset_allocation_filtered, request.GET.get("page") + ) + allocations_ids = json.dumps( + [instance.id for instance in asset_allocation_filtered.object_list] + ) + + assets_ids = paginator_qry(assets, request.GET.get("page")) + assets_id = json.dumps([instance.id for instance in assets_ids.object_list]) + asset_paginator = Paginator(assets_filtered.qs, get_pagination()) + page_number = request.GET.get("page") + assets = asset_paginator.get_page(page_number) + data_dict = parse_qs(previous_data) + get_key_instances(AssetRequest, data_dict) + get_key_instances(AssetAssignment, data_dict) + get_key_instances(Asset, data_dict) + return { + "assets": assets, + "asset_requests": asset_request_filtered, + "asset_allocations": asset_allocation_filtered, + "assets_filter_form": assets_filtered.form, + "asset_request_filter_form": AssetRequestFilter().form, + "asset_allocation_filter_form": AssetAllocationFilter().form, + "pg": previous_data, + "filter_dict": data_dict, + "gp_request_fields": AssetRequestReGroup.fields, + "gp_Allocation_fields": AssetAllocationReGroup.fields, + "request_field": request_field, + "allocation_field": allocation_field, + "requests_ids": requests_ids, + "allocations_ids": allocations_ids, + "asset_ids": assets_id, + } + + +@login_required +def asset_request_allocation_view(request): + """ + This view is used to display a paginated list of asset allocation requests. + Args: + request (HttpRequest): The HTTP request object. + Returns: + HttpResponse: The HTTP response object with the rendered HTML template. + """ + context = filter_pagination_asset_request_allocation(request) + template = "request_allocation/asset_request_allocation_view.html" + + if ( + request.GET.get("request_field") != "" + and request.GET.get("request_field") is not None + or request.GET.get("allocation_field") != "" + and request.GET.get("allocation_field") is not None + ): + template = "request_allocation/group_by.html" + + return render(request, template, context) + + +@login_required +def asset_request_alloaction_view_search_filter(request): + """ + This view handles the search and filter functionality for the asset request allocation list. + Args: + request: HTTP request object. + Returns: + Rendered HTTP response with the filtered and paginated asset request allocation list. + """ + context = filter_pagination_asset_request_allocation(request) + template = "request_allocation/asset_request_allocation_list.html" + if ( + request.GET.get("request_field") != "" + and request.GET.get("request_field") is not None + or request.GET.get("allocation_field") != "" + and request.GET.get("allocation_field") is not None + ): + template = "request_allocation/group_by.html" + + return render(request, template, context) + + +@login_required +@hx_request_required +def own_asset_individual_view(request, asset_id): + """ + This function is responsible for view the individual own asset + + Args: + request : HTTP request object + id (int): Id of the asset assignment + """ + asset_assignment = AssetAssignment.objects.get(id=asset_id) + asset = asset_assignment.asset_id + context = { + "asset": asset, + "asset_assignment": asset_assignment, + } + requests_ids_json = request.GET.get("assets_ids") + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + previous_id, next_id = closest_numbers(requests_ids, asset_id) + context["assets_ids"] = requests_ids_json + context["previous"] = previous_id + context["next"] = next_id + return render(request, "request_allocation/individual_own.html", context) + + +@login_required +@hx_request_required +def asset_request_individual_view(request, asset_request_id): + """ + Display the details of an individual asset request. + + This view retrieves the asset request with the given ID and renders it in the + 'individual_request.html' template. If a JSON-encoded list of request IDs is + provided in the GET parameters, the view also determines the previous and next + request IDs for easy navigation. + + Args: + request (HttpRequest): The HTTP request object containing metadata about the request. + id (int): The ID of the asset request to be viewed. + + Returns: + HttpResponse: The rendered 'individual_request.html' template with the context data. + """ + dashboard = not request.META.get("HTTP_HX_CURRENT_URL", "").endswith( + "asset-request-allocation-view/" + ) + asset_request = AssetRequest.objects.get(id=asset_request_id) + context = { + "asset_request": asset_request, + "dashboard": dashboard, + } + requests_ids_json = request.GET.get("requests_ids") + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + previous_id, next_id = closest_numbers(requests_ids, asset_request_id) + context["requests_ids"] = requests_ids_json + context["previous"] = previous_id + context["next"] = next_id + return render(request, "request_allocation/individual_request.html", context) + + +@login_required +@hx_request_required +def asset_allocation_individual_view(request, asset_allocation_id): + """ + Display the details of an individual asset allocation. + + This view retrieves the asset allocation with the given ID and renders it in the + 'individual_allocation.html' template. If a JSON-encoded list of allocation IDs is + provided in the GET parameters, the view also determines the previous and next + allocation IDs for easy navigation. + + Args: + request (HttpRequest): The HTTP request object containing metadata about the request. + id (int): The ID of the asset allocation to be viewed. + + Returns: + HttpResponse: The rendered 'individual_allocation.html' template with the context data. + """ + asset_allocation = AssetAssignment.objects.get(id=asset_allocation_id) + context = {"asset_allocation": asset_allocation} + allocation_ids_json = request.GET.get("allocations_ids") + if allocation_ids_json: + allocation_ids = json.loads(allocation_ids_json) + previous_id, next_id = closest_numbers(allocation_ids, asset_allocation_id) + context["allocations_ids"] = allocation_ids_json + context["previous"] = previous_id + context["next"] = next_id + return render(request, "request_allocation/individual allocation.html", context) + + +def convert_nan(val): + """ + Convert NaN values to None. + """ + if pd.isna(val): + return None + return val + + +fs = FileSystemStorage(location="csv_tmp/") + + +def csv_asset_import(file): + file_content = ContentFile(file.read()) + file_name = fs.save("_tmp.csv", file_content) + tmp_file = fs.path(file_name) + + with open(tmp_file, errors="ignore") as csv_file: + reader = csv.reader(csv_file) + next(reader) # Skip header row + + asset_list = [] + for row in reader: + ( + asset_name, + asset_description, + asset_tracking_id, + asset_purchase_date, + asset_purchase_cost, + asset_category_name, + asset_status, + asset_lot_number, + ) = row + + # Helper function to get or create categories and lots + asset_category, _ = AssetCategory.objects.get_or_create( + asset_category_name=asset_category_name + ) + asset_lot, _ = AssetLot.objects.get_or_create(lot_number=asset_lot_number) + + asset_list.append( + Asset( + asset_name=asset_name, + asset_description=asset_description, + asset_tracking_id=asset_tracking_id, + asset_purchase_date=asset_purchase_date, + asset_purchase_cost=asset_purchase_cost, + asset_status=asset_status, + asset_category_id=asset_category, + asset_lot_number_id=asset_lot, + ) + ) + + # Bulk create assets from CSV + Asset.objects.bulk_create(asset_list) + + # Delete the temporary file + if os.path.exists(tmp_file): + os.remove(tmp_file) + + +def spreadsheetml_asset_import(dataframe): + for index, row in dataframe.iterrows(): + asset_name = convert_nan(row["Asset name"]) + asset_description = convert_nan(row["Description"]) + asset_tracking_id = convert_nan(row["Tracking id"]) + purchase_date = convert_nan(row["Purchase date"]) + purchase_cost = convert_nan(row["Purchase cost"]) + category_name = convert_nan(row["Category"]) + lot_number = convert_nan(row["Batch number"]) + status = convert_nan(row["Status"]) + + asset_category, create = AssetCategory.objects.get_or_create( + asset_category_name=category_name + ) + asset_lot_number, create = AssetLot.objects.get_or_create(lot_number=lot_number) + Asset.objects.create( + asset_name=asset_name, + asset_description=asset_description, + asset_tracking_id=asset_tracking_id, + asset_purchase_date=purchase_date, + asset_purchase_cost=purchase_cost, + asset_category_id=asset_category, + asset_status=status, + asset_lot_number_id=asset_lot_number, + ) + + +@login_required +@permission_required(perm="asset.add_asset") +def asset_import(request): + """ + Handle the import of asset data from an uploaded Excel file. + + This view processes a POST request containing an Excel file, reads the data, + creates Asset objects from the data, and saves them to the database. If the + import is successful, a success message is displayed. Otherwise, appropriate + error messages are shown. + + Args: + request (HttpRequest): The HTTP request object containing metadata about the request. + + Returns: + HttpResponseRedirect: A redirect to the asset category view after processing the import. + """ + if request.META.get("HTTP_HX_REQUEST"): + return render(request, "asset/asset_import.html") + try: + if request.method == "POST": + file = request.FILES.get("asset_import") + if file is not None and file.content_type == "text/csv": + try: + csv_asset_import(file) + messages.success(request, _("Successfully imported Assets")) + except Exception as exception: + messages.error(request, f"{exception}") + elif ( + file is not None + and file.content_type + == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ): + try: + dataframe = pd.read_excel(file) + spreadsheetml_asset_import(dataframe) + messages.success(request, _("Successfully imported Assets")) + except KeyError as exception: + messages.error(request, f"{exception}") + else: + messages.error(request, _("File Error")) + return redirect(asset_category_view) + except Exception as exception: + messages.error(request, f"{exception}") + return redirect(asset_category_view) + + +@login_required +def asset_excel(_request): + """asset excel download view""" + + try: + columns = [ + "Asset name", + "Description", + "Tracking id", + "Purchase date", + "Purchase cost", + "Category", + "Status", + "Batch number", + ] + # Create a pandas DataFrame with columns but no data + dataframe = pd.DataFrame(columns=columns) + # Write the DataFrame to an Excel file + response = HttpResponse(content_type="application/ms-excel") + response["Content-Disposition"] = 'attachment; filename="my_excel_file.xlsx"' + dataframe.to_excel(response, index=False) + return response + except Exception as exception: + return HttpResponse(exception) + + +@login_required +@permission_required("asset.view_assetcategory") +def asset_export_excel(request): + """asset export view""" + asset_export_filter = AssetExportFilter(request.GET, queryset=Asset.objects.all()) + if request.method == "POST": + queryset_all = Asset.objects.all() + if not queryset_all: + messages.warning(request, _("There are no assets to export.")) + return redirect("asset-category-view") # or some other URL + + queryset = AssetExportFilter(request.POST, queryset=queryset_all).qs + + # Convert the queryset to a Pandas DataFrame + data = { + "asset_name": [], + "asset_description": [], + "asset_tracking_id": [], + "asset_purchase_date": [], + "asset_purchase_cost": [], + "asset_category_id": [], + "asset_status": [], + "asset_lot_number_id": [], + } + + fields_to_check = [ + "asset_name", + "asset_description", + "asset_tracking_id", + "asset_purchase_date", + "asset_purchase_cost", + "asset_category_id", + "asset_status", + "asset_lot_number_id", + ] + + for asset in queryset: + for field in fields_to_check: + # Get the value of the field for the current asset + value = getattr(asset, field) + + if isinstance(value, date): + user = request.user + emp = user.employee_get + + # Taking the company_name of the user + info = EmployeeWorkInformation.objects.filter(employee_id=emp) + if info.exists(): + for i in info: + employee_company = i.company_id + company_name = Company.objects.filter(company=employee_company) + emp_company = company_name.first() + + # Access the date_format attribute directly + date_format = ( + emp_company.date_format if emp_company else "MMM. D, YYYY" + ) + else: + date_format = "MMM. D, YYYY" + + # Convert the string to a datetime.date object + start_date = datetime.strptime(str(value), "%Y-%m-%d").date() + + # The formatted date for each format + for format_name, format_string in HORILLA_DATE_FORMATS.items(): + if format_name == date_format: + value = start_date.strftime(format_string) + + # Append the value if it exists, or append None if it's None + data[field].append(value if value is not None else None) + + # Fill any missing values with None + for key in data: + data[key] = data[key] + [None] * (len(queryset) - len(data[key])) + + # Convert the data dictionary to a Pandas DataFrame + dataframe = pd.DataFrame(data) + + # Convert any date fields to the desired format + # Rename the columns as needed + dataframe = dataframe.rename( + columns={ + "asset_name": "Asset name", + "asset_description": "Description", + "asset_tracking_id": "Tracking id", + "asset_purchase_date": "Purchase date", + "asset_purchase_cost": "Purchase cost", + "asset_category_id": "Category", + "asset_status": "Status", + "asset_lot_number_id": "Batch number", + } + ) + + # Write the DataFrame to an Excel file + response = HttpResponse(content_type="application/vnd.ms-excel") + response["Content-Disposition"] = 'attachment; filename="assets.xlsx"' + dataframe.to_excel(response, index=False) + return response + context = {"asset_export_filter": asset_export_filter} + return render(request, "category/asset_filter_export.html", context) + + +@login_required +@hx_request_required +@permission_required(perm="asset.add_assetlot") +def asset_batch_number_creation(request): + """asset batch number creation view""" + hx_vals = ( + request.GET.get("data") if request.GET.get("data") else request.GET.urlencode() + ) + asset_batch_form = AssetBatchForm() + context = { + "asset_batch_form": asset_batch_form, + "hx_vals": hx_vals, + "hx_get": None, + "hx_target": None, + } + if request.method == "POST": + asset_batch_form = AssetBatchForm(request.POST) + if asset_batch_form.is_valid(): + asset_batch_form.save() + asset_batch_form = AssetBatchForm() + messages.success(request, _("Batch number created successfully.")) + if AssetLot.objects.filter().count() == 1 and not hx_vals: + return HttpResponse(status=204, headers={"HX-Refresh": "true"}) + if hx_vals: + category_id = request.GET.get("asset_category_id") + url = reverse("asset-creation", args=[category_id]) + instance = AssetLot.objects.all().order_by("-id").first() + mutable_get = request.GET.copy() + mutable_get["asset_lot_number_id"] = str(instance.id) + context["hx_get"] = f"{url}?{mutable_get.urlencode()}" + context["hx_target"] = "#objectCreateModalTarget" + context["asset_batch_form"] = asset_batch_form + return render(request, "batch/asset_batch_number_creation.html", context) + + +@login_required +@permission_required(perm="asset.view_assetlot") +def asset_batch_view(request): + """ + View function to display details of all batch numbers. + + Returns: + - all asset batch numbers based on page + """ + + asset_batches = AssetLot.objects.all() + previous_data = request.GET.urlencode() + asset_batch_numbers_search_paginator = Paginator(asset_batches, get_pagination()) + page_number = request.GET.get("page") + asset_batch_numbers = asset_batch_numbers_search_paginator.get_page(page_number) + asset_batch_form = AssetBatchForm() + if asset_batches.exists(): + template = "batch/asset_batch_number_view.html" + else: + template = "batch/asset_batch_empty.html" + context = { + "batch_numbers": asset_batch_numbers, + "asset_batch_form": asset_batch_form, + "pg": previous_data, + } + return render(request, template, context) + + +@login_required +@permission_required(perm="asset.change_assetlot") +def asset_batch_update(request, batch_id): + """ + View function to return asset. + Args: + - batch_id: integer value representing the ID of the asset + Returns: + - message of the return + """ + asset_batch_number = AssetLot.objects.get(id=batch_id) + asset_batch = AssetLot.objects.get(id=batch_id) + asset_batch_form = AssetBatchForm(instance=asset_batch) + context = { + "asset_batch_update_form": asset_batch_form, + } + assigned_batch_number = Asset.objects.filter(asset_lot_number_id=asset_batch_number) + if assigned_batch_number: + asset_batch_form = AssetBatchForm(instance=asset_batch) + asset_batch_form["lot_number"].field.widget.attrs.update( + {"readonly": "readonly"} + ) + context["asset_batch_update_form"] = asset_batch_form + context["in_use_message"] = ( + _("This batch number is already in-use") + if request.method == "GET" + else None + ) + if request.method == "POST": + asset_batch_form = AssetBatchForm(request.POST, instance=asset_batch_number) + if asset_batch_form.is_valid(): + asset_batch_form.save() + messages.success(request, _("Batch updated successfully.")) + context["asset_batch_update_form"] = asset_batch_form + return render(request, "batch/asset_batch_number_update.html", context) + + +@login_required +@hx_request_required +@permission_required(perm="asset.delete_assetlot") +def asset_batch_number_delete(request, batch_id): + """ + View function to return asset. + Args: + - batch_id: integer value representing the ID of the asset + Returns: + - message of the return + """ + previous_data = request.GET.urlencode() + try: + asset_batch_number = AssetLot.objects.get(id=batch_id) + assigned_batch_number = Asset.objects.filter( + asset_lot_number_id=asset_batch_number + ) + if assigned_batch_number: + messages.error(request, _("Batch number in-use")) + return redirect(f"/asset/asset-batch-number-search?{previous_data}") + asset_batch_number.delete() + messages.success(request, _("Batch number deleted")) + except AssetLot.DoesNotExist: + messages.error(request, _("Batch number not found")) + except ProtectedError: + messages.error(request, _("You cannot delete this Batch number.")) + if not AssetLot.objects.filter(): + return HttpResponse(status=204, headers={"HX-Refresh": "true"}) + return redirect(f"/asset/asset-batch-number-search?{previous_data}") + + +@login_required +@hx_request_required +def asset_batch_number_search(request): + """ + View function to return search data of asset batch number. + + Args: + - id: integer value representing the ID of the asset + + Returns: + - message of the return + """ + search_query = request.GET.get("search") + if search_query is None: + search_query = "" + + asset_batches = AssetLot.objects.all().filter(lot_number__icontains=search_query) + previous_data = request.GET.urlencode() + asset_batch_numbers_search_paginator = Paginator(asset_batches, get_pagination()) + page_number = request.GET.get("page") + asset_batch_numbers = asset_batch_numbers_search_paginator.get_page(page_number) + + context = { + "batch_numbers": asset_batch_numbers, + "pg": previous_data, + } + + return render(request, "batch/asset_batch_number_list.html", context) + + +@login_required +def asset_count_update(request): + """ + View function to return update asset count at asset category. + Args: + - id: integer value representing the ID of the asset category + Returns: + - count of asset inside the category + """ + if request.method == "POST": + category_id = request.POST.get("asset_category_id") + if category_id is not None: + category = AssetCategory.objects.get(id=category_id) + asset_count = category.asset_set.count() + return HttpResponse(asset_count) + return HttpResponse("error") + + +@login_required +@permission_required(perm="asset.view_assetcategory") +def asset_dashboard(request): + """ + This method is used to render the dashboard of the asset module. + """ + assets = Asset.objects.all() + asset_in_use = Asset.objects.filter(asset_status="In use") + asset_requests = AssetRequest.objects.filter( + asset_request_status="Requested", requested_employee_id__is_active=True + ) + + context = { + "assets": assets, + "asset_in_use": asset_in_use, + "asset_requests": asset_requests, + } + return render(request, "asset/dashboard.html", context) + + +@login_required +@permission_required(perm="asset.view_assetrequest") +def asset_dashboard_requests(request): + """ + Handles the asset request approval dashboard view. + + This view fetches and filters asset requests that are currently in the + "Requested" status and belong to employees who are active. + + The filtered asset requests are then passed to the template for rendering, + along with a JSON-encoded list of the request IDs. + """ + asset_requests = AssetRequest.objects.filter( + asset_request_status="Requested", requested_employee_id__is_active=True + ) + requests_ids = json.dumps([instance.id for instance in asset_requests]) + context = { + "asset_requests": asset_requests, + "requests_ids": requests_ids, + } + return render(request, "asset/dashboard_asset_requests.html", context) + + +@login_required +@permission_required(perm="asset.view_assetassignment") +def asset_dashboard_allocates(request): + asset_allocations = AssetAssignment.objects.filter( + asset_id__asset_status="In use", assigned_to_employee_id__is_active=True + ) + context = { + "asset_allocations": asset_allocations, + } + return render(request, "asset/dashboard_allocated_assets.html", context) + + +@login_required +@permission_required(perm="asset.view_assetcategory") +def asset_available_chart(_request): + """ + This function returns the response for the available asset chart in the asset dashboard. + """ + asset_available = Asset.objects.filter(asset_status="Available") + asset_unavailable = Asset.objects.filter(asset_status="Not-Available") + asset_in_use = Asset.objects.filter(asset_status="In use") + + labels = ["In use", "Available", "Not-Available"] + dataset = [ + { + "label": _("asset"), + "data": [len(asset_in_use), len(asset_available), len(asset_unavailable)], + }, + ] + + response = { + "labels": labels, + "dataset": dataset, + "message": _("Oops!! No Asset found..."), + "emptyImageSrc": f"/{settings.STATIC_URL}images/ui/asset.png", + } + return JsonResponse(response) + + +@login_required +@permission_required(perm="asset.view_assetcategory") +def asset_category_chart(_request): + """ + This function returns the response for the asset category chart in the asset dashboard. + """ + asset_categories = AssetCategory.objects.all() + data = [] + for asset_category in asset_categories: + category_count = 0 + category_count = len(asset_category.asset_set.filter(asset_status="In use")) + data.append(category_count) + + labels = [category.asset_category_name for category in asset_categories] + dataset = [ + { + "label": _("assets in use"), + "data": data, + }, + ] + + response = { + "labels": labels, + "dataset": dataset, + "message": _("Oops!! No Asset found..."), + "emptyImageSrc": f"/{settings.STATIC_URL}images/ui/asset.png", + } + return JsonResponse(response) + + +@login_required +@permission_required(perm="asset.view_assetassignment") +def asset_history(request): + """ + This function is responsible for loading the asset history view + + Args: + + + Returns: + returns asset history view template + """ + previous_data = request.GET.urlencode() + "&returned_assets=True" + asset_assignments = AssetHistoryFilter({"returned_assets": "True"}).qs.order_by( + "-id" + ) + data_dict = parse_qs(previous_data) + get_key_instances(AssetAssignment, data_dict) + asset_assignments = paginator_qry(asset_assignments, request.GET.get("page")) + requests_ids = json.dumps( + [instance.id for instance in asset_assignments.object_list] + ) + context = { + "asset_assignments": asset_assignments, + "f": AssetHistoryFilter(), + "filter_dict": data_dict, + "gp_fields": AssetHistoryReGroup().fields, + "pd": previous_data, + "requests_ids": requests_ids, + } + return render(request, "asset_history/asset_history_view.html", context) + + +@login_required +@permission_required(perm="asset.view_assetassignment") +def asset_history_single_view(request, asset_id): + """ + this method is used to view details of individual asset assignments + + Args: + request (HTTPrequest): http request + asset_id (int): ID of the asset assignment + + Returns: + html: Returns asset history single view template + """ + asset_assignment = get_object_or_404(AssetAssignment, id=asset_id) + context = {"asset_assignment": asset_assignment} + requests_ids_json = request.GET.get("requests_ids") + if requests_ids_json: + requests_ids = json.loads(requests_ids_json) + previous_id, next_id = closest_numbers(requests_ids, asset_id) + context["requests_ids"] = requests_ids_json + context["previous"] = previous_id + context["next"] = next_id + return render( + request, + "asset_history/asset_history_single_view.html", + context, + ) + + +@login_required +@permission_required(perm="asset.view_assetassignment") +def asset_history_search(request): + """ + This method is used to filter the asset history view or to group by the datas. + + Args: + request (HTTPrequest):http request + + Returns: + returns asset history list or group by + """ + previous_data = request.GET.urlencode() + asset_assignments = AssetHistoryFilter(request.GET).qs.order_by("-id") + asset_assignments = sortby(request, asset_assignments, "sortby") + template = "asset_history/asset_history_list.html" + field = request.GET.get("field") + if field != "" and field is not None: + asset_assignments = group_by_queryset( + asset_assignments, field, request.GET.get("page"), "page" + ) + template = "asset_history/group_by.html" + list_values = [entry["list"] for entry in asset_assignments] + id_list = [] + for value in list_values: + for instance in value.object_list: + id_list.append(instance.id) + + requests_ids = json.dumps(list(id_list)) + else: + asset_assignments = paginator_qry(asset_assignments, request.GET.get("page")) + + requests_ids = json.dumps( + [instance.id for instance in asset_assignments.object_list] + ) + data_dict = parse_qs(previous_data) + get_key_instances(AssetAssignment, data_dict) + + return render( + request, + template, + { + "asset_assignments": asset_assignments, + "filter_dict": data_dict, + "field": field, + "pd": previous_data, + "requests_ids": requests_ids, + }, + ) + + +@login_required +@owner_can_enter("asset.view_asset", Employee) +def asset_tab(request, emp_id): + """ + This function is used to view asset tab of an employee in employee individual view. + + Parameters: + request (HttpRequest): The HTTP request object. + emp_id (int): The id of the employee. + + Returns: return asset-tab template + + """ + employee = Employee.objects.get(id=emp_id) + assets_requests = employee.requested_employee.all() + assets = employee.allocated_employee.all() + assets_ids = ( + json.dumps([instance.id for instance in assets]) if assets else json.dumps([]) + ) + context = { + "assets": assets, + "requests": assets_requests, + "assets_ids": assets_ids, + "employee": emp_id, + } + return render(request, "tabs/asset-tab.html", context=context) + + +@login_required +@hx_request_required +def profile_asset_tab(request, emp_id): + """ + This function is used to view asset tab of an employee in employee profile view. + + Parameters: + request (HttpRequest): The HTTP request object. + emp_id (int): The id of the employee. + + Returns: return profile-asset-tab template + + """ + employee = Employee.objects.get(id=emp_id) + assets = employee.allocated_employee.all() + assets_ids = json.dumps([instance.id for instance in assets]) + context = { + "assets": assets, + "assets_ids": assets_ids, + } + return render(request, "tabs/profile-asset-tab.html", context=context) + + +@login_required +@hx_request_required +def asset_request_tab(request, emp_id): + """ + This function is used to view asset request tab of an employee in employee individual view. + + Parameters: + request (HttpRequest): The HTTP request object. + emp_id (int): The id of the employee. + + Returns: return asset-request-tab template + + """ + employee = Employee.objects.get(id=emp_id) + assets_requests = employee.requested_employee.all() + requests_ids = json.dumps([instance.id for instance in assets_requests]) + context = { + "asset_requests": assets_requests, + "emp_id": emp_id, + "requests_ids": requests_ids, + } + return render(request, "tabs/asset_request_tab.html", context=context)