Upload files to "asset"

Signed-off-by: nestict <developer@nestict.com>
This commit is contained in:
2026-01-16 12:47:27 +01:00
parent 4d615fe6ea
commit cd1cd2e83a
12 changed files with 3430 additions and 0 deletions

1
asset/__init__.py Normal file
View File

@@ -0,0 +1 @@
from asset import scheduler

34
asset/admin.py Normal file
View File

@@ -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)

33
asset/apps.py Normal file
View File

@@ -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()

375
asset/filters.py Normal file
View File

@@ -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"),
]

384
asset/forms.py Normal file
View File

@@ -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__"

331
asset/models.py Normal file
View File

@@ -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),
}

21
asset/resources.py Normal file
View File

@@ -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

102
asset/scheduler.py Normal file
View File

@@ -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()

59
asset/sidebar.py Normal file
View File

@@ -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")

5
asset/tests.py Normal file
View File

@@ -0,0 +1,5 @@
"""
This module contains test cases for the assets application.
"""
from django.test import TestCase

207
asset/urls.py Normal file
View File

@@ -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/<int:asset_category_id>/",
views.asset_creation,
name="asset-creation",
),
path("asset-list/<int:cat_id>", views.asset_list, name="asset-list"),
path("asset-update/<int:asset_id>/", views.asset_update, name="asset-update"),
path(
"duplicate-asset/<int:obj_id>/",
object_duplicate,
name="duplicate-asset",
kwargs={
"model": Asset,
"form": AssetForm,
"form_name": "asset_creation_form",
"template": "asset/asset_creation.html",
},
),
path("asset-delete/<int:asset_id>/", views.asset_delete, name="asset-delete"),
path(
"asset-information/<int:asset_id>/",
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/<int:obj_id>/",
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/<int:cat_id>",
views.asset_category_update,
name="asset-category-update",
),
path(
"asset-category-delete/<int:cat_id>",
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/<int:asset_request_id>",
views.asset_request_individual_view,
name="asset-request-individual-view",
),
path(
"own-asset-individual-view/<int:asset_id>",
views.own_asset_individual_view,
name="own-asset-individual-view",
),
path(
"asset-allocation-individual-view/<int:asset_allocation_id>",
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/<int:req_id>/",
views.asset_request_approve,
name="asset-request-approve",
),
path(
"asset-request-reject/<int:req_id>/",
views.asset_request_reject,
name="asset-request-reject",
),
path(
"asset-allocate-creation",
views.asset_allocate_creation,
name="asset-allocate-creation",
),
path(
"asset-allocate-return/<int:asset_id>/",
views.asset_allocate_return,
name="asset-allocate-return",
),
path(
"asset-allocate-return-request/<int:asset_id>/",
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/<int:batch_id>",
views.asset_batch_update,
name="asset-batch-update",
),
path(
"asset-batch-number-delete/<int:batch_id>",
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/<int:asset_id>",
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/<int:asset_id>",
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/<int:emp_id>", views.asset_tab, name="asset-tab"),
path(
"profile-asset-tab/<int:emp_id>",
views.profile_asset_tab,
name="profile-asset-tab",
),
path(
"asset-request-tab/<int:emp_id>",
views.asset_request_tab,
name="asset-request-tab",
),
path(
"main-dashboard-asset-requests",
views.asset_dashboard_requests,
name="main-dashboard-asset-requests",
),
]

1878
asset/views.py Normal file

File diff suppressed because it is too large Load Diff