Versiunea funcțională EUMI
This commit is contained in:
+10
-2
@@ -1,3 +1,11 @@
|
||||
.venv
|
||||
# Python & Django
|
||||
__pycache__/
|
||||
db.sqlite3
|
||||
*.py[cod]
|
||||
*.sqlite3
|
||||
.env
|
||||
|
||||
# Docker & Volume
|
||||
postgres_data/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
+79
-33
@@ -1,4 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
from .models import (
|
||||
Computer,
|
||||
@@ -7,49 +9,50 @@ from .models import (
|
||||
DigitalCertificate,
|
||||
MaintenanceRecord,
|
||||
DecommissionRecord,
|
||||
HandoverProtocol,
|
||||
)
|
||||
|
||||
from .models import DecommissionRecord
|
||||
|
||||
# --- Inlines for Computer ---
|
||||
class MaintenanceInlineComputer(admin.TabularInline):
|
||||
model = MaintenanceRecord
|
||||
fk_name = "computer"
|
||||
extra = 0
|
||||
# class MaintenanceInlineComputer(admin.TabularInline):
|
||||
# model = MaintenanceRecord
|
||||
# fk_name = "computer"
|
||||
# extra = 0
|
||||
|
||||
|
||||
class DecommissionInlineComputer(admin.StackedInline):
|
||||
model = DecommissionRecord
|
||||
fk_name = "computer"
|
||||
extra = 0
|
||||
max_num = 1
|
||||
# class DecommissionInlineComputer(admin.StackedInline):
|
||||
# model = DecommissionRecord
|
||||
# fk_name = "computer"
|
||||
# extra = 0
|
||||
# max_num = 1
|
||||
|
||||
|
||||
# --- Inlines for Monitor ---
|
||||
class MaintenanceInlineMonitor(admin.TabularInline):
|
||||
model = MaintenanceRecord
|
||||
fk_name = "monitor"
|
||||
extra = 0
|
||||
# # --- Inlines for Monitor ---
|
||||
# class MaintenanceInlineMonitor(admin.TabularInline):
|
||||
# model = MaintenanceRecord
|
||||
# fk_name = "monitor"
|
||||
# extra = 0
|
||||
|
||||
|
||||
class DecommissionInlineMonitor(admin.StackedInline):
|
||||
model = DecommissionRecord
|
||||
fk_name = "monitor"
|
||||
extra = 0
|
||||
max_num = 1
|
||||
# class DecommissionInlineMonitor(admin.StackedInline):
|
||||
# model = DecommissionRecord
|
||||
# fk_name = "monitor"
|
||||
# extra = 0
|
||||
# max_num = 1
|
||||
|
||||
|
||||
# --- Inlines for Peripheral ---
|
||||
class MaintenanceInlinePeripheral(admin.TabularInline):
|
||||
model = MaintenanceRecord
|
||||
fk_name = "peripheral"
|
||||
extra = 0
|
||||
# # --- Inlines for Peripheral ---
|
||||
# class MaintenanceInlinePeripheral(admin.TabularInline):
|
||||
# model = MaintenanceRecord
|
||||
# fk_name = "peripheral"
|
||||
# extra = 0
|
||||
|
||||
|
||||
class DecommissionInlinePeripheral(admin.StackedInline):
|
||||
model = DecommissionRecord
|
||||
fk_name = "peripheral"
|
||||
extra = 0
|
||||
max_num = 1
|
||||
# class DecommissionInlinePeripheral(admin.StackedInline):
|
||||
# model = DecommissionRecord
|
||||
# fk_name = "peripheral"
|
||||
# extra = 0
|
||||
# max_num = 1
|
||||
|
||||
|
||||
@admin.register(Computer)
|
||||
@@ -57,7 +60,7 @@ class ComputerAdmin(SimpleHistoryAdmin):
|
||||
list_display = ("equipment_code", "oem_name", "status", "assigned_to", "entry_date")
|
||||
list_filter = ("status", "entry_date", "assigned_to")
|
||||
search_fields = ("equipment_code", "oem_name", "mac_address", "observations")
|
||||
inlines = [MaintenanceInlineComputer, DecommissionInlineComputer]
|
||||
# inlines = [MaintenanceInlineComputer, DecommissionInlineComputer]
|
||||
|
||||
|
||||
@admin.register(Monitor)
|
||||
@@ -65,7 +68,7 @@ class MonitorAdmin(SimpleHistoryAdmin):
|
||||
list_display = ("equipment_code", "name", "status", "assigned_to", "entry_date")
|
||||
list_filter = ("status", "entry_date", "assigned_to")
|
||||
search_fields = ("equipment_code", "name", "serial_number")
|
||||
inlines = [MaintenanceInlineMonitor, DecommissionInlineMonitor]
|
||||
# inlines = [MaintenanceInlineMonitor, DecommissionInlineMonitor]
|
||||
|
||||
|
||||
@admin.register(Peripheral)
|
||||
@@ -79,7 +82,7 @@ class PeripheralAdmin(SimpleHistoryAdmin):
|
||||
)
|
||||
list_filter = ("status", "peripheral_type", "entry_date", "assigned_to")
|
||||
search_fields = ("equipment_code", "name", "serial_number")
|
||||
inlines = [MaintenanceInlinePeripheral, DecommissionInlinePeripheral]
|
||||
# inlines = [MaintenanceInlinePeripheral, DecommissionInlinePeripheral]
|
||||
|
||||
|
||||
@admin.register(DigitalCertificate)
|
||||
@@ -108,3 +111,46 @@ class DecommissionRecordAdmin(SimpleHistoryAdmin):
|
||||
list_display = ("document_number", "date", "reason")
|
||||
list_filter = ("date",)
|
||||
search_fields = ("document_number", "reason")
|
||||
|
||||
|
||||
@admin.register(HandoverProtocol)
|
||||
class HandoverProtocolAdmin(SimpleHistoryAdmin):
|
||||
list_display = (
|
||||
"document_number",
|
||||
"date",
|
||||
"previous_holder",
|
||||
"new_holder",
|
||||
"it_validator",
|
||||
"print_protocol_button",
|
||||
)
|
||||
list_filter = ("date", "it_validator")
|
||||
filter_horizontal = ("computers", "monitors", "peripherals")
|
||||
# Make these fields read-only so they cannot be manually overwritten
|
||||
readonly_fields = ("document_number", "it_validator")
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not obj.it_validator:
|
||||
# Captures the logged-in user's name automatically
|
||||
obj.it_validator = request.user.get_full_name() or request.user.username
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def print_protocol_button(self, obj):
|
||||
if obj.pk:
|
||||
url = reverse("equipment:print_handover", args=[obj.pk])
|
||||
return format_html(
|
||||
'<a class="button" href="{}" target="_blank" style="background-color: #417690; color: white; padding: 5px 10px; border-radius: 4px; text-decoration: none;">🖨️ Printează PV</a>',
|
||||
url,
|
||||
)
|
||||
return "-"
|
||||
|
||||
print_protocol_button.short_description = "Acțiune"
|
||||
|
||||
# @admin.register(DecommissionRecord)
|
||||
# class DecommissionRecordAdmin(SimpleHistoryAdmin):
|
||||
# list_display = ("document_number", "date", "reason")
|
||||
# search_fields = ("document_number", "reason")
|
||||
# list_filter = ("date",)
|
||||
|
||||
# Acest filter_horizontal va crea o interfață cu două coloane foarte utilă
|
||||
# pentru a selecta ușor multiple echipamente din stânga în dreapta
|
||||
filter_horizontal = ("computers", "monitors", "peripherals")
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from .models import (
|
||||
Computer,
|
||||
Monitor,
|
||||
Peripheral,
|
||||
MaintenanceRecord,
|
||||
HandoverProtocol,
|
||||
DecommissionRecord,
|
||||
DigitalCertificate,
|
||||
)
|
||||
|
||||
|
||||
class ComputerForm(forms.ModelForm):
|
||||
entry_date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Computer
|
||||
fields = [
|
||||
"equipment_code",
|
||||
"status",
|
||||
"assigned_to",
|
||||
"entry_date",
|
||||
"oem_name",
|
||||
"motherboard",
|
||||
"cpu",
|
||||
"ram",
|
||||
"storage",
|
||||
"optical_drive",
|
||||
"gpu",
|
||||
"mac_address",
|
||||
"os_license",
|
||||
"additional_licenses",
|
||||
"observations",
|
||||
]
|
||||
widgets = {
|
||||
"equipment_code": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"assigned_to": forms.Select(attrs={"class": "form-select"}),
|
||||
"oem_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"motherboard": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"cpu": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"ram": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"storage": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"optical_drive": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"gpu": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"mac_address": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"os_license": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"additional_licenses": forms.Textarea(
|
||||
attrs={"class": "form-control", "rows": 2}
|
||||
),
|
||||
"observations": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["assigned_to"].disabled = True
|
||||
self.fields["assigned_to"].help_text = (
|
||||
"Transferul se face doar generând un Proces Verbal nou."
|
||||
)
|
||||
|
||||
|
||||
class MonitorForm(forms.ModelForm):
|
||||
entry_date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Monitor
|
||||
fields = [
|
||||
"equipment_code",
|
||||
"status",
|
||||
"assigned_to",
|
||||
"entry_date",
|
||||
"name",
|
||||
"serial_number",
|
||||
"screen_diagonal",
|
||||
"observations",
|
||||
]
|
||||
widgets = {
|
||||
"equipment_code": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"assigned_to": forms.Select(attrs={"class": "form-select"}),
|
||||
"name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"serial_number": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"screen_diagonal": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"observations": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["assigned_to"].disabled = True
|
||||
self.fields["assigned_to"].help_text = (
|
||||
"Transferul se face doar generând un Proces Verbal nou."
|
||||
)
|
||||
|
||||
|
||||
class PeripheralForm(forms.ModelForm):
|
||||
entry_date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Peripheral
|
||||
fields = [
|
||||
"equipment_code",
|
||||
"status",
|
||||
"assigned_to",
|
||||
"entry_date",
|
||||
"peripheral_type",
|
||||
"name",
|
||||
"serial_number",
|
||||
"observations",
|
||||
]
|
||||
widgets = {
|
||||
"equipment_code": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"assigned_to": forms.Select(attrs={"class": "form-select"}),
|
||||
"peripheral_type": forms.Select(attrs={"class": "form-select"}),
|
||||
"name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"serial_number": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"observations": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["assigned_to"].disabled = True
|
||||
self.fields["assigned_to"].help_text = (
|
||||
"Transferul se face doar generând un Proces Verbal nou."
|
||||
)
|
||||
|
||||
|
||||
class MaintenanceRecordForm(forms.ModelForm):
|
||||
date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MaintenanceRecord
|
||||
fields = ["date", "operation_type", "description", "performed_by"]
|
||||
widgets = {
|
||||
"operation_type": forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"placeholder": "ex: Curățare praf, Upgrade RAM...",
|
||||
}
|
||||
),
|
||||
"description": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
"performed_by": forms.TextInput(attrs={"class": "form-control"}),
|
||||
}
|
||||
|
||||
|
||||
class HandoverProtocolForm(forms.ModelForm):
|
||||
date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = HandoverProtocol
|
||||
fields = [
|
||||
"date",
|
||||
"previous_holder",
|
||||
"new_holder",
|
||||
"computers",
|
||||
"monitors",
|
||||
"peripherals",
|
||||
"notes",
|
||||
"scanned_document",
|
||||
]
|
||||
widgets = {
|
||||
"previous_holder": forms.Select(
|
||||
attrs={
|
||||
"class": "form-select",
|
||||
"hx-get": reverse_lazy("equipment:load_user_equipment"),
|
||||
"hx-target": "#equipment-lists",
|
||||
"hx-trigger": "change",
|
||||
}
|
||||
),
|
||||
"new_holder": forms.Select(attrs={"class": "form-select"}),
|
||||
"computers": forms.SelectMultiple(
|
||||
attrs={"class": "form-select", "size": 5}
|
||||
),
|
||||
"monitors": forms.SelectMultiple(attrs={"class": "form-select", "size": 5}),
|
||||
"peripherals": forms.SelectMultiple(
|
||||
attrs={"class": "form-select", "size": 5}
|
||||
),
|
||||
"notes": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
"scanned_document": forms.FileInput(attrs={"class": "form-control"}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.is_bound and not self.instance.pk:
|
||||
self.fields["computers"].queryset = Computer.objects.none()
|
||||
self.fields["monitors"].queryset = Monitor.objects.none()
|
||||
self.fields["peripherals"].queryset = Peripheral.objects.none()
|
||||
|
||||
|
||||
class DecommissionRecordForm(forms.ModelForm):
|
||||
date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DecommissionRecord
|
||||
fields = [
|
||||
"document_number",
|
||||
"date",
|
||||
"committee_members",
|
||||
"reason",
|
||||
"computers",
|
||||
"monitors",
|
||||
"peripherals",
|
||||
]
|
||||
widgets = {
|
||||
"document_number": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"committee_members": forms.Textarea(
|
||||
attrs={"class": "form-control", "rows": 2}
|
||||
),
|
||||
"reason": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
"computers": forms.SelectMultiple(
|
||||
attrs={"class": "form-select", "size": 5}
|
||||
),
|
||||
"monitors": forms.SelectMultiple(attrs={"class": "form-select", "size": 5}),
|
||||
"peripherals": forms.SelectMultiple(
|
||||
attrs={"class": "form-select", "size": 5}
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.is_bound and not self.instance.pk:
|
||||
self.fields["computers"].queryset = Computer.objects.filter(status="activ")
|
||||
self.fields["monitors"].queryset = Monitor.objects.filter(status="activ")
|
||||
self.fields["peripherals"].queryset = Peripheral.objects.filter(
|
||||
status="activ"
|
||||
)
|
||||
|
||||
|
||||
class DigitalCertificateForm(forms.ModelForm):
|
||||
issue_date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
expiration_date = forms.DateField(
|
||||
input_formats=["%d.%m.%Y", "%Y-%m-%d"],
|
||||
widget=forms.TextInput(
|
||||
attrs={"class": "form-control flatpickr-date", "placeholder": "ZZ.LL.AAAA"}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DigitalCertificate
|
||||
fields = [
|
||||
"certificate_type",
|
||||
"assigned_to",
|
||||
"issue_date",
|
||||
"expiration_date",
|
||||
"observations",
|
||||
"is_canceled",
|
||||
]
|
||||
widgets = {
|
||||
"certificate_type": forms.Select(attrs={"class": "form-select"}),
|
||||
"assigned_to": forms.Select(attrs={"class": "form-select"}),
|
||||
"observations": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
"is_canceled": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# Generated by Django 6.1 on 2026-08-20 09:20
|
||||
|
||||
import datetime
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0003_computer_status_historicalcomputer_status_and_more'),
|
||||
('users', '0002_historicalcompartiment_historicaldirectie_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HandoverProtocol',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(default=datetime.date.today, verbose_name='Data documentului')),
|
||||
('document_number', models.CharField(max_length=100, unique=True, verbose_name='Număr Proces Verbal')),
|
||||
('it_validator', models.CharField(default='Compartimentul IT - Primăria Municipiului Roman', max_length=255, verbose_name='Validat de (IT)')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Observații')),
|
||||
('computers', models.ManyToManyField(blank=True, to='equipment.computer', verbose_name='Computere transferate')),
|
||||
('monitors', models.ManyToManyField(blank=True, to='equipment.monitor', verbose_name='Monitoare transferate')),
|
||||
('new_holder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipment_received', to='users.employee', verbose_name='Primit de')),
|
||||
('peripherals', models.ManyToManyField(blank=True, to='equipment.peripheral', verbose_name='Periferice transferate')),
|
||||
('previous_holder', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipment_handed_over', to='users.employee', verbose_name='Predat de')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Proces Verbal Predare-Primire',
|
||||
'verbose_name_plural': 'Procese Verbale Predare-Primire',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoricalHandoverProtocol',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('date', models.DateField(default=datetime.date.today, verbose_name='Data documentului')),
|
||||
('document_number', models.CharField(db_index=True, max_length=100, verbose_name='Număr Proces Verbal')),
|
||||
('it_validator', models.CharField(default='Compartimentul IT - Primăria Municipiului Roman', max_length=255, verbose_name='Validat de (IT)')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Observații')),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('new_holder', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Primit de')),
|
||||
('previous_holder', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Predat de')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Proces Verbal Predare-Primire',
|
||||
'verbose_name_plural': 'historical Procese Verbale Predare-Primire',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,65 @@
|
||||
# Generated by Django 6.1 on 2026-08-24 09:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0004_handoverprotocol_historicalhandoverprotocol'),
|
||||
('users', '0002_historicalcompartiment_historicaldirectie_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='handoverprotocol',
|
||||
name='scanned_document',
|
||||
field=models.FileField(blank=True, null=True, upload_to='handover_protocols/', verbose_name='Document scanat (semnat)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalhandoverprotocol',
|
||||
name='scanned_document',
|
||||
field=models.TextField(blank=True, max_length=100, null=True, verbose_name='Document scanat (semnat)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='handoverprotocol',
|
||||
name='document_number',
|
||||
field=models.IntegerField(blank=True, unique=True, verbose_name='Număr Proces Verbal'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='handoverprotocol',
|
||||
name='it_validator',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='Întocmit de (IT)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='handoverprotocol',
|
||||
name='new_holder',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipment_received', to='users.employee', verbose_name='Primit de (Primitor)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='handoverprotocol',
|
||||
name='previous_holder',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipment_handed_over', to='users.employee', verbose_name='Predat de (Predător)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalhandoverprotocol',
|
||||
name='document_number',
|
||||
field=models.IntegerField(blank=True, db_index=True, verbose_name='Număr Proces Verbal'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalhandoverprotocol',
|
||||
name='it_validator',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='Întocmit de (IT)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalhandoverprotocol',
|
||||
name='new_holder',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Primit de (Primitor)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalhandoverprotocol',
|
||||
name='previous_holder',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Predat de (Predător)'),
|
||||
),
|
||||
]
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 6.1 on 2026-08-26 11:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0005_handoverprotocol_scanned_document_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='historicalmaintenancerecord',
|
||||
name='next_maintenance_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Următoarea mentenanță planificată'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalperipheral',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='maintenancerecord',
|
||||
name='next_maintenance_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Următoarea mentenanță planificată'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='peripheral',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 6.1 on 2026-08-27 05:46
|
||||
|
||||
import equipment.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0006_historicalmaintenancerecord_next_maintenance_date_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='handoverprotocol',
|
||||
name='scanned_document',
|
||||
field=models.FileField(blank=True, null=True, upload_to='handover_protocols/', validators=[equipment.models.validate_file_size], verbose_name='Document scanat (semnat)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalhandoverprotocol',
|
||||
name='scanned_document',
|
||||
field=models.TextField(blank=True, max_length=100, null=True, validators=[equipment.models.validate_file_size], verbose_name='Document scanat (semnat)'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 06:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0007_alter_handoverprotocol_scanned_document_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='decommissionrecord',
|
||||
options={'verbose_name': 'Proces Verbal de Casare', 'verbose_name_plural': 'Procese Verbale de Casare'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='historicaldecommissionrecord',
|
||||
options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Proces Verbal de Casare', 'verbose_name_plural': 'historical Procese Verbale de Casare'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='decommissionrecord',
|
||||
name='computer',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='decommissionrecord',
|
||||
name='monitor',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='decommissionrecord',
|
||||
name='peripheral',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicaldecommissionrecord',
|
||||
name='computer',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicaldecommissionrecord',
|
||||
name='monitor',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicaldecommissionrecord',
|
||||
name='peripheral',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='decommissionrecord',
|
||||
name='committee_members',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Membrii comisiei de casare (nume și funcție)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='decommissionrecord',
|
||||
name='computers',
|
||||
field=models.ManyToManyField(blank=True, to='equipment.computer', verbose_name='Computere casate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='decommissionrecord',
|
||||
name='monitors',
|
||||
field=models.ManyToManyField(blank=True, to='equipment.monitor', verbose_name='Monitoare casate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='decommissionrecord',
|
||||
name='peripherals',
|
||||
field=models.ManyToManyField(blank=True, to='equipment.peripheral', verbose_name='Periferice casate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicaldecommissionrecord',
|
||||
name='committee_members',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Membrii comisiei de casare (nume și funcție)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='decommissionrecord',
|
||||
name='document_number',
|
||||
field=models.CharField(max_length=100, verbose_name='Număr Proces Verbal'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicaldecommissionrecord',
|
||||
name='document_number',
|
||||
field=models.CharField(max_length=100, verbose_name='Număr Proces Verbal'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 09:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('equipment', '0008_alter_decommissionrecord_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='digitalcertificate',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicaldigitalcertificate',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
]
|
||||
+108
-48
@@ -3,12 +3,21 @@ from datetime import date
|
||||
from simple_history.models import HistoricalRecords
|
||||
from users.models import Employee
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max
|
||||
|
||||
EQUIPMENT_STATUS = [
|
||||
("activ", "Activ"),
|
||||
("casat", "Casat (Decommissioned)"),
|
||||
]
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
def validate_file_size(value):
|
||||
limit_mb = 3
|
||||
if value.size > limit_mb * 1024 * 1024:
|
||||
raise ValidationError(f"Dimensiunea maximă permisă este de {limit_mb} MB.")
|
||||
|
||||
|
||||
class Computer(models.Model):
|
||||
# Unique ID and Entry
|
||||
@@ -56,7 +65,8 @@ class Computer(models.Model):
|
||||
history = HistoricalRecords()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.equipment_code} - {self.oem_name or 'Sistem Asamblat'}"
|
||||
# Format: Cod Echipament | CPU | RAM | Placă de bază
|
||||
return f"{self.equipment_code} | {self.cpu} | {self.ram} | {self.motherboard}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Computer / Laptop"
|
||||
@@ -89,7 +99,8 @@ class Monitor(models.Model):
|
||||
history = HistoricalRecords()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.equipment_code} - {self.name}"
|
||||
# Format: Cod Echipament | Model
|
||||
return f"{self.equipment_code} | {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Monitor"
|
||||
@@ -119,6 +130,7 @@ class Peripheral(models.Model):
|
||||
)
|
||||
name = models.CharField(max_length=255, verbose_name="Nume Echipament")
|
||||
serial_number = models.CharField(max_length=100, verbose_name="Număr Serial")
|
||||
observations = models.TextField(blank=True, null=True, verbose_name="Observații")
|
||||
|
||||
assigned_to = models.ForeignKey(
|
||||
Employee,
|
||||
@@ -135,7 +147,8 @@ class Peripheral(models.Model):
|
||||
history = HistoricalRecords()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.equipment_code} - {self.name} ({self.get_peripheral_type_display()})"
|
||||
# Format: Cod Echipament | Tip | Model
|
||||
return f"{self.equipment_code} | {self.get_peripheral_type_display()} | {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Periferic"
|
||||
@@ -153,6 +166,7 @@ class DigitalCertificate(models.Model):
|
||||
)
|
||||
issue_date = models.DateField(verbose_name="Data emiterii")
|
||||
expiration_date = models.DateField(verbose_name="Data expirării")
|
||||
observations = models.TextField(blank=True, null=True, verbose_name="Observații")
|
||||
is_canceled = models.BooleanField(default=False, verbose_name="Anulat?")
|
||||
|
||||
# Links directly to the employee
|
||||
@@ -180,6 +194,9 @@ class MaintenanceRecord(models.Model):
|
||||
)
|
||||
description = models.TextField(verbose_name="Descriere detaliată")
|
||||
performed_by = models.CharField(max_length=100, verbose_name="Efectuat de")
|
||||
next_maintenance_date = models.DateField(
|
||||
null=True, blank=True, verbose_name="Următoarea mentenanță planificată"
|
||||
)
|
||||
|
||||
# Links to equipment (only one should be populated)
|
||||
computer = models.ForeignKey(
|
||||
@@ -230,62 +247,105 @@ class MaintenanceRecord(models.Model):
|
||||
class DecommissionRecord(models.Model):
|
||||
date = models.DateField(default=date.today, verbose_name="Data casării")
|
||||
document_number = models.CharField(
|
||||
max_length=100, verbose_name="Număr document (Proces Verbal)"
|
||||
max_length=100, verbose_name="Număr Proces Verbal"
|
||||
)
|
||||
reason = models.TextField(verbose_name="Motivul casării (Defect, Uzat moral, etc.)")
|
||||
|
||||
# Links to equipment
|
||||
computer = models.OneToOneField(
|
||||
Computer,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="decommission_log",
|
||||
# Am transformat relațiile în ManyToMany pentru a permite liste de echipamente
|
||||
computers = models.ManyToManyField(
|
||||
Computer, blank=True, verbose_name="Computere casate"
|
||||
)
|
||||
monitor = models.OneToOneField(
|
||||
Monitor,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="decommission_log",
|
||||
monitors = models.ManyToManyField(
|
||||
Monitor, blank=True, verbose_name="Monitoare casate"
|
||||
)
|
||||
peripheral = models.OneToOneField(
|
||||
Peripheral,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
peripherals = models.ManyToManyField(
|
||||
Peripheral, blank=True, verbose_name="Periferice casate"
|
||||
)
|
||||
|
||||
# Comisia de casare (opțional, dar util pentru PDF)
|
||||
committee_members = models.TextField(
|
||||
blank=True,
|
||||
related_name="decommission_log",
|
||||
null=True,
|
||||
verbose_name="Membrii comisiei de casare (nume și funcție)",
|
||||
)
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
def clean(self):
|
||||
equipment_count = sum(
|
||||
[bool(self.computer), bool(self.monitor), bool(self.peripheral)]
|
||||
)
|
||||
if equipment_count != 1:
|
||||
raise ValidationError(
|
||||
"Un proces verbal de casare trebuie să fie legat de exact un singur echipament."
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Automatically update the status of the linked equipment to 'casat'
|
||||
if self.computer:
|
||||
self.computer.status = "casat"
|
||||
self.computer.save()
|
||||
elif self.monitor:
|
||||
self.monitor.status = "casat"
|
||||
self.monitor.save()
|
||||
elif self.peripheral:
|
||||
self.peripheral.status = "casat"
|
||||
self.peripheral.save()
|
||||
# Eliminăm vechea funcție clean() care forța un singur echipament
|
||||
# Eliminăm vechea funcție save() - vom face actualizarea statusului direct din View (ca la predare)
|
||||
|
||||
def __str__(self):
|
||||
return f"Casare {self.document_number} din {self.date}"
|
||||
return f"PV Casare nr. {self.document_number} din {self.date}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Înregistrare Casare"
|
||||
verbose_name_plural = "Înregistrări Casare"
|
||||
verbose_name = "Proces Verbal de Casare"
|
||||
verbose_name_plural = "Procese Verbale de Casare"
|
||||
|
||||
|
||||
class HandoverProtocol(models.Model):
|
||||
date = models.DateField(default=date.today, verbose_name="Data documentului")
|
||||
document_number = models.IntegerField(
|
||||
unique=True, blank=True, verbose_name="Număr Proces Verbal"
|
||||
)
|
||||
|
||||
previous_holder = models.ForeignKey(
|
||||
Employee,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="equipment_handed_over",
|
||||
verbose_name="Predat de (Predător)",
|
||||
)
|
||||
new_holder = models.ForeignKey(
|
||||
Employee,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="equipment_received",
|
||||
verbose_name="Primit de (Primitor)",
|
||||
)
|
||||
|
||||
# We will populate this automatically in the admin
|
||||
it_validator = models.CharField(
|
||||
max_length=255, blank=True, verbose_name="Întocmit de (IT)"
|
||||
)
|
||||
|
||||
computers = models.ManyToManyField(
|
||||
Computer, blank=True, verbose_name="Computere transferate"
|
||||
)
|
||||
monitors = models.ManyToManyField(
|
||||
Monitor, blank=True, verbose_name="Monitoare transferate"
|
||||
)
|
||||
peripherals = models.ManyToManyField(
|
||||
Peripheral, blank=True, verbose_name="Periferice transferate"
|
||||
)
|
||||
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Observații")
|
||||
|
||||
# Field to attach the signed document
|
||||
scanned_document = models.FileField(
|
||||
upload_to="handover_protocols/",
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[validate_file_size],
|
||||
verbose_name="Document scanat (semnat)",
|
||||
)
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.document_number:
|
||||
max_num = HandoverProtocol.objects.aggregate(Max("document_number"))[
|
||||
"document_number__max"
|
||||
]
|
||||
self.document_number = (
|
||||
(max_num + 1) if (max_num and max_num >= 1000) else 1000
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"PV Predare-Primire nr. {self.document_number} din {self.date}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Proces Verbal Predare-Primire"
|
||||
verbose_name_plural = "Procese Verbale Predare-Primire"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold text-info">Certificate Digitale (Semnături)</h2>
|
||||
<p class="text-muted">Gestiunea token-urilor și a semnăturilor în cloud.</p>
|
||||
</div>
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-info fw-bold shadow-sm"
|
||||
hx-get="{% url 'equipment:certificate_create' %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#form-modal">
|
||||
➕ Adaugă Certificat
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-info">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Titular</th>
|
||||
<th>Tip</th>
|
||||
<th>Dată Emitere</th>
|
||||
<th>Dată Expirare</th>
|
||||
<th>Status</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="certificate-table-body">
|
||||
{% for cert in certificates %}
|
||||
<tr>
|
||||
<td class="align-middle fw-bold">👤 {{ cert.assigned_to.name }}</td>
|
||||
<td class="align-middle">
|
||||
{% if cert.certificate_type == 'usb' %}🔑 USB Token{% else %}☁️ Cloud{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">{{ cert.issue_date|date:"d.m.Y" }}</td>
|
||||
<td class="align-middle fw-bold">{{ cert.expiration_date|date:"d.m.Y" }}</td>
|
||||
<td class="align-middle">
|
||||
<span class="badge bg-{{ cert.status_color }}">{{ cert.status_text }}</span>
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
hx-get="{% url 'equipment:certificate_edit' cert.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
{% endif %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">Nu există certificate digitale înregistrate.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content" id="modal-content-area"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,104 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Am adăugat clasa 'text-center' aici pentru a centra titlul -->
|
||||
<div class="mb-4 text-center">
|
||||
<h2 class="fw-bold">Panou de Control</h2>
|
||||
<p class="text-muted">Privire de ansamblu asupra infrastructurii I.T.</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Coloana 1: Sumar Inventar -->
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm border-primary">
|
||||
<div class="card-header bg-primary text-white fw-bold">
|
||||
ℹ️ Sumar & Echipamente Libere
|
||||
</div>
|
||||
<!-- Am adăugat înălțime fixă (max-height) și overflow-y pentru scroll -->
|
||||
<div class="card-body" style="max-height: 450px; overflow-y: auto;">
|
||||
<p class="mb-2">Total Calculatoare: <strong>{{ total_computers }}</strong></p>
|
||||
<hr>
|
||||
<h6 class="fw-bold text-danger mt-3">Calculatoare Neasignate:</h6>
|
||||
<ul class="list-unstyled mb-3">
|
||||
{% for pc in unassigned_computers %}
|
||||
<li>💻 {{ pc.equipment_code }} - <small>{{ pc.cpu }} / {{ pc.ram }}</small></li>
|
||||
{% empty %}
|
||||
<li><small class="text-muted">Toate calculatoarele sunt asignate.</small></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h6 class="fw-bold text-danger mt-3">Monitoare Neasignate:</h6>
|
||||
<ul class="list-unstyled">
|
||||
{% for mon in unassigned_monitors %}
|
||||
<li>🖥️ {{ mon.equipment_code }} - <small>{{ mon.screen_diagonal }}</small></li>
|
||||
{% empty %}
|
||||
<li><small class="text-muted">Toate monitoarele sunt asignate.</small></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coloana 2: Acțiuni Executate (Istoric) -->
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm border-success">
|
||||
<div class="card-header bg-success text-white fw-bold">
|
||||
🔄 Ultimele Acțiuni
|
||||
</div>
|
||||
<!-- Scroll adăugat aici -->
|
||||
<div class="card-body p-0" style="max-height: 450px; overflow-y: auto;">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for record in activity_log %}
|
||||
<li class="list-group-item">
|
||||
<small class="text-muted d-block">{{ record.history_date|date:"d.m.Y H:i" }}</small>
|
||||
<strong>{{ record.history_user.username|default:"Sistem" }}</strong> a
|
||||
{% if record.history_type == '+' %}
|
||||
<span class="text-success">adăugat</span>
|
||||
{% elif record.history_type == '~' %}
|
||||
<span class="text-warning text-dark">modificat</span>
|
||||
{% elif record.history_type == '-' %}
|
||||
<span class="text-danger">șters</span>
|
||||
{% endif %}
|
||||
<br>
|
||||
<span class="text-dark">{{ record.equipment_code }}</span>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-muted text-center py-4">Nu există acțiuni recente.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coloana 3: Mentenanțe Efectuate -->
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm border-warning">
|
||||
<div class="card-header bg-warning text-dark fw-bold">
|
||||
🛠️ Ultimele Mentenanțe
|
||||
</div>
|
||||
<!-- Scroll adăugat aici -->
|
||||
<div class="card-body p-0" style="max-height: 450px; overflow-y: auto;">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for maint in recent_maintenance %}
|
||||
<li class="list-group-item">
|
||||
<small class="text-muted d-block">{{ maint.date|date:"d.m.Y" }} | Efectuat de: {{ maint.performed_by }}</small>
|
||||
<strong>{{ maint.operation_type }}</strong>
|
||||
<br>
|
||||
<span class="text-dark small">
|
||||
Pentru:
|
||||
{% if maint.computer %}💻 {{ maint.computer.equipment_code }}{% endif %}
|
||||
{% if maint.monitor %}🖥️ {{ maint.monitor.equipment_code }}{% endif %}
|
||||
{% if maint.peripheral %}🖨️ {{ maint.peripheral.equipment_code }}{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-muted text-center py-4">Nu există mentenanțe înregistrate.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,79 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold text-danger">Procese Verbale de Casare</h2>
|
||||
<p class="text-muted">Evidența echipamentelor scoase din uz.</p>
|
||||
</div>
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-danger fw-bold shadow-sm"
|
||||
hx-get="{% url 'equipment:decommission_create' %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#form-modal">
|
||||
➕ Adaugă PV Casare
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-danger">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Număr PV</th>
|
||||
<th>Data</th>
|
||||
<th>Motiv</th>
|
||||
<th>Echipamente Casate</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="decommission-table-body">
|
||||
{% for record in records %}
|
||||
<tr>
|
||||
<td class="align-middle fw-bold"># {{ record.document_number }}</td>
|
||||
<td class="align-middle">{{ record.date|date:"d.m.Y" }}</td>
|
||||
<td class="align-middle text-truncate" style="max-width: 250px;" title="{{ record.reason }}">{{ record.reason }}</td>
|
||||
<td class="align-middle">
|
||||
<small>
|
||||
💻 Computere: <strong>{{ record.computers.count }}</strong><br>
|
||||
🖥️ Monitoare: <strong>{{ record.monitors.count }}</strong><br>
|
||||
🖨️ Periferice: <strong>{{ record.peripherals.count }}</strong>
|
||||
</small>
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<a href="{% url 'equipment:decommission_pdf' record.id %}"
|
||||
class="btn btn-sm btn-outline-primary me-1"
|
||||
target="_blank"
|
||||
title="Descarcă PV Casare">
|
||||
📄 PDF
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
hx-get="{% url 'equipment:decommission_edit' record.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">Nu există procese verbale de casare înregistrate.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content" id="modal-content-area">
|
||||
<!-- Formularul se va încărca aici prin HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Inventar Echipamente Active</h2>
|
||||
|
||||
<div class="d-flex gap-3 w-50 justify-content-end">
|
||||
<!-- HTMX Live Search Input -->
|
||||
<input class="form-control w-50" type="search"
|
||||
name="search" placeholder="Caută cod, nume sau angajat..."
|
||||
hx-get="{% url 'equipment:equipment_list' %}"
|
||||
hx-trigger="input changed delay:400ms, search"
|
||||
hx-target="#equipment-tables-container">
|
||||
|
||||
<!-- Dropdown to add different types of equipment -->
|
||||
<div class="dropdown">
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-success fw-bold dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
+ Adaugă Echipament
|
||||
</button>
|
||||
{% endif %}
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'equipment:computer_create' %}" hx-target="#modal-content-area" data-bs-toggle="modal" data-bs-target="#form-modal">💻 Computer / Laptop</a></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'equipment:monitor_create' %}" hx-target="#modal-content-area" data-bs-toggle="modal" data-bs-target="#form-modal">🖥️ Monitor</a></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'equipment:peripheral_create' %}" hx-target="#modal-content-area" data-bs-toggle="modal" data-bs-target="#form-modal">🖨️ Periferic</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="{% url 'equipment:export_inventory' %}" class="btn btn-success fw-bold shadow-sm me-2">
|
||||
📊 Exportă Inventar (Excel)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Added hx-trigger="equipmentListChanged from:body" to refresh the tables upon save -->
|
||||
<div id="equipment-tables-container" hx-get="{% url 'equipment:equipment_list' %}" hx-trigger="equipmentListChanged from:body">
|
||||
{% include 'equipment/partials/equipment_tables.html' %}
|
||||
</div>
|
||||
|
||||
<!-- Empty Bootstrap Modal Container -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content" id="modal-content-area">
|
||||
<!-- Form will be injected here by HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script to automatically close the modal when HTMX triggers 'closeModal' -->
|
||||
<script>
|
||||
document.body.addEventListener('closeModal', function() {
|
||||
var modalEl = document.getElementById('form-modal');
|
||||
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,87 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Procese Verbale (Predare-Primire)</h2>
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-success fw-bold"
|
||||
hx-get="{% url 'equipment:handover_create' %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#form-modal">
|
||||
+ Adaugă Proces Verbal
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Nr. / Dată</th>
|
||||
<th>Predat de</th>
|
||||
<th>Primit de</th>
|
||||
<th>Echipamente transferate</th>
|
||||
<th>Document Scanat</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for protocol in protocols %}
|
||||
<tr>
|
||||
<td class="align-middle fw-bold">Nr. {{ protocol.document_number }}<br><small class="text-muted">{{ protocol.date|date:"d.m.Y" }}</small></td>
|
||||
<td class="align-middle text-danger">{{ protocol.previous_holder.name|default:"-" }}</td>
|
||||
<td class="align-middle text-success">{{ protocol.new_holder.name|default:"-" }}</td>
|
||||
<td class="align-middle">
|
||||
<small>
|
||||
{% for pc in protocol.computers.all %}💻 {{ pc.equipment_code }}<br>{% endfor %}
|
||||
{% for mon in protocol.monitors.all %}🖥️ {{ mon.equipment_code }}<br>{% endfor %}
|
||||
{% for peri in protocol.peripherals.all %}🖨️ {{ peri.equipment_code }}<br>{% endfor %}
|
||||
</small>
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
{% if protocol.scanned_document %}
|
||||
<a href="{{ protocol.scanned_document.url }}" target="_blank" class="btn btn-sm btn-outline-danger">📥 Vezi PDF</a>
|
||||
{% else %}
|
||||
<span class="text-muted small">Fără document</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<!-- Butonul nou pentru Generare PDF -->
|
||||
<a href="{% url 'equipment:handover_pdf' protocol.id %}"
|
||||
class="btn btn-sm btn-outline-primary me-1"
|
||||
target="_blank"
|
||||
title="Descarcă Procesul Verbal în format PDF">
|
||||
📄 PDF
|
||||
</a>
|
||||
|
||||
<!-- Butonul existent de Editare -->
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
hx-get="{% url 'equipment:handover_edit' protocol.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4 text-muted">Nu există procese verbale înregistrate.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container Modal -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content" id="modal-content-area">
|
||||
<!-- Formularul se va încărca aici prin HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Fișă Tehnică Monitor: {{ monitor.equipment_code }}</h2>
|
||||
<a href="{% url 'equipment:equipment_list' %}" class="btn btn-outline-secondary">⬅️ Înapoi la Inventar</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-white fw-bold">Specificații Curente</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item"><strong>Model:</strong> {{ monitor.name }}</li>
|
||||
<li class="list-group-item"><strong>Serial (S/N):</strong> {{ monitor.serial_number }}</li>
|
||||
<li class="list-group-item"><strong>Diagonală:</strong> {{ monitor.screen_diagonal }}</li>
|
||||
<li class="list-group-item"><strong>Utilizator Curent:</strong> {{ monitor.assigned_to.name|default:"Neasignat" }}</li>
|
||||
<li class="list-group-item"><strong>Observații:</strong> {{ monitor.observations|default:"-" }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-warning text-dark fw-bold d-flex justify-content-between align-items-center">
|
||||
<span>Jurnal Mentenanță</span>
|
||||
<button class="btn btn-sm btn-dark" hx-get="{% url 'equipment:monitor_maintenance_create' monitor.id %}" hx-target="#modal-content-area" data-bs-toggle="modal" data-bs-target="#form-modal">+ Adaugă Intervenție</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="table-light"><tr><th>Dată</th><th>Tip Operațiune</th><th>Efectuat de</th></tr></thead>
|
||||
<tbody>
|
||||
{% for log in maintenance_logs %}
|
||||
<tr><td>{{ log.date|date:"d.m.Y" }}</td><td>{{ log.operation_type }}<br><small class="text-muted">{{ log.description }}</small></td><td>{{ log.performed_by }}</td></tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3" class="text-center py-3">Nu există intervenții de mentenanță.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold mt-4 mb-3 border-bottom pb-2">Istoric Posesie (Custodie)</h5>
|
||||
<ul class="list-group mb-4 shadow-sm">
|
||||
{% for record in ownership_history %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>👤 {{ record.owner }}</strong>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
{{ record.start_date|date:"d.m.Y" }}
|
||||
<span class="mx-1">→</span>
|
||||
{% if record.end_date %}
|
||||
{{ record.end_date|date:"d.m.Y" }}
|
||||
{% else %}
|
||||
<span class="badge bg-success">Prezent</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-muted text-center">Fără istoric de posesie.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true"><div class="modal-dialog"><div class="modal-content" id="modal-content-area"></div></div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,79 @@
|
||||
<div class="modal-header bg-info text-dark">
|
||||
<h5 class="modal-title fw-bold">
|
||||
{% if certificate %}Editează Certificat{% else %}Adaugă Certificat Nou{% endif %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form {% if certificate %} hx-post="{% url 'equipment:certificate_edit' certificate.id %}" {% else %} hx-post="{% url 'equipment:certificate_create' %}" {% endif %}
|
||||
hx-target="#modal-content-area">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body bg-light">
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger p-2 mb-3">
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}<li><strong>{{ field|title }}:</strong> {{ error }}</li>{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Titular Certificat</label>
|
||||
{{ form.assigned_to }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Tip Certificat</label>
|
||||
{{ form.certificate_type }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Data Emiterii</label>
|
||||
{{ form.issue_date }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold text-danger">Data Expirării</label>
|
||||
{{ form.expiration_date }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Observații</label>
|
||||
{{ form.observations }}
|
||||
</div>
|
||||
|
||||
{% if certificate %}
|
||||
<div class="col-md-12 mt-2">
|
||||
<div class="form-check form-switch fs-5">
|
||||
{{ form.is_canceled }}
|
||||
<label class="form-check-label text-danger fw-bold ms-2" for="{{ form.is_canceled.id_for_label }}">
|
||||
Marchează certificatul ca ANULAT / REVOCAT
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-info fw-bold">Salvează Certificat</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Încărcăm biblioteca Flatpickr și pachetul de limba română -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://npmcdn.com/flatpickr/dist/l10n/ro.js"></script>
|
||||
|
||||
<!-- <script>
|
||||
// Așteptăm 100ms pentru ca HTMX să descarce complet librăria înainte de inițializare
|
||||
setTimeout(function() {
|
||||
flatpickr("#cert-issue-picker, #cert-expiry-picker", {
|
||||
dateFormat: "d.m.Y",
|
||||
locale: "ro",
|
||||
allowInput: true
|
||||
});
|
||||
}, 100);
|
||||
</script> -->
|
||||
@@ -0,0 +1,86 @@
|
||||
<div class="modal-header bg-dark text-white">
|
||||
<h5 class="modal-title">{% if computer %}Editează Computer{% else %}Adaugă Computer Nou{% endif %}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form hx-post="{% if computer %}{% url 'equipment:computer_edit' computer.pk %}{% else %}{% url 'equipment:computer_create' %}{% endif %}"
|
||||
hx-target="#modal-content-area">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<!-- Bloc pentru afișarea erorilor -->
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger p-2 mb-3">
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li><strong>{{ field|title }}:</strong> {{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h6 class="text-primary border-bottom pb-1 mb-3">Informații Generale</h6>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Cod Echipament</label>
|
||||
{{ form.equipment_code }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Status</label>
|
||||
{{ form.status }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Data Intrării</label>
|
||||
{{ form.entry_date }}
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Atribuit Către (Custodie)</label>
|
||||
{{ form.assigned_to }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="text-primary border-bottom pb-1 mb-3">Specificații Hardware & Software</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Producător (OEM)</label>
|
||||
{{ form.oem_name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Placă de bază</label>
|
||||
{{ form.motherboard }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Procesor (CPU)</label>
|
||||
{{ form.cpu }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Memorie RAM</label>
|
||||
{{ form.ram }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Stocare (HDD/SSD)</label>
|
||||
{{ form.storage }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Adresă MAC</label>
|
||||
{{ form.mac_address }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Licență OS</label>
|
||||
{{ form.os_license }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Licențe adiționale</label>
|
||||
{{ form.additional_licenses }}
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Observații</label>
|
||||
{{ form.observations }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-primary">Salvează Echipament</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,76 @@
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title fw-bold">
|
||||
{% if record %}Editează PV Casare Nr. {{ record.document_number }}{% else %}Adaugă PV Casare Nou{% endif %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form {% if record %} hx-post="{% url 'equipment:decommission_edit' record.id %}" {% else %} hx-post="{% url 'equipment:decommission_create' %}" {% endif %}
|
||||
hx-target="#modal-content-area">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="modal-body bg-light">
|
||||
<div class="alert alert-warning mb-4">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i> Atenție: Echipamentele selectate aici vor fi trecute automat cu statusul <strong>"Casat"</strong> și vor fi scoase din gestiunea angajaților!
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Număr Proces Verbal</label>
|
||||
{{ form.document_number }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Data Casării</label>
|
||||
{{ form.date }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Motivul Casării</label>
|
||||
{{ form.reason }}
|
||||
<div class="form-text">Ex: Uzaură fizică și morală, defect iremediabil etc.</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<h6 class="fw-bold text-danger mb-3">Selecție Echipamente pentru Casare:</h6>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-bold">Computere</label>
|
||||
{{ form.computers }}
|
||||
<div class="form-text">Doar echipamentele ACTIVE apar în listă. Folosiți CTRL pt. selecție multiplă.</div>
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-bold">Monitoare</label>
|
||||
{{ form.monitors }}
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-bold">Periferice</label>
|
||||
{{ form.peripherals }}
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Membrii Comisiei de Casare</label>
|
||||
{{ form.committee_members }}
|
||||
<div class="form-text">Nume și funcție (pentru generarea PDF-ului ulterior).</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-danger fw-bold">Casare Echipamente</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Flatpickr pentru calendar -->
|
||||
<!-- <script>
|
||||
if (typeof flatpickr !== 'undefined') {
|
||||
flatpickr("#decommission-date-picker", {
|
||||
dateFormat: "d.m.Y",
|
||||
locale: "ro",
|
||||
allowInput: true
|
||||
});
|
||||
}
|
||||
</script> -->
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-bold">Computere</label>
|
||||
{{ form.computers }}
|
||||
<div class="form-text">Folosiți CTRL pt. selecție multiplă.</div>
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-bold">Monitoare</label>
|
||||
{{ form.monitors }}
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-bold">Periferice</label>
|
||||
{{ form.peripherals }}
|
||||
</div>
|
||||
@@ -0,0 +1,150 @@
|
||||
<ul class="nav nav-tabs mb-3" id="equipmentTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active fw-bold" id="computers-tab" data-bs-toggle="tab" data-bs-target="#computers" type="button" role="tab">
|
||||
Computere / Laptopuri <span class="badge bg-secondary ms-1">{{ computers.count }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold" id="monitors-tab" data-bs-toggle="tab" data-bs-target="#monitors" type="button" role="tab">
|
||||
Monitoare <span class="badge bg-secondary ms-1">{{ monitors.count }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link fw-bold" id="peripherals-tab" data-bs-toggle="tab" data-bs-target="#peripherals" type="button" role="tab">
|
||||
Periferice <span class="badge bg-secondary ms-1">{{ peripherals.count }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content card shadow-sm p-0 border-top-0" id="equipmentTabsContent">
|
||||
|
||||
<!-- COMPUTERS TAB -->
|
||||
<div class="tab-pane fade show active" id="computers" role="tabpanel">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Cod</th>
|
||||
<th>Detalii Sistem</th>
|
||||
<th>Asignat Către</th>
|
||||
<th>Data Intrării</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pc in computers %}
|
||||
<tr>
|
||||
<td class="align-middle fw-medium">{{ pc.equipment_code }}</td>
|
||||
<td>
|
||||
<!--<strong>{{ pc.oem_name|default:"Sistem Asamblat" }}</strong><br>-->
|
||||
<small class="text-muted">
|
||||
<span class="d-block">{{ pc.cpu }} | {{ pc.ram }}</span>
|
||||
<span class="d-block">{{ pc.motherboard }} | {{ pc.mac_address }}</span>
|
||||
</small>
|
||||
</td>
|
||||
<td class="align-middle">{{ pc.assigned_to.name|default:"<span class='text-danger'>Neasignat</span>" }}</td>
|
||||
<td class="align-middle">{{ pc.entry_date|date:"d.m.Y" }}</td>
|
||||
<td class="align-middle text-center">
|
||||
<!-- Technical Sheet Button -->
|
||||
<a href="{% url 'equipment:computer_sheet' pc.id %}" class="btn btn-sm btn-outline-info me-1">
|
||||
📄 Fișă Tehnică
|
||||
</a>
|
||||
<!-- Edit Button -->
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
hx-get="{% url 'equipment:computer_edit' pc.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal" data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center py-4">Nu există computere active.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- MONITORS TAB -->
|
||||
<div class="tab-pane fade" id="monitors" role="tabpanel">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Cod</th>
|
||||
<th>Model & S/N</th>
|
||||
<th>Asignat Către</th>
|
||||
<th>Data Intrării</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for monitor in monitors %}
|
||||
<tr>
|
||||
<td class="align-middle fw-medium">{{ monitor.equipment_code }}</td>
|
||||
<td>
|
||||
<strong>{{ monitor.name }} ({{ monitor.screen_diagonal }})</strong><br>
|
||||
<small class="text-muted">S/N: {{ monitor.serial_number }}</small>
|
||||
</td>
|
||||
<td class="align-middle">{{ monitor.assigned_to.name|default:"<span class='text-danger'>Neasignat</span>" }}</td>
|
||||
<td class="align-middle">{{ monitor.entry_date|date:"d.m.Y" }}</td>
|
||||
<td class="align-middle text-center">
|
||||
<a href="{% url 'equipment:monitor_sheet' monitor.id %}" class="btn btn-sm btn-outline-info me-1">📄 Fișă</a>
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
hx-get="{% url 'equipment:monitor_edit' monitor.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal" data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5" class="text-center py-4">Nu există monitoare active.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- PERIPHERALS TAB -->
|
||||
<div class="tab-pane fade" id="peripherals" role="tabpanel">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Cod</th>
|
||||
<th>Tip & Model</th>
|
||||
<th>Asignat Către</th>
|
||||
<th>Data Intrării</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for peripheral in peripherals %}
|
||||
<tr>
|
||||
<td class="align-middle fw-medium">{{ peripheral.equipment_code }}</td>
|
||||
<td>
|
||||
<strong>{{ peripheral.get_peripheral_type_display }}: {{ peripheral.name }}</strong><br>
|
||||
<small class="text-muted">S/N: {{ peripheral.serial_number }}</small>
|
||||
</td>
|
||||
<td class="align-middle">{{ peripheral.assigned_to.name|default:"<span class='text-danger'>Neasignat</span>" }}</td>
|
||||
<td class="align-middle">{{ peripheral.entry_date|date:"d.m.Y" }}</td>
|
||||
<td class="align-middle text-center">
|
||||
<a href="{% url 'equipment:peripheral_sheet' peripheral.id %}" class="btn btn-sm btn-outline-info me-1">📄 Fișă</a>
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
hx-get="{% url 'equipment:peripheral_edit' peripheral.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal" data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5" class="text-center py-4">Nu există periferice active.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
<div class="modal-header bg-dark text-white">
|
||||
<h5 class="modal-title fw-bold">
|
||||
{% if protocol %}Editează Proces Verbal Nr. {{ protocol.document_number }}{% else %}Adaugă Proces Verbal Nou{% endif %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- ATENȚIE: hx-encoding este OBLIGATORIU pentru încărcarea de fișiere (PDF) prin HTMX -->
|
||||
<form {% if protocol %} hx-post="{% url 'equipment:handover_edit' protocol.id %}" {% else %} hx-post="{% url 'equipment:handover_create' %}" {% endif %}
|
||||
hx-target="#modal-content-area"
|
||||
hx-encoding="multipart/form-data"
|
||||
enctype="multipart/form-data">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="modal-body bg-light">
|
||||
<!-- Bloc afișare erori (ex: PDF prea mare) -->
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger p-2 mb-3">
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}<li><strong>{{ field|title }}:</strong> {{ error }}</li>{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold text-danger">Document Scanat (Semnat)</label>
|
||||
{{ form.scanned_document }}
|
||||
<div class="form-text">Încărcați varianta semnată (max. 3 MB).</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Dată</label>
|
||||
{{ form.date }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Predat de</label>
|
||||
{{ form.previous_holder }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Primit de</label>
|
||||
{{ form.new_holder }}
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Aici se vor încărca dinamic listele verticale prin HTMX -->
|
||||
<div id="equipment-lists" class="row">
|
||||
{% include 'equipment/partials/equipment_dropdowns.html' %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mt-2">
|
||||
<label class="form-label fw-bold">Observații</label>
|
||||
{{ form.notes }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-success fw-bold">Salvează Documentul</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Încărcăm biblioteca Flatpickr pentru calendarul în limba română -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://npmcdn.com/flatpickr/dist/l10n/ro.js"></script>
|
||||
<script>
|
||||
flatpickr("#date-picker", {
|
||||
dateFormat: "d.m.Y",
|
||||
locale: "ro",
|
||||
allowInput: true
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="modal-header bg-warning text-dark">
|
||||
<h5 class="modal-title fw-bold">Adaugă Mentenanță: {{ equipment_title }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form hx-post="{{ post_url }}" hx-target="#modal-content-area">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Data intervenției</label>
|
||||
{{ form.date }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Efectuat de</label>
|
||||
{{ form.performed_by }}
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Tip operațiune</label>
|
||||
{{ form.operation_type }}
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Descriere detaliată</label>
|
||||
{{ form.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-warning fw-bold text-dark">Salvează Intervenția</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="modal-header bg-dark text-white">
|
||||
<h5 class="modal-title">{% if monitor %}Editează Monitor{% else %}Adaugă Monitor Nou{% endif %}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form hx-post="{% if monitor %}{% url 'equipment:monitor_edit' monitor.pk %}{% else %}{% url 'equipment:monitor_create' %}{% endif %}" hx-target="#modal-content-area">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<!-- Bloc pentru afișarea erorilor -->
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger p-2 mb-3">
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li><strong>{{ field|title }}:</strong> {{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Cod Echipament</label>{{ form.equipment_code }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Status</label>{{ form.status }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Data Intrării</label>{{ form.entry_date }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Atribuit Către</label>{{ form.assigned_to }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Denumire / Model</label>{{ form.name }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Număr Serial (S/N)</label>{{ form.serial_number }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Diagonală</label>{{ form.screen_diagonal }}</div>
|
||||
<div class="col-md-12"><label class="form-label fw-bold">Observații</label>{{ form.observations }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-primary">Salvează</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="modal-header bg-dark text-white">
|
||||
<h5 class="modal-title">{% if peripheral %}Editează Periferic{% else %}Adaugă Periferic Nou{% endif %}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form hx-post="{% if peripheral %}{% url 'equipment:peripheral_edit' peripheral.pk %}{% else %}{% url 'equipment:peripheral_create' %}{% endif %}" hx-target="#modal-content-area">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<!-- Bloc pentru afișarea erorilor -->
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger p-2 mb-3">
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li><strong>{{ field|title }}:</strong> {{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Cod Echipament</label>{{ form.equipment_code }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Status</label>{{ form.status }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Data Intrării</label>{{ form.entry_date }}</div>
|
||||
<div class="col-md-6"><label class="form-label fw-bold">Atribuit Către</label>{{ form.assigned_to }}</div>
|
||||
<div class="col-md-4"><label class="form-label fw-bold">Tip Periferic</label>{{ form.peripheral_type }}</div>
|
||||
<div class="col-md-8"><label class="form-label fw-bold">Denumire / Model</label>{{ form.name }}</div>
|
||||
<div class="col-md-12"><label class="form-label fw-bold">Număr Serial (S/N)</label>{{ form.serial_number }}</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Observații</label>
|
||||
{{ form.observations }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-primary">Salvează</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'LiberationSerif';
|
||||
src: url('{{ font_regular }}');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'LiberationSerif';
|
||||
src: url('{{ font_bold }}');
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 1.5cm;
|
||||
margin-bottom: 2.5cm; /* Lăsăm loc pentru footer */
|
||||
|
||||
/* Așa forțăm xhtml2pdf să lipească footer-ul de marginea de jos pe fiecare pagină */
|
||||
@frame footer_frame {
|
||||
-pdf-frame-content: footer_content;
|
||||
left: 1.5cm;
|
||||
right: 1.5cm;
|
||||
bottom: 1cm;
|
||||
height: 1.5cm;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'LiberationSerif', serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.title { text-align: center; font-size: 16px; font-weight: bold; margin-top: 15px; margin-bottom: 20px; }
|
||||
|
||||
ul { margin-top: 5px; margin-bottom: 10px; }
|
||||
li { margin-bottom: 4px; text-align: justify; }
|
||||
p { margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- 1. FOOTER-UL (Capturat de rama "footer_frame" setată în CSS) -->
|
||||
<div id="footer_content">
|
||||
<table style="width: 100%; border-top: 1px dashed #ccc; padding-top: 5px; font-size: 10px; color: #555;">
|
||||
<tr>
|
||||
<td style="text-align: left; width: 70%; border: none;">
|
||||
Generat de E.U.M.I. v.4.0.0 • Aplicație dezvoltată de Tololoi Cătălin (catalin@x3c.ro)
|
||||
</td>
|
||||
<td style="text-align: right; width: 30%; border: none;">
|
||||
pag. <pdf:pagenumber> din <pdf:pagecount>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 2. ANTETUL -->
|
||||
<table style="width: 100%; border: none;">
|
||||
<tr>
|
||||
<td style="width: 15%; text-align: left; vertical-align: middle; border: none;">
|
||||
{% if logo_path %}
|
||||
<img src="{{ logo_path }}" width="50">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="width: 85%; text-align: right; vertical-align: middle; border: none;">
|
||||
<strong>U.A.T. Municipiul Roman</strong><br>
|
||||
Compartiment administrare infrastructură I.T. și baze de date
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Linia de departajare -->
|
||||
<hr style="border: 0; border-top: 1px solid #000; margin-top: 5px; margin-bottom: 15px;">
|
||||
|
||||
<!-- 3. TITLUL -->
|
||||
<div class="title">
|
||||
PROCES VERBAL DE CASARE (SCOATERE DIN FUNCȚIUNE)<br>
|
||||
Nr. {{ record.document_number }} din {{ record.date|date:"d.m.Y" }}
|
||||
</div>
|
||||
|
||||
<!-- 4. CONȚINUTUL -->
|
||||
<p>Prezentul proces verbal s-a încheiat astăzi, <strong>{{ record.date|date:"d F Y"|lower }}</strong>, pentru scoaterea din funcțiune și casarea următoarelor echipamente de calcul și periferice:</p>
|
||||
|
||||
<ul>
|
||||
{% for pc in record.computers.all %}
|
||||
<li>
|
||||
unitate centrală, nr. inventar {{ pc.equipment_code }}, caracteristici:
|
||||
{{ pc.cpu|default:"-" }} | {{ pc.ram|default:"-" }} | {{ pc.motherboard|default:"-" }}.
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% for mon in record.monitors.all %}
|
||||
<li>
|
||||
monitor, nr. inventar {{ mon.equipment_code }}, model: {{ mon.name|default:"-" }}.
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% for per in record.peripherals.all %}
|
||||
<li>
|
||||
periferic ({{ per.get_peripheral_type_display }}), nr. inventar {{ per.equipment_code }}, model: {{ per.name|default:"-" }}.
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<p><strong>Motivul casării:</strong> {{ record.reason }}</p>
|
||||
|
||||
<!-- 5. SEMNĂTURILE -->
|
||||
<table style="width: 100%; border: none; margin-top: 40px; text-align: center;">
|
||||
<tr>
|
||||
<td style="width: 50%; border: none; vertical-align: top;">
|
||||
<strong>Membrii Comisiei de Casare:</strong><br><br>
|
||||
{{ record.committee_members|linebreaksbr }}<br><br><br>
|
||||
Semnături:<br>
|
||||
...............................................<br><br>
|
||||
...............................................
|
||||
</td>
|
||||
<td style="width: 50%; border: none; vertical-align: top;">
|
||||
<strong>Întocmit de,</strong><br><br>
|
||||
{{ generated_by }}<br><br><br>
|
||||
Semnătură:<br>
|
||||
...............................................
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'LiberationSerif';
|
||||
src: url('{{ font_regular }}');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'LiberationSerif';
|
||||
src: url('{{ font_bold }}');
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 1.5cm;
|
||||
margin-bottom: 2.5cm; /* Lăsăm loc pentru footer */
|
||||
|
||||
/* Așa forțăm xhtml2pdf să lipească footer-ul de marginea de jos pe fiecare pagină */
|
||||
@frame footer_frame {
|
||||
-pdf-frame-content: footer_content;
|
||||
left: 1.5cm;
|
||||
right: 1.5cm;
|
||||
bottom: 1cm;
|
||||
height: 1.5cm;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'LiberationSerif', serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.title { text-align: center; font-size: 16px; font-weight: bold; margin-top: 15px; margin-bottom: 20px; }
|
||||
|
||||
ul { margin-top: 5px; margin-bottom: 10px; }
|
||||
li { margin-bottom: 4px; text-align: justify; }
|
||||
p { margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- 1. FOOTER-UL (Capturat de rama "footer_frame" setată în CSS) -->
|
||||
<div id="footer_content">
|
||||
<table style="width: 100%; border-top: 1px dashed #ccc; padding-top: 5px; font-size: 10px; color: #555;">
|
||||
<tr>
|
||||
<td style="text-align: left; width: 70%; border: none;">
|
||||
Generat de E.U.M.I. v.4.0.0 • Aplicație dezvoltată de Tololoi Cătălin (catalin@x3c.ro)
|
||||
</td>
|
||||
<td style="text-align: right; width: 30%; border: none;">
|
||||
pag. <pdf:pagenumber> din <pdf:pagecount>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 2. ANTETUL -->
|
||||
<table style="width: 100%; border: none;">
|
||||
<tr>
|
||||
<td style="width: 15%; text-align: left; vertical-align: middle; border: none;">
|
||||
{% if logo_path %}
|
||||
<img src="{{ logo_path }}" width="50">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="width: 85%; text-align: right; vertical-align: middle; border: none;">
|
||||
<strong>U.A.T. Municipiul Roman</strong><br>
|
||||
Compartiment administrare infrastructură I.T. și baze de date
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Linia de departajare -->
|
||||
<hr style="border: 0; border-top: 1px solid #000; margin-top: 5px; margin-bottom: 15px;">
|
||||
|
||||
<!-- 3. TITLUL -->
|
||||
<div class="title">
|
||||
PROCES VERBAL DE PREDARE – PRIMIRE<br>
|
||||
Nr. {{ protocol.document_number }}
|
||||
</div>
|
||||
|
||||
<!-- 4. CONȚINUTUL -->
|
||||
<p>Încheiat astăzi: <strong>{{ protocol.date|date:"d F Y"|lower }}</strong>, între:</p>
|
||||
|
||||
<!-- ATENȚIE: Înlocuiește "job_title" și "department" cu variabilele corecte din modelul tău Employee -->
|
||||
<p>
|
||||
<strong>{{ protocol.previous_holder.name|default:"Magazie (Neasignat)" }}</strong>, având funcția de
|
||||
{{ protocol.previous_holder.job_title|default:"-" }} de la {{ protocol.previous_holder.department|default:"-" }},
|
||||
în calitate de <strong>PREDĂTOR</strong>
|
||||
</p>
|
||||
<p style="text-align: center;">și</p>
|
||||
<p>
|
||||
<strong>{{ protocol.new_holder.name|default:"Magazie (Neasignat)" }}</strong>, având funcția de
|
||||
{{ protocol.new_holder.job_title|default:"-" }} de la {{ protocol.new_holder.department|default:"-" }},
|
||||
în calitate de <strong>PRIMITOR</strong>
|
||||
</p>
|
||||
|
||||
<p>Primitorul a preluat de la predător următoarele:</p>
|
||||
|
||||
<ul>
|
||||
{% for pc in protocol.computers.all %}
|
||||
<li>
|
||||
unitate centrală, nr. inventar {{ pc.equipment_code }}, caracteristici:
|
||||
{{ pc.cpu|default:"-" }} | {{ pc.motherboard|default:"-" }} |
|
||||
{{ pc.ram|default:"-" }} | {{ pc.storage|default:"-" }} |
|
||||
{{ pc.mac_address|default:"-" }};
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% for mon in protocol.monitors.all %}
|
||||
<li>
|
||||
monitor, nr. inventar {{ mon.equipment_code }}, {{ mon.name|default:"-" }},
|
||||
s/n {{ mon.serial_number|default:"-" }};
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% for per in protocol.peripherals.all %}
|
||||
<li>
|
||||
periferic ({{ per.get_peripheral_type_display }}), nr. inventar {{ per.equipment_code }},
|
||||
{{ per.name|default:"-" }}, s/n {{ per.serial_number|default:"-" }};
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<p><strong>Observații:</strong> {{ protocol.notes|default:"Fără observații." }}</p>
|
||||
|
||||
<!-- 5. SEMNĂTURILE -->
|
||||
<table style="width: 100%; border: none; margin-top: 30px; text-align: center;">
|
||||
<tr>
|
||||
<td style="width: 33%; border: none; vertical-align: top;">
|
||||
<strong>Am predat,</strong><br><br><br><br>
|
||||
...................................
|
||||
</td>
|
||||
<td style="width: 34%; border: none; vertical-align: top;">
|
||||
<strong>Avizat I.T.,</strong><br>
|
||||
{{ generated_by }}<br><br><br>
|
||||
...................................
|
||||
</td>
|
||||
<td style="width: 33%; border: none; vertical-align: top;">
|
||||
<strong>Am primit,</strong><br><br><br><br>
|
||||
...................................
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Fișă Tehnică Periferic: {{ peripheral.equipment_code }}</h2>
|
||||
<a href="{% url 'equipment:equipment_list' %}" class="btn btn-outline-secondary">⬅️ Înapoi la Inventar</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-white fw-bold">Specificații Curente</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item"><strong>Tip:</strong> {{ peripheral.get_peripheral_type_display }}</li>
|
||||
<li class="list-group-item"><strong>Model:</strong> {{ peripheral.name }}</li>
|
||||
<li class="list-group-item"><strong>Serial (S/N):</strong> {{ peripheral.serial_number }}</li>
|
||||
<li class="list-group-item"><strong>Utilizator Curent:</strong> {{ peripheral.assigned_to.name|default:"Neasignat" }}</li>
|
||||
<li class="list-group-item"><strong>Observații:</strong> {{ peripheral.observations|default:"-" }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-warning text-dark fw-bold d-flex justify-content-between align-items-center">
|
||||
<span>Jurnal Mentenanță</span>
|
||||
<button class="btn btn-sm btn-dark" hx-get="{% url 'equipment:peripheral_maintenance_create' peripheral.id %}" hx-target="#modal-content-area" data-bs-toggle="modal" data-bs-target="#form-modal">+ Adaugă Intervenție</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="table-light"><tr><th>Dată</th><th>Tip Operațiune</th><th>Efectuat de</th></tr></thead>
|
||||
<tbody>
|
||||
{% for log in maintenance_logs %}
|
||||
<tr><td>{{ log.date|date:"d.m.Y" }}</td><td>{{ log.operation_type }}<br><small class="text-muted">{{ log.description }}</small></td><td>{{ log.performed_by }}</td></tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3" class="text-center py-3">Nu există intervenții de mentenanță.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold mt-4 mb-3 border-bottom pb-2">Istoric Posesie (Custodie)</h5>
|
||||
<ul class="list-group mb-4 shadow-sm">
|
||||
{% for record in ownership_history %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>👤 {{ record.owner }}</strong>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
{{ record.start_date|date:"d.m.Y" }}
|
||||
<span class="mx-1">→</span>
|
||||
{% if record.end_date %}
|
||||
{{ record.end_date|date:"d.m.Y" }}
|
||||
{% else %}
|
||||
<span class="badge bg-success">Prezent</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-muted text-center">Fără istoric de posesie.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true"><div class="modal-dialog"><div class="modal-content" id="modal-content-area"></div></div></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>PV Predare-Primire {{ protocol.document_number }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
@media print { .no-print { display: none !important; } }
|
||||
body { font-size: 14px; line-height: 1.6; }
|
||||
.signature-box { margin-top: 60px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-5">
|
||||
<div class="text-end mb-4 no-print">
|
||||
<button class="btn btn-primary" onclick="window.print()">Printează PV</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-center fw-bold mb-2">PROCES VERBAL DE PREDARE – PRIMIRE</h3>
|
||||
<h4 class="text-center fw-bold mb-4">Nr. {{ protocol.document_number }}</h4>
|
||||
|
||||
<p>
|
||||
Încheiat astăzi: <strong>{{ protocol.date|date:"d F Y" }}</strong>, între:<br><br>
|
||||
|
||||
<strong>{{ protocol.previous_holder.name|default:"____________________" }}</strong>, având funcția
|
||||
{{ protocol.previous_holder.role|default:"____________________" }} -
|
||||
{% if protocol.previous_holder.compartiment %}{{ protocol.previous_holder.compartiment.name }}
|
||||
{% elif protocol.previous_holder.serviciu %}{{ protocol.previous_holder.serviciu.name }}
|
||||
{% elif protocol.previous_holder.directie %}{{ protocol.previous_holder.directie.name }}{% endif %},
|
||||
în calitate de <strong>PREDĂTOR</strong> și<br><br>
|
||||
|
||||
<strong>{{ protocol.new_holder.name|default:"____________________" }}</strong>, având funcția
|
||||
{{ protocol.new_holder.role|default:"____________________" }} -
|
||||
{% if protocol.new_holder.compartiment %}{{ protocol.new_holder.compartiment.name }}
|
||||
{% elif protocol.new_holder.serviciu %}{{ protocol.new_holder.serviciu.name }}
|
||||
{% elif protocol.new_holder.directie %}{{ protocol.new_holder.directie.name }}{% endif %},
|
||||
în calitate de <strong>PRIMITOR</strong>.
|
||||
</p>
|
||||
|
||||
<p class="mt-3">Primitorul a preluat de la predător echipamentul de mai jos:</p>
|
||||
|
||||
<div class="mb-4 ps-3">
|
||||
{% for pc in protocol.computers.all %}
|
||||
<strong>Unitate centrală:</strong> nr. inventar {{ pc.equipment_code }} | Procesor: {{ pc.cpu }} | Placă de bază: {{ pc.motherboard }} | RAM: {{ pc.ram }} | Stocare: {{ pc.storage }} {% if pc.optical_drive %}| Optic: {{ pc.optical_drive }}{% endif %} | MAC {{ pc.mac_address }} | Licența S.O.: {{ pc.os_license }} {% if pc.additional_licenses %}| Licențe: {{ pc.additional_licenses }}{% endif %};<br>
|
||||
{% endfor %}
|
||||
|
||||
{% for monitor in protocol.monitors.all %}
|
||||
<strong>Monitor:</strong> nr. inventar {{ monitor.equipment_code }} | Denumire: {{ monitor.name }} | s/n: {{ monitor.serial_number }};<br>
|
||||
{% endfor %}
|
||||
|
||||
{% for peripheral in protocol.peripherals.all %}
|
||||
<strong>{{ peripheral.get_peripheral_type_display }}:</strong> nr. inventar {{ peripheral.equipment_code }} | Denumire: {{ peripheral.name }} | s/n: {{ peripheral.serial_number }};<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row signature-box">
|
||||
<div class="col-4">
|
||||
<strong>Întocmit,</strong><br>
|
||||
Numele în clar: {{ protocol.it_validator }}<br>
|
||||
Data: {{ protocol.date|date:"d.m.Y" }}<br>
|
||||
Semnătura: ....................................
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<strong>Am predat,</strong><br>
|
||||
Numele în clar: {{ protocol.previous_holder.name|default:"...................................." }}<br>
|
||||
Data: {{ protocol.date|date:"d.m.Y" }}<br>
|
||||
Semnătura: ....................................
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<strong>Am primit,</strong><br>
|
||||
Numele în clar: {{ protocol.new_holder.name|default:"...................................." }}<br>
|
||||
Data: {{ protocol.date|date:"d.m.Y" }}<br>
|
||||
Semnătura: ....................................
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Fișă Inventar - {{ employee.name }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
.signature-box { margin-top: 50px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<!-- Antet Oficial pentru interfața web -->
|
||||
<div class="d-flex align-items-center mb-4 pb-3 border-bottom">
|
||||
<img src="{% static 'img/stema.png' %}" alt="Stema Roman" style="width: 55px; height: auto;" class="me-3">
|
||||
<div class="lh-sm">
|
||||
<div class="fw-bold fs-5">U.A.T. Municipiul Roman</div>
|
||||
<div class="text-secondary" style="font-size: 0.9em;">Compartiment administrare infrastructură I.T. și baze de date</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary no-print" onclick="window.print()">Printează Fișa</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-center mb-4">Fișă de Inventar Echipamente IT</h3>
|
||||
<h5 class="text-center text-muted mb-4">la data de: {% now "d.m.Y" %}</h5>
|
||||
|
||||
<div class="mb-4">
|
||||
<strong>Nume Angajat:</strong> {{ employee.name }}<br>
|
||||
<strong>Funcție:</strong> {{ employee.role }}<br>
|
||||
<strong>Birou:</strong> {{ employee.office_number }}<br>
|
||||
<strong>Structură:</strong>
|
||||
{% if employee.directie %}{{ employee.directie.name }}{% endif %}
|
||||
{% if employee.serviciu %} / {{ employee.serviciu.name }}{% endif %}
|
||||
{% if employee.compartiment %} / {{ employee.compartiment.name }}{% endif %}
|
||||
</div>
|
||||
|
||||
<h4>Echipamente în custodie</h4>
|
||||
<table class="table table-bordered table-sm mt-3">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Cod Echipament</th>
|
||||
<th>Tip</th>
|
||||
<th>Detalii / Producător</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pc in inventory.computers %}
|
||||
<tr>
|
||||
<td class="align-middle">{{ pc.equipment_code }}</td>
|
||||
<td class="align-middle">Computer/Laptop</td>
|
||||
<td>
|
||||
<strong>{{ pc.oem_name|default:"Sistem Asamblat" }}</strong><br>
|
||||
<span style="font-size: 0.9em;">
|
||||
<strong>Procesor:</strong> {{ pc.cpu }} | <strong>Placă de bază:</strong> {{ pc.motherboard }} | <strong>RAM:</strong> {{ pc.ram }} | <strong>Stocare:</strong> {{ pc.storage }} {% if pc.optical_drive %}| <strong>Optic:</strong> {{ pc.optical_drive }}{% endif %}<br>
|
||||
<strong>MAC:</strong> {{ pc.mac_address }} | <strong>Licența S.O.:</strong> {{ pc.os_license }} {% if pc.additional_licenses %}| <strong>Licențe:</strong> {{ pc.additional_licenses }}{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% for monitor in inventory.monitors %}
|
||||
<tr>
|
||||
<td class="align-middle">{{ monitor.equipment_code }}</td>
|
||||
<td class="align-middle">Monitor</td>
|
||||
<td>
|
||||
<strong>{{ monitor.name }}</strong><br>
|
||||
<span style="font-size: 0.9em;">
|
||||
<strong>Diagonală:</strong> {{ monitor.screen_diagonal }} | <strong>S/N:</strong> {{ monitor.serial_number }}
|
||||
{% if monitor.observations %}<br><strong>Observații:</strong> {{ monitor.observations }}{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% for peripheral in inventory.peripherals %}
|
||||
<tr>
|
||||
<td class="align-middle">{{ peripheral.equipment_code }}</td>
|
||||
<td class="align-middle">{{ peripheral.get_peripheral_type_display }}</td>
|
||||
<td>
|
||||
<strong>{{ peripheral.name }}</strong><br>
|
||||
<span style="font-size: 0.9em;">
|
||||
<strong>S/N:</strong> {{ peripheral.serial_number }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row signature-box">
|
||||
<div class="col-6 text-center">
|
||||
<strong>Compartimentul IT</strong><br><br>
|
||||
....................................
|
||||
</div>
|
||||
<div class="col-6 text-center">
|
||||
<strong>Semnătură Titular (Angajat)</strong><br><br>
|
||||
....................................
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,127 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Fișă Tehnică: {{ computer.equipment_code }}</h2>
|
||||
<a href="{% url 'equipment:equipment_list' %}" class="btn btn-outline-secondary">⬅️ Înapoi la Inventar</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Left Column: Current Specs -->
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-dark text-white fw-bold">Specificații Curente</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item"><strong>OEM:</strong> {{ computer.oem_name|default:"Sistem Asamblat" }}</li>
|
||||
<li class="list-group-item"><strong>Placă de bază:</strong> {{ computer.motherboard }}</li>
|
||||
<li class="list-group-item"><strong>CPU:</strong> {{ computer.cpu }}</li>
|
||||
<li class="list-group-item"><strong>RAM:</strong> {{ computer.ram }}</li>
|
||||
<li class="list-group-item"><strong>Stocare:</strong> {{ computer.storage }}</li>
|
||||
<li class="list-group-item"><strong>OS:</strong> {{ computer.os_license }}</li>
|
||||
<li class="list-group-item"><strong>MAC:</strong> {{ computer.mac_address }}</li>
|
||||
<li class="list-group-item"><strong>Utilizator Curent:</strong> {{ computer.assigned_to.name|default:"Neasignat" }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: History & Maintenance -->
|
||||
<div class="col-md-7">
|
||||
|
||||
<!-- Hardware History -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white fw-bold">Istoric Modificări Hardware</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Dată</th>
|
||||
<th>Componentă</th>
|
||||
<th>Valoare Veche</th>
|
||||
<th>Valoare Nouă</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for change in hardware_changes %}
|
||||
<tr>
|
||||
<td>{{ change.date|date:"d.m.Y H:i" }}</td>
|
||||
<td><strong>{{ change.field }}</strong></td>
|
||||
<td class="text-danger text-decoration-line-through">{{ change.old|default:"-" }}</td>
|
||||
<td class="text-success">{{ change.new|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center py-3">Nu există modificări hardware înregistrate.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance Logs -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-warning text-dark fw-bold d-flex justify-content-between align-items-center">
|
||||
<span>Jurnal Mentenanță</span>
|
||||
<!-- Buton care deschide modalul HTMX -->
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-dark"
|
||||
hx-get="{% url 'equipment:computer_maintenance_create' computer.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#form-modal">
|
||||
+ Adaugă Mentenanță
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Dată</th>
|
||||
<th>Tip Operațiune</th>
|
||||
<th>Efectuat de</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in maintenance_logs %}
|
||||
<tr>
|
||||
<td>{{ log.date|date:"d.m.Y" }}</td>
|
||||
<td>{{ log.operation_type }}<br><small class="text-muted">{{ log.description }}</small></td>
|
||||
<td>{{ log.performed_by }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3" class="text-center py-3">Nu există intervenții de mentenanță.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold mt-4 mb-3 border-bottom pb-2">Istoric Posesie (Custodie)</h5>
|
||||
<ul class="list-group mb-4 shadow-sm">
|
||||
{% for record in ownership_history %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>👤 {{ record.owner }}</strong>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
{{ record.start_date|date:"d.m.Y" }}
|
||||
<span class="mx-1">→</span>
|
||||
{% if record.end_date %}
|
||||
{{ record.end_date|date:"d.m.Y" }}
|
||||
{% else %}
|
||||
<span class="badge bg-success">Prezent</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-muted text-center">Fără istoric de posesie.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content" id="modal-content-area">
|
||||
<!-- Conținutul se va încărca dinamic -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+142
-2
@@ -1,3 +1,143 @@
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
from users.models import Employee
|
||||
from .models import Computer, Monitor, Peripheral, HandoverProtocol, DecommissionRecord
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class EquipmentModelTests(TestCase):
|
||||
def setUp(self):
|
||||
self.emp = Employee.objects.create(
|
||||
name="Angajat Test",
|
||||
username="angajat.test",
|
||||
role="Consilier",
|
||||
office_number="10",
|
||||
email="test@primariaroman.ro",
|
||||
)
|
||||
self.pc = Computer.objects.create(
|
||||
equipment_code="92601011",
|
||||
motherboard="ASUS Test",
|
||||
cpu="Intel i5",
|
||||
ram="16GB",
|
||||
storage="512GB SSD",
|
||||
mac_address="00:11:22:33:44:55",
|
||||
os_license="Windows 11 Pro",
|
||||
assigned_to=self.emp,
|
||||
status="activ",
|
||||
)
|
||||
|
||||
def test_active_inventory_property(self):
|
||||
# Verify the get_active_inventory property pulls the assigned PC
|
||||
inventory = self.emp.get_active_inventory
|
||||
self.assertIn(self.pc, inventory["computers"])
|
||||
self.assertEqual(len(inventory["computers"]), 1)
|
||||
|
||||
def test_handover_protocol_auto_numbering(self):
|
||||
# First protocol should be exactly 1000
|
||||
pv1 = HandoverProtocol.objects.create(previous_holder=self.emp)
|
||||
self.assertEqual(pv1.document_number, 1000)
|
||||
|
||||
# Second protocol should increment to 1001
|
||||
pv2 = HandoverProtocol.objects.create(previous_holder=self.emp)
|
||||
self.assertEqual(pv2.document_number, 1001)
|
||||
|
||||
|
||||
class EquipmentViewsTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
# Creăm un utilizator simplu (non-staff)
|
||||
self.normal_user = User.objects.create_user(
|
||||
username="user.simplu", password="password123", is_staff=False
|
||||
)
|
||||
|
||||
# Creăm un utilizator IT (staff)
|
||||
self.staff_user = User.objects.create_user(
|
||||
username="user.it", password="password123", is_staff=True
|
||||
)
|
||||
|
||||
def test_dashboard_access_authenticated(self):
|
||||
# Un utilizator logat trebuie să primească status 200 (OK) pe dashboard
|
||||
self.client.login(username="user.simplu", password="password123")
|
||||
response = self.client.get(
|
||||
reverse("equipment:dashboard")
|
||||
) # Ajustează numele rutei dacă diferă
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_dashboard_access_unauthenticated(self):
|
||||
# Un vizitator nelogat trebuie redirecționat (302) către pagina de login
|
||||
response = self.client.get(reverse("equipment:dashboard"))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/accounts/login/", response.url)
|
||||
|
||||
def test_staff_permission_denied_for_normal_users(self):
|
||||
# Un utilizator simplu nu are voie să acceseze formularul de adăugare certificat
|
||||
self.client.login(username="user.simplu", password="password123")
|
||||
response = self.client.get(reverse("equipment:certificate_create"))
|
||||
self.assertEqual(
|
||||
response.status_code, 403
|
||||
) # 403 = Forbidden (PermissionDenied)
|
||||
|
||||
def test_staff_permission_granted_for_it(self):
|
||||
# Un utilizator IT trebuie să poată accesa formularul
|
||||
self.client.login(username="user.it", password="password123")
|
||||
response = self.client.get(reverse("equipment:certificate_create"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class EquipmentBusinessLogicTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.staff_user = User.objects.create_user(
|
||||
username="user.it", password="password123", is_staff=True
|
||||
)
|
||||
self.client.login(username="user.it", password="password123")
|
||||
|
||||
# Creăm un angajat
|
||||
self.emp = Employee.objects.create(
|
||||
name="Angajat Test", username="angajat.test", role="Inspector"
|
||||
)
|
||||
|
||||
# Creăm un calculator activ, atribuit angajatului
|
||||
self.pc = Computer.objects.create(
|
||||
equipment_code="COMP-TEST-01",
|
||||
cpu="Intel i5",
|
||||
ram="16GB",
|
||||
motherboard="ASUS",
|
||||
mac_address="00:00:00:00:00:00",
|
||||
status="activ",
|
||||
assigned_to=self.emp,
|
||||
)
|
||||
|
||||
def test_export_csv_headers(self):
|
||||
# Verificăm dacă ruta de export returnează un fișier CSV valid
|
||||
response = self.client.get(reverse("equipment:export_inventory"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["Content-Type"], "text/csv; charset=utf-8-sig")
|
||||
self.assertIn(
|
||||
'attachment; filename="Inventar_Echipamente.csv"',
|
||||
response["Content-Disposition"],
|
||||
)
|
||||
|
||||
def test_decommission_logic_updates_status(self):
|
||||
# Simulăm trimiterea formularului de casare (POST request)
|
||||
response = self.client.post(
|
||||
reverse("equipment:decommission_create"),
|
||||
{
|
||||
"document_number": "PV-999",
|
||||
"date": "2026-08-31",
|
||||
"reason": "Test de casare automată",
|
||||
"computers": [self.pc.id], # Selectăm PC-ul creat în setUp
|
||||
# Celelalte pot fi goale dacă nu sunt required
|
||||
},
|
||||
)
|
||||
|
||||
# Status 204 sau 302 confirmă că formularul s-a procesat cu succes
|
||||
self.assertIn(response.status_code, [204, 302])
|
||||
|
||||
# Reîncărcăm PC-ul din baza de date pentru a vedea modificările
|
||||
self.pc.refresh_from_db()
|
||||
|
||||
# Verificăm dacă logica noastră a eliberat echipamentul corect
|
||||
self.assertEqual(self.pc.status, "casat")
|
||||
self.assertIsNone(self.pc.assigned_to)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "equipment"
|
||||
|
||||
urlpatterns = [
|
||||
# Print Routes
|
||||
path(
|
||||
"print/inventory/<int:employee_id>/",
|
||||
views.print_inventory_sheet,
|
||||
name="print_inventory",
|
||||
),
|
||||
path(
|
||||
"print/handover/<int:protocol_id>/",
|
||||
views.print_handover_protocol,
|
||||
name="print_handover",
|
||||
),
|
||||
# Dashboard
|
||||
# path("", views.equipment_list, name="equipment_list"),
|
||||
# Ruta principală devine Dashboard-ul
|
||||
path("", views.dashboard_view, name="dashboard"),
|
||||
path("inventory/", views.equipment_list, name="equipment_list"),
|
||||
path("inventory/export/", views.export_inventory_csv, name="export_inventory"),
|
||||
# Computer Routes
|
||||
path("computer/new/", views.computer_form, name="computer_create"),
|
||||
path("computer/<int:pk>/edit/", views.computer_form, name="computer_edit"),
|
||||
path(
|
||||
"computer/<int:pk>/sheet/",
|
||||
views.computer_technical_sheet,
|
||||
name="computer_sheet",
|
||||
),
|
||||
path(
|
||||
"computer/<int:pk>/maintenance/new/",
|
||||
views.computer_maintenance_create,
|
||||
name="computer_maintenance_create",
|
||||
),
|
||||
# Monitor Routes
|
||||
path("monitor/new/", views.monitor_form, name="monitor_create"),
|
||||
path("monitor/<int:pk>/edit/", views.monitor_form, name="monitor_edit"),
|
||||
path(
|
||||
"monitor/<int:pk>/sheet/", views.monitor_technical_sheet, name="monitor_sheet"
|
||||
),
|
||||
path(
|
||||
"monitor/<int:pk>/maintenance/new/",
|
||||
views.monitor_maintenance_create,
|
||||
name="monitor_maintenance_create",
|
||||
),
|
||||
# Peripheral Routes
|
||||
path("peripheral/new/", views.peripheral_form, name="peripheral_create"),
|
||||
path("peripheral/<int:pk>/edit/", views.peripheral_form, name="peripheral_edit"),
|
||||
path(
|
||||
"peripheral/<int:pk>/sheet/",
|
||||
views.peripheral_technical_sheet,
|
||||
name="peripheral_sheet",
|
||||
),
|
||||
path(
|
||||
"peripheral/<int:pk>/maintenance/new/",
|
||||
views.peripheral_maintenance_create,
|
||||
name="peripheral_maintenance_create",
|
||||
),
|
||||
# Rute Procese Verbale (Handover)
|
||||
path("handover/", views.handover_protocol_list, name="handover_list"),
|
||||
path("handover/new/", views.handover_protocol_form, name="handover_create"),
|
||||
path("handover/<int:pk>/edit/", views.handover_protocol_form, name="handover_edit"),
|
||||
path("handover/<int:pk>/pdf/", views.generate_handover_pdf, name="handover_pdf"),
|
||||
path(
|
||||
"handover/load-equipment/",
|
||||
views.load_user_equipment,
|
||||
name="load_user_equipment",
|
||||
),
|
||||
path("decommission/", views.decommission_list, name="decommission_list"),
|
||||
path("decommission/create/", views.decommission_form, name="decommission_create"),
|
||||
path(
|
||||
"decommission/<int:pk>/edit/", views.decommission_form, name="decommission_edit"
|
||||
),
|
||||
path(
|
||||
"decommission/<int:pk>/pdf/",
|
||||
views.generate_decommission_pdf,
|
||||
name="decommission_pdf",
|
||||
),
|
||||
path("certificates/", views.certificate_list, name="certificate_list"),
|
||||
path("certificates/new/", views.certificate_form, name="certificate_create"),
|
||||
path(
|
||||
"certificates/<int:pk>/edit/", views.certificate_form, name="certificate_edit"
|
||||
),
|
||||
]
|
||||
+915
-2
@@ -1,3 +1,916 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.template.loader import get_template
|
||||
from xhtml2pdf import pisa
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponse
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from users.models import Employee
|
||||
from .models import Computer, Monitor, Peripheral, MaintenanceRecord, HandoverProtocol
|
||||
from .forms import (
|
||||
ComputerForm,
|
||||
MonitorForm,
|
||||
PeripheralForm,
|
||||
MaintenanceRecordForm,
|
||||
HandoverProtocolForm,
|
||||
)
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
from django.template.loader import get_template
|
||||
from django.http import HttpResponse
|
||||
from .forms import DecommissionRecordForm
|
||||
from .models import DecommissionRecord
|
||||
from datetime import date, timedelta
|
||||
from .models import DigitalCertificate
|
||||
from .forms import DigitalCertificateForm
|
||||
import csv
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@login_required
|
||||
def computer_maintenance_create(request, pk):
|
||||
computer = get_object_or_404(Computer, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
# 1. Creăm instanța și îi legăm computerul DE LA ÎNCEPUT
|
||||
maintenance_instance = MaintenanceRecord(computer=computer)
|
||||
|
||||
# 2. Pasăm instanța către formular pentru a trece de validarea din clean()
|
||||
form = MaintenanceRecordForm(request.POST, instance=maintenance_instance)
|
||||
|
||||
if form.is_valid():
|
||||
form.save() # Acum se va salva perfect!
|
||||
|
||||
# Returnăm status 204 și instruim HTMX să reîncarce complet pagina
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Refresh"] = "true"
|
||||
return response
|
||||
else:
|
||||
initial_name = (
|
||||
request.user.get_full_name() or request.user.username
|
||||
if request.user.is_authenticated
|
||||
else ""
|
||||
)
|
||||
form = MaintenanceRecordForm(initial={"performed_by": initial_name})
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/maintenance_form.html",
|
||||
{
|
||||
"form": form,
|
||||
"equipment_title": computer.equipment_code,
|
||||
"post_url": reverse(
|
||||
"equipment:computer_maintenance_create", args=[computer.pk]
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def print_inventory_sheet(request, employee_id):
|
||||
employee = get_object_or_404(Employee, id=employee_id)
|
||||
inventory = employee.get_active_inventory
|
||||
|
||||
context = {
|
||||
"employee": employee,
|
||||
"inventory": inventory,
|
||||
}
|
||||
return render(request, "equipment/print_inventory.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def print_handover_protocol(request, protocol_id):
|
||||
protocol = get_object_or_404(HandoverProtocol, id=protocol_id)
|
||||
|
||||
context = {
|
||||
"protocol": protocol,
|
||||
}
|
||||
return render(request, "equipment/print_handover.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def equipment_list(request):
|
||||
query = request.GET.get("search", "")
|
||||
|
||||
# Fetch only active equipment
|
||||
computers = Computer.objects.filter(status="activ").order_by("-entry_date")
|
||||
monitors = Monitor.objects.filter(status="activ").order_by("-entry_date")
|
||||
peripherals = Peripheral.objects.filter(status="activ").order_by("-entry_date")
|
||||
|
||||
if query:
|
||||
computers = computers.filter(
|
||||
Q(equipment_code__icontains=query)
|
||||
| Q(oem_name__icontains=query)
|
||||
| Q(motherboard__icontains=query)
|
||||
| Q(cpu__icontains=query)
|
||||
| Q(ram__icontains=query)
|
||||
| Q(storage__icontains=query)
|
||||
| Q(gpu__icontains=query)
|
||||
| Q(mac_address__icontains=query)
|
||||
| Q(os_license__icontains=query)
|
||||
| Q(observations__icontains=query)
|
||||
| Q(assigned_to__name__icontains=query)
|
||||
)
|
||||
monitors = monitors.filter(
|
||||
Q(equipment_code__icontains=query)
|
||||
| Q(name__icontains=query)
|
||||
| Q(serial_number__icontains=query)
|
||||
| Q(screen_diagonal__icontains=query)
|
||||
| Q(observations__icontains=query)
|
||||
| Q(assigned_to__name__icontains=query)
|
||||
)
|
||||
peripherals = peripherals.filter(
|
||||
Q(equipment_code__icontains=query)
|
||||
| Q(name__icontains=query)
|
||||
| Q(peripheral_type__icontains=query)
|
||||
| Q(serial_number__icontains=query)
|
||||
| Q(observations__icontains=query)
|
||||
| Q(assigned_to__name__icontains=query)
|
||||
)
|
||||
|
||||
context = {
|
||||
"computers": computers,
|
||||
"monitors": monitors,
|
||||
"peripherals": peripherals,
|
||||
}
|
||||
|
||||
if request.headers.get("HX-Request"):
|
||||
return render(request, "equipment/partials/equipment_tables.html", context)
|
||||
|
||||
return render(request, "equipment/equipment_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def computer_form(request, pk=None):
|
||||
# Doar personalul IT (staff) are voie să adauge/modifice
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied("Nu aveți permisiunea de a edita echipamente.")
|
||||
|
||||
if pk:
|
||||
computer = get_object_or_404(Computer, pk=pk)
|
||||
else:
|
||||
computer = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = ComputerForm(request.POST, instance=computer)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# Return 204 and trigger the table refresh and modal close
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Trigger"] = "equipmentListChanged, closeModal"
|
||||
return response
|
||||
else:
|
||||
form = ComputerForm(instance=computer)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/computer_form.html",
|
||||
{"form": form, "computer": computer},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def monitor_form(request, pk=None):
|
||||
if pk:
|
||||
monitor = get_object_or_404(Monitor, pk=pk)
|
||||
else:
|
||||
monitor = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = MonitorForm(request.POST, instance=monitor)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Trigger"] = "equipmentListChanged, closeModal"
|
||||
return response
|
||||
else:
|
||||
form = MonitorForm(instance=monitor)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/monitor_form.html",
|
||||
{"form": form, "monitor": monitor},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def peripheral_form(request, pk=None):
|
||||
if pk:
|
||||
peripheral = get_object_or_404(Peripheral, pk=pk)
|
||||
else:
|
||||
peripheral = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = PeripheralForm(request.POST, instance=peripheral)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Trigger"] = "equipmentListChanged, closeModal"
|
||||
return response
|
||||
else:
|
||||
form = PeripheralForm(instance=peripheral)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/peripheral_form.html",
|
||||
{"form": form, "peripheral": peripheral},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def computer_technical_sheet(request, pk):
|
||||
computer = get_object_or_404(Computer, pk=pk)
|
||||
|
||||
# Fetch maintenance logs (using the related_name from your models)
|
||||
maintenance_logs = computer.maintenance_logs.all().order_by("-date")
|
||||
|
||||
# Calculate hardware changes from simple_history
|
||||
history = list(computer.history.all())
|
||||
hardware_changes = []
|
||||
|
||||
tracked_fields = {
|
||||
"cpu": "Procesor",
|
||||
"ram": "Memorie RAM",
|
||||
"storage": "Stocare",
|
||||
"gpu": "Placă Video",
|
||||
"motherboard": "Placă de bază",
|
||||
"status": "Status",
|
||||
}
|
||||
|
||||
# Compare each historical record with the one that came before it
|
||||
for i in range(len(history) - 1):
|
||||
new_rec = history[i]
|
||||
old_rec = history[i + 1]
|
||||
|
||||
for field, label in tracked_fields.items():
|
||||
old_val = getattr(old_rec, field)
|
||||
new_val = getattr(new_rec, field)
|
||||
|
||||
if old_val != new_val:
|
||||
hardware_changes.append(
|
||||
{
|
||||
"date": new_rec.history_date,
|
||||
"field": label,
|
||||
"old": old_val,
|
||||
"new": new_val,
|
||||
}
|
||||
)
|
||||
|
||||
# --- Extragere istoric posesie ---
|
||||
ownership_history = []
|
||||
# Parcurgem cronologic (de la cea mai veche la cea mai nouă înregistrare)
|
||||
history_asc = list(computer.history.all().order_by("history_date"))
|
||||
|
||||
current_owner = None
|
||||
start_date = None
|
||||
|
||||
for record in history_asc:
|
||||
owner = record.assigned_to
|
||||
|
||||
# Dacă s-a schimbat posesorul
|
||||
if owner != current_owner:
|
||||
# Salvăm perioada precedentă
|
||||
if start_date is not None:
|
||||
ownership_history.append(
|
||||
{
|
||||
"owner": (
|
||||
current_owner.name
|
||||
if current_owner
|
||||
else "Magazie (Neasignat)"
|
||||
),
|
||||
"start_date": start_date,
|
||||
"end_date": record.history_date,
|
||||
}
|
||||
)
|
||||
|
||||
# Începem perioada nouă
|
||||
current_owner = owner
|
||||
start_date = record.history_date
|
||||
|
||||
# Adăugăm perioada curentă, care e încă în desfășurare
|
||||
if start_date is not None:
|
||||
ownership_history.append(
|
||||
{
|
||||
"owner": current_owner.name if current_owner else "Magazie (Neasignat)",
|
||||
"start_date": start_date,
|
||||
"end_date": None, # Semnifică 'Prezent'
|
||||
}
|
||||
)
|
||||
|
||||
# Inversăm lista pentru a avea cel mai recent posesor sus
|
||||
ownership_history.reverse()
|
||||
# ------------------------------------------
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/technical_sheet.html",
|
||||
{
|
||||
"computer": computer,
|
||||
"maintenance_logs": maintenance_logs,
|
||||
"hardware_changes": hardware_changes,
|
||||
"ownership_history": ownership_history,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def monitor_technical_sheet(request, pk):
|
||||
monitor = get_object_or_404(Monitor, pk=pk)
|
||||
maintenance_logs = monitor.maintenance_logs.all().order_by("-date")
|
||||
|
||||
# --- Extragere istoric posesie ---
|
||||
ownership_history = []
|
||||
history_asc = list(monitor.history.all().order_by("history_date"))
|
||||
current_owner = None
|
||||
start_date = None
|
||||
|
||||
for record in history_asc:
|
||||
owner = record.assigned_to
|
||||
if owner != current_owner:
|
||||
if start_date is not None:
|
||||
ownership_history.append(
|
||||
{
|
||||
"owner": (
|
||||
current_owner.name
|
||||
if current_owner
|
||||
else "Magazie (Neasignat)"
|
||||
),
|
||||
"start_date": start_date,
|
||||
"end_date": record.history_date,
|
||||
}
|
||||
)
|
||||
current_owner = owner
|
||||
start_date = record.history_date
|
||||
|
||||
if start_date is not None:
|
||||
ownership_history.append(
|
||||
{
|
||||
"owner": current_owner.name if current_owner else "Magazie (Neasignat)",
|
||||
"start_date": start_date,
|
||||
"end_date": None,
|
||||
}
|
||||
)
|
||||
ownership_history.reverse()
|
||||
# ---------------------------------
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/monitor_technical_sheet.html",
|
||||
{
|
||||
"monitor": monitor,
|
||||
"maintenance_logs": maintenance_logs,
|
||||
"ownership_history": ownership_history, # Trimitem variabila către HTML
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def monitor_maintenance_create(request, pk):
|
||||
monitor = get_object_or_404(Monitor, pk=pk)
|
||||
if request.method == "POST":
|
||||
maintenance_instance = MaintenanceRecord(monitor=monitor)
|
||||
form = MaintenanceRecordForm(request.POST, instance=maintenance_instance)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Refresh"] = "true"
|
||||
return response
|
||||
else:
|
||||
initial_name = (
|
||||
request.user.get_full_name() or request.user.username
|
||||
if request.user.is_authenticated
|
||||
else ""
|
||||
)
|
||||
form = MaintenanceRecordForm(initial={"performed_by": initial_name})
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/maintenance_form.html",
|
||||
{
|
||||
"form": form,
|
||||
"equipment_title": monitor.equipment_code,
|
||||
"post_url": reverse(
|
||||
"equipment:monitor_maintenance_create", args=[monitor.pk]
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@login_required
|
||||
def peripheral_technical_sheet(request, pk):
|
||||
peripheral = get_object_or_404(Peripheral, pk=pk)
|
||||
maintenance_logs = peripheral.maintenance_logs.all().order_by("-date")
|
||||
|
||||
# --- Extragere istoric posesie ---
|
||||
ownership_history = []
|
||||
history_asc = list(peripheral.history.all().order_by("history_date"))
|
||||
current_owner = None
|
||||
start_date = None
|
||||
|
||||
for record in history_asc:
|
||||
owner = record.assigned_to
|
||||
if owner != current_owner:
|
||||
if start_date is not None:
|
||||
ownership_history.append(
|
||||
{
|
||||
"owner": (
|
||||
current_owner.name
|
||||
if current_owner
|
||||
else "Magazie (Neasignat)"
|
||||
),
|
||||
"start_date": start_date,
|
||||
"end_date": record.history_date,
|
||||
}
|
||||
)
|
||||
current_owner = owner
|
||||
start_date = record.history_date
|
||||
|
||||
if start_date is not None:
|
||||
ownership_history.append(
|
||||
{
|
||||
"owner": current_owner.name if current_owner else "Magazie (Neasignat)",
|
||||
"start_date": start_date,
|
||||
"end_date": None,
|
||||
}
|
||||
)
|
||||
ownership_history.reverse()
|
||||
# ---------------------------------
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/peripheral_technical_sheet.html",
|
||||
{
|
||||
"peripheral": peripheral,
|
||||
"maintenance_logs": maintenance_logs,
|
||||
"ownership_history": ownership_history, # Trimitem variabila către HTML
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def peripheral_maintenance_create(request, pk):
|
||||
peripheral = get_object_or_404(Peripheral, pk=pk)
|
||||
if request.method == "POST":
|
||||
maintenance_instance = MaintenanceRecord(peripheral=peripheral)
|
||||
form = MaintenanceRecordForm(request.POST, instance=maintenance_instance)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Refresh"] = "true"
|
||||
return response
|
||||
else:
|
||||
initial_name = (
|
||||
request.user.get_full_name() or request.user.username
|
||||
if request.user.is_authenticated
|
||||
else ""
|
||||
)
|
||||
form = MaintenanceRecordForm(initial={"performed_by": initial_name})
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/maintenance_form.html",
|
||||
{
|
||||
"form": form,
|
||||
"equipment_title": peripheral.equipment_code,
|
||||
"post_url": reverse(
|
||||
"equipment:peripheral_maintenance_create", args=[peripheral.pk]
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def handover_protocol_list(request):
|
||||
# Preluăm toate procesele verbale, ordonate de la cel mai nou la cel mai vechi
|
||||
protocols = HandoverProtocol.objects.all().order_by("-date", "-document_number")
|
||||
return render(request, "equipment/handover_list.html", {"protocols": protocols})
|
||||
|
||||
|
||||
@login_required
|
||||
def handover_protocol_form(request, pk=None):
|
||||
# --- Restricție de securitate ---
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied(
|
||||
"Nu aveți permisiunea de a genera procese verbale de predare-primire."
|
||||
)
|
||||
|
||||
if pk:
|
||||
protocol = get_object_or_404(HandoverProtocol, pk=pk)
|
||||
else:
|
||||
protocol = None
|
||||
|
||||
if request.method == "POST":
|
||||
# ATENȚIE: request.FILES este obligatoriu pentru a prelua documentul PDF/Scanat
|
||||
form = HandoverProtocolForm(request.POST, request.FILES, instance=protocol)
|
||||
|
||||
if form.is_valid():
|
||||
protocol_instance = form.save(commit=False)
|
||||
|
||||
# Completăm automat "Întocmit de (IT)" cu numele utilizatorului logat
|
||||
if not protocol_instance.it_validator and request.user.is_authenticated:
|
||||
protocol_instance.it_validator = (
|
||||
request.user.get_full_name() or request.user.username
|
||||
)
|
||||
|
||||
protocol_instance.save()
|
||||
|
||||
# ATENȚIE: Pentru câmpurile de tip SelectMultiple (Many-to-Many),
|
||||
# trebuie apelată manual această funcție după salvarea instanței
|
||||
form.save_m2m()
|
||||
|
||||
# Preluăm noul posesor din formular
|
||||
new_owner = protocol_instance.new_holder
|
||||
|
||||
# Trecem toate echipamentele selectate în proprietatea noului posesor
|
||||
if new_owner:
|
||||
# Folosim .save() în loc de .update() pentru ca simple_history să înregistreze modificarea
|
||||
for pc in protocol_instance.computers.all():
|
||||
pc.assigned_to = new_owner
|
||||
pc.save()
|
||||
|
||||
for mon in protocol_instance.monitors.all():
|
||||
mon.assigned_to = new_owner
|
||||
mon.save()
|
||||
|
||||
for per in protocol_instance.peripherals.all():
|
||||
per.assigned_to = new_owner
|
||||
per.save()
|
||||
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Refresh"] = "true"
|
||||
return response
|
||||
else:
|
||||
form = HandoverProtocolForm(instance=protocol)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/handover_form.html",
|
||||
{"form": form, "protocol": protocol},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard_view(request):
|
||||
# --- Coloana 1: Sumar Inventar (Înlocuitor temporar pt. Tichete) ---
|
||||
total_computers = Computer.objects.count()
|
||||
unassigned_computers = Computer.objects.filter(assigned_to__isnull=True)
|
||||
unassigned_monitors = Monitor.objects.filter(assigned_to__isnull=True)
|
||||
|
||||
# --- Coloana 2: Acțiuni Executate (Istoric simple_history) ---
|
||||
# Preluăm ultimele 10 acțiuni din fiecare categorie
|
||||
pc_hist = Computer.history.all()[:10]
|
||||
mon_hist = Monitor.history.all()[:10]
|
||||
per_hist = Peripheral.history.all()[:10]
|
||||
|
||||
# Combinăm listele și le sortăm descrescător după data acțiunii (history_date)
|
||||
activity_log = sorted(
|
||||
chain(pc_hist, mon_hist, per_hist), key=attrgetter("history_date"), reverse=True
|
||||
)[
|
||||
:10
|
||||
] # Păstrăm doar ultimele 10 cele mai recente acțiuni în total
|
||||
|
||||
# --- Coloana 3: Mentenanțe Efectuate ---
|
||||
recent_maintenance = MaintenanceRecord.objects.all().order_by("-date", "-id")[:10]
|
||||
|
||||
context = {
|
||||
"total_computers": total_computers,
|
||||
"unassigned_computers": unassigned_computers,
|
||||
"unassigned_monitors": unassigned_monitors,
|
||||
"activity_log": activity_log,
|
||||
"recent_maintenance": recent_maintenance,
|
||||
}
|
||||
|
||||
return render(request, "equipment/dashboard.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def generate_handover_pdf(request, pk):
|
||||
protocol = get_object_or_404(HandoverProtocol, pk=pk)
|
||||
|
||||
# Lăsăm Django să găsească calea absolută a fonturilor pe disc
|
||||
font_regular = finders.find("fonts/LiberationSerif-Regular.ttf")
|
||||
font_bold = finders.find("fonts/LiberationSerif-Bold.ttf")
|
||||
|
||||
# Găsim calea către siglă
|
||||
logo_path = finders.find("img/stema.png")
|
||||
|
||||
# Definim contextul care va fi trimis către șablonul HTML
|
||||
context = {
|
||||
"protocol": protocol,
|
||||
"font_regular": font_regular,
|
||||
"font_bold": font_bold,
|
||||
"logo_path": logo_path, # Trimitem calea siglei în context
|
||||
# Preluăm numele complet (dacă e setat în admin), altfel luăm username-ul
|
||||
"generated_by": request.user.get_full_name() or request.user.username,
|
||||
}
|
||||
|
||||
template = get_template("equipment/pdf_handover.html")
|
||||
html = template.render(context)
|
||||
|
||||
# --- REZOLVARE DIACRITICE ---
|
||||
# Înlocuim "ș" și "ț" cu virgulă (standard nou) cu variantele cu sedilă (standard vechi)
|
||||
# pentru a fi recunoscute de fontul din 2010 și librăria PDF.
|
||||
html = html.replace("ș", "ş").replace("ț", "ţ")
|
||||
html = html.replace("Ș", "Ş").replace("Ț", "Ţ")
|
||||
# ----------------------------
|
||||
|
||||
# Creăm răspunsul HTTP de tip PDF
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
# Setăm numele fișierului descărcat
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="Proces_Verbal_Nr_{protocol.document_number}.pdf"'
|
||||
)
|
||||
|
||||
# Generăm PDF-ul
|
||||
pisa_status = pisa.CreatePDF(html, dest=response)
|
||||
|
||||
if pisa_status.err:
|
||||
return HttpResponse("A apărut o eroare la generarea PDF-ului", status=500)
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def load_user_equipment(request):
|
||||
user_id = request.GET.get("previous_holder")
|
||||
form = HandoverProtocolForm()
|
||||
|
||||
if user_id:
|
||||
# Filtrăm doar echipamentele active ale utilizatorului selectat
|
||||
form.fields["computers"].queryset = Computer.objects.filter(
|
||||
assigned_to_id=user_id, status="activ"
|
||||
)
|
||||
form.fields["monitors"].queryset = Monitor.objects.filter(
|
||||
assigned_to_id=user_id, status="activ"
|
||||
)
|
||||
form.fields["peripherals"].queryset = Peripheral.objects.filter(
|
||||
assigned_to_id=user_id, status="activ"
|
||||
)
|
||||
else:
|
||||
form.fields["computers"].queryset = Computer.objects.none()
|
||||
form.fields["monitors"].queryset = Monitor.objects.none()
|
||||
form.fields["peripherals"].queryset = Peripheral.objects.none()
|
||||
|
||||
# Trimitem doar bucata HTML cu cele 3 liste
|
||||
return render(
|
||||
request, "equipment/partials/equipment_dropdowns.html", {"form": form}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def decommission_list(request):
|
||||
# Listăm toate procesele verbale de casare
|
||||
records = DecommissionRecord.objects.all().order_by("-date", "-document_number")
|
||||
return render(request, "equipment/decommission_list.html", {"records": records})
|
||||
|
||||
|
||||
@login_required
|
||||
def decommission_form(request, pk=None):
|
||||
# --- Restricție de securitate ---
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied("Nu aveți permisiunea de a gestiona casările.")
|
||||
|
||||
if pk:
|
||||
record = get_object_or_404(DecommissionRecord, pk=pk)
|
||||
else:
|
||||
record = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = DecommissionRecordForm(request.POST, instance=record)
|
||||
|
||||
if form.is_valid():
|
||||
decommission_instance = form.save()
|
||||
|
||||
# --- ACTUALIZARE AUTOMATĂ A INVENTARULUI ---
|
||||
# Trecem toate echipamentele selectate pe status "casat" și le scoatem de la utilizatori
|
||||
for pc in decommission_instance.computers.all():
|
||||
pc.status = "casat"
|
||||
pc.assigned_to = None
|
||||
pc.save()
|
||||
|
||||
for mon in decommission_instance.monitors.all():
|
||||
mon.status = "casat"
|
||||
mon.assigned_to = None
|
||||
mon.save()
|
||||
|
||||
for per in decommission_instance.peripherals.all():
|
||||
per.status = "casat"
|
||||
per.assigned_to = None
|
||||
per.save()
|
||||
# -------------------------------------------
|
||||
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Refresh"] = "true"
|
||||
return response
|
||||
else:
|
||||
form = DecommissionRecordForm(instance=record)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/decommission_form.html",
|
||||
{"form": form, "record": record},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def generate_decommission_pdf(request, pk):
|
||||
record = get_object_or_404(DecommissionRecord, pk=pk)
|
||||
|
||||
font_regular = finders.find("fonts/LiberationSerif-Regular.ttf")
|
||||
font_bold = finders.find("fonts/LiberationSerif-Bold.ttf")
|
||||
logo_path = finders.find("img/stema.png")
|
||||
|
||||
context = {
|
||||
"record": record,
|
||||
"font_regular": font_regular,
|
||||
"font_bold": font_bold,
|
||||
"logo_path": logo_path,
|
||||
"generated_by": request.user.get_full_name() or request.user.username,
|
||||
}
|
||||
|
||||
template = get_template("equipment/pdf_decommission.html")
|
||||
html = template.render(context)
|
||||
|
||||
# Rezolvare diacritice pentru fontul vechi
|
||||
html = html.replace("ș", "ş").replace("ț", "ţ")
|
||||
html = html.replace("Ș", "Ş").replace("Ț", "Ţ")
|
||||
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="PV_Casare_{record.document_number}.pdf"'
|
||||
)
|
||||
|
||||
pisa_status = pisa.CreatePDF(html, dest=response)
|
||||
|
||||
if pisa_status.err:
|
||||
return HttpResponse("A apărut o eroare la generarea PDF-ului", status=500)
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def certificate_list(request):
|
||||
certificates = DigitalCertificate.objects.all().order_by("expiration_date")
|
||||
|
||||
today = date.today()
|
||||
warning_date = today + timedelta(days=30)
|
||||
|
||||
# Calculăm statusul în timp real pentru fiecare certificat
|
||||
for cert in certificates:
|
||||
if cert.is_canceled:
|
||||
cert.status_color = "secondary"
|
||||
cert.status_text = "Anulat"
|
||||
elif cert.expiration_date < today:
|
||||
cert.status_color = "danger"
|
||||
cert.status_text = "Expirat"
|
||||
elif cert.expiration_date <= warning_date:
|
||||
cert.status_color = "warning text-dark"
|
||||
cert.status_text = "Expiră curând"
|
||||
else:
|
||||
cert.status_color = "success"
|
||||
cert.status_text = "Activ"
|
||||
|
||||
return render(
|
||||
request, "equipment/certificate_list.html", {"certificates": certificates}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def certificate_form(request, pk=None):
|
||||
# --- Restricție de securitate ---
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied(
|
||||
"Nu aveți permisiunea de a adăuga sau edita certificate digitale."
|
||||
)
|
||||
|
||||
if pk:
|
||||
certificate = get_object_or_404(DigitalCertificate, pk=pk)
|
||||
else:
|
||||
certificate = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = DigitalCertificateForm(request.POST, instance=certificate)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Refresh"] = "true"
|
||||
return response
|
||||
else:
|
||||
form = DigitalCertificateForm(instance=certificate)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"equipment/partials/certificate_form.html",
|
||||
{"form": form, "certificate": certificate},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def export_inventory_csv(request):
|
||||
response = HttpResponse(content_type="text/csv; charset=utf-8-sig")
|
||||
response["Content-Disposition"] = 'attachment; filename="Inventar_Echipamente.csv"'
|
||||
|
||||
writer = csv.writer(response, delimiter=";")
|
||||
|
||||
# ==========================================
|
||||
# 1. SECȚIUNEA COMPUTERE
|
||||
# ==========================================
|
||||
writer.writerow(["--- COMPUTERE ---"])
|
||||
writer.writerow(
|
||||
[
|
||||
"Cod Inventar",
|
||||
"OEM / Model",
|
||||
"Placă de bază",
|
||||
"Procesor (CPU)",
|
||||
"Memorie RAM",
|
||||
"Stocare",
|
||||
"Adresă MAC",
|
||||
"Utilizator Curent",
|
||||
"Status",
|
||||
]
|
||||
)
|
||||
|
||||
computers = Computer.objects.filter(status="activ").order_by("assigned_to__name")
|
||||
for pc in computers:
|
||||
owner = pc.assigned_to.name if pc.assigned_to else "Magazie (Neasignat)"
|
||||
writer.writerow(
|
||||
[
|
||||
pc.equipment_code,
|
||||
pc.oem_name or "Asamblat",
|
||||
pc.motherboard,
|
||||
pc.cpu,
|
||||
pc.ram,
|
||||
pc.storage,
|
||||
pc.mac_address,
|
||||
owner,
|
||||
pc.get_status_display(),
|
||||
]
|
||||
)
|
||||
|
||||
writer.writerow([]) # Rând liber pentru spațiere
|
||||
writer.writerow([])
|
||||
|
||||
# ==========================================
|
||||
# 2. SECȚIUNEA MONITOARE
|
||||
# ==========================================
|
||||
writer.writerow(["--- MONITOARE ---"])
|
||||
writer.writerow(
|
||||
[
|
||||
"Cod Inventar",
|
||||
"Model / Nume",
|
||||
"Diagonală",
|
||||
"Număr Serial",
|
||||
"Utilizator Curent",
|
||||
"Status",
|
||||
]
|
||||
)
|
||||
|
||||
monitors = Monitor.objects.filter(status="activ").order_by("assigned_to__name")
|
||||
for mon in monitors:
|
||||
owner = mon.assigned_to.name if mon.assigned_to else "Magazie (Neasignat)"
|
||||
writer.writerow(
|
||||
[
|
||||
mon.equipment_code,
|
||||
mon.name,
|
||||
mon.screen_diagonal,
|
||||
mon.serial_number,
|
||||
owner,
|
||||
mon.get_status_display(),
|
||||
]
|
||||
)
|
||||
|
||||
writer.writerow([]) # Rând liber pentru spațiere
|
||||
writer.writerow([])
|
||||
|
||||
# ==========================================
|
||||
# 3. SECȚIUNEA PERIFERICE
|
||||
# ==========================================
|
||||
writer.writerow(["--- PERIFERICE ---"])
|
||||
writer.writerow(
|
||||
[
|
||||
"Cod Inventar",
|
||||
"Tip Echipament",
|
||||
"Model / Nume",
|
||||
"Număr Serial",
|
||||
"Utilizator Curent",
|
||||
"Status",
|
||||
]
|
||||
)
|
||||
|
||||
peripherals = Peripheral.objects.filter(status="activ").order_by(
|
||||
"assigned_to__name"
|
||||
)
|
||||
for per in peripherals:
|
||||
owner = per.assigned_to.name if per.assigned_to else "Magazie (Neasignat)"
|
||||
writer.writerow(
|
||||
[
|
||||
per.equipment_code,
|
||||
per.get_peripheral_type_display(),
|
||||
per.name,
|
||||
per.serial_number,
|
||||
owner,
|
||||
per.get_status_display(),
|
||||
]
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@@ -63,7 +63,7 @@ ROOT_URLCONF = "eumi__v4_project.urls"
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
@@ -115,7 +115,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "ro"
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "Europe/Bucharest"
|
||||
|
||||
@@ -128,6 +128,9 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/6.1/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / "static",
|
||||
]
|
||||
|
||||
|
||||
# Email
|
||||
@@ -138,3 +141,13 @@ MAILERS = {
|
||||
"BACKEND": "django.core.mail.backends.console.EmailBackend",
|
||||
},
|
||||
}
|
||||
|
||||
MEDIA_URL = "media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
# Setări pentru Autentificare
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
LOGOUT_REDIRECT_URL = (
|
||||
"/accounts/login/" # După logout, se întoarce la formularul de conectare
|
||||
)
|
||||
|
||||
@@ -14,9 +14,24 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("admin/", admin.site.urls),
|
||||
# Rutele standard Django pentru login/logout
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
# Rutele aplicațiilor
|
||||
# path("equipment/", include("equipment.urls")),
|
||||
path("users/", include("users.urls")),
|
||||
path("interventions/", include("interventions.urls")),
|
||||
# Poți seta temporar echipamentele ca pagină de start dacă dorești,
|
||||
# până vom construi Dashboard-ul
|
||||
path("", include("equipment.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Intervenții (Helpdesk)</h2>
|
||||
|
||||
<!-- HTMX Live Search Input -->
|
||||
<input class="form-control w-25" type="search"
|
||||
name="search" placeholder="Caută tichet..."
|
||||
hx-get="{% url 'interventions:intervention_list' %}"
|
||||
hx-trigger="input changed delay:400ms, search"
|
||||
hx-target="#ticket-table-body">
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Subiect & Descriere</th>
|
||||
<th>Raportat de</th>
|
||||
<th>Status</th>
|
||||
<th>Prioritate</th>
|
||||
<th>Data Raportării</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ticket-table-body">
|
||||
{% include 'interventions/partials/ticket_table.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% for ticket in tickets %}
|
||||
<tr>
|
||||
<td class="align-middle fw-medium">#{{ ticket.id }}</td>
|
||||
<td class="align-middle">
|
||||
<strong>{{ ticket.title }}</strong><br>
|
||||
<small class="text-muted text-truncate d-inline-block" style="max-width: 300px;">
|
||||
{{ ticket.description }}
|
||||
</small>
|
||||
</td>
|
||||
<td class="align-middle">{{ ticket.reported_by.name }}</td>
|
||||
<td class="align-middle">
|
||||
{% if ticket.status == 'nou' %}
|
||||
<span class="badge bg-info text-dark">Nou (Deschis)</span>
|
||||
{% elif ticket.status == 'in_lucru' %}
|
||||
<span class="badge bg-warning text-dark">În lucru</span>
|
||||
{% elif ticket.status == 'in_asteptare' %}
|
||||
<span class="badge bg-secondary">În așteptare</span>
|
||||
{% elif ticket.status == 'solutionat' %}
|
||||
<span class="badge bg-success">Soluționat</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark">Închis</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{% if ticket.priority == 'scazuta' %}
|
||||
<span class="badge bg-secondary">Scăzută</span>
|
||||
{% elif ticket.priority == 'medie' %}
|
||||
<span class="badge bg-primary">Medie</span>
|
||||
{% elif ticket.priority == 'ridicata' %}
|
||||
<span class="badge bg-warning text-dark">Ridicată</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Critică</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">{{ ticket.created_at|date:"d.m.Y H:i" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">Nu au fost găsite tichete de suport.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
+25
-1
@@ -1,3 +1,27 @@
|
||||
from django.test import TestCase
|
||||
from users.models import Employee
|
||||
from .models import Intervention
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class InterventionModelTests(TestCase):
|
||||
def setUp(self):
|
||||
self.emp = Employee.objects.create(
|
||||
name="Angajat Test",
|
||||
username="angajat.test",
|
||||
role="Consilier",
|
||||
office_number="10",
|
||||
email="test@primariaroman.ro",
|
||||
)
|
||||
|
||||
def test_intervention_defaults(self):
|
||||
# Create a ticket providing only the required fields
|
||||
ticket = Intervention.objects.create(
|
||||
title="Problemă Rețea",
|
||||
description="Nu mă pot conecta la internet.",
|
||||
reported_by=self.emp,
|
||||
)
|
||||
|
||||
# Verify default values are correctly applied
|
||||
self.assertEqual(ticket.status, "nou")
|
||||
self.assertEqual(ticket.priority, "medie")
|
||||
self.assertIsNone(ticket.assigned_to_it)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "interventions"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.intervention_list, name="intervention_list"),
|
||||
]
|
||||
+21
-1
@@ -1,3 +1,23 @@
|
||||
from django.shortcuts import render
|
||||
from django.db.models import Q
|
||||
from .models import Intervention
|
||||
|
||||
# Create your views here.
|
||||
|
||||
def intervention_list(request):
|
||||
query = request.GET.get("search", "")
|
||||
tickets = Intervention.objects.all().order_by("-created_at")
|
||||
|
||||
if query:
|
||||
tickets = tickets.filter(
|
||||
Q(title__icontains=query)
|
||||
| Q(description__icontains=query)
|
||||
| Q(reported_by__name__icontains=query)
|
||||
| Q(assigned_to_it__icontains=query)
|
||||
)
|
||||
|
||||
context = {"tickets": tickets}
|
||||
|
||||
if request.headers.get("HX-Request"):
|
||||
return render(request, "interventions/partials/ticket_table.html", context)
|
||||
|
||||
return render(request, "interventions/intervention_list.html", context)
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -3,4 +3,4 @@ Django==6.1
|
||||
psycopg2-binary==2.9.12
|
||||
sqlparse==0.6.0
|
||||
django-simple-history
|
||||
|
||||
xhtml2pdf>=0.2.11
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,107 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EUMI - Management IT</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- HTMX -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
</head>
|
||||
<body class="bg-light" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
|
||||
<!-- Main Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #417690;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold" href="{% url 'equipment:dashboard' %}">E.U.M.I. v.4.0.0</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'users:user_list' %}">Angajați</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'equipment:equipment_list' %}">Echipamente</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'equipment:certificate_list' %}">Certificate Digitale</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'equipment:handover_list' %}">Procese Verbale</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'equipment:decommission_list' %}">Casări</a>
|
||||
</li>
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'interventions:intervention_list' %}">Intervenții (Helpdesk)</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item me-3 text-white">
|
||||
Salut, <strong>{{ user.username }}</strong>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<form method="post" action="{% url 'logout' %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-light">Deconectare</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<span class="navbar-text text-white">Compartimentul IT</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Dynamic Content Container -->
|
||||
<div class="container mt-4" id="main-content">
|
||||
<!-- Antet Oficial pentru interfața web -->
|
||||
<div class="d-flex align-items-center mb-4 pb-3 border-bottom">
|
||||
<img src="{% static 'img/stema.png' %}" alt="Stema Roman" style="width: 55px; height: auto;" class="me-3">
|
||||
<div class="lh-sm">
|
||||
<div class="fw-bold fs-5">U.A.T. Municipiul Roman</div>
|
||||
<div class="text-secondary" style="font-size: 0.9em;">Compartiment administrare infrastructură I.T. și baze de date</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block content %}
|
||||
<!-- Module views will be injected here -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
<footer class="mt-5 py-3 text-center text-muted small border-top">
|
||||
<div class="container">
|
||||
Aplicație dezvoltată de <strong>Tololoi Cătălin</strong> |
|
||||
✉️ <a href="mailto:catalin@x3c.ro" class="text-decoration-none text-muted">catalin@x3c.ro</a>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://npmcdn.com/flatpickr/dist/l10n/ro.js"></script>
|
||||
<script>
|
||||
function initializeCalendars() {
|
||||
flatpickr(".flatpickr-date", {
|
||||
dateFormat: "d.m.Y",
|
||||
locale: "ro",
|
||||
allowInput: true
|
||||
});
|
||||
}
|
||||
|
||||
// Inițializăm la prima încărcare a paginii
|
||||
initializeCalendars();
|
||||
|
||||
// Re-inițializăm de fiecare dată când HTMX deschide un modal nou
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
initializeCalendars();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-4">
|
||||
|
||||
<!-- Logo / Titlu instituție -->
|
||||
<!-- <div class="text-center mb-4">
|
||||
<img src="{% static 'img/stema.png' %}" alt="Stema Roman" style="width: 80px;" class="mb-3">
|
||||
<h4 class="fw-bold">U.A.T. Municipiul Roman</h4>
|
||||
<p class="text-muted small">Platformă Gestiune IT (IT EUMI)</p>
|
||||
</div> -->
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-dark text-white text-center fw-bold">
|
||||
Autentificare
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger p-2 small text-center">
|
||||
Utilizator sau parolă incorectă. Vă rugăm să încercați din nou.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_username" class="form-label fw-bold">Nume utilizator</label>
|
||||
<input type="text" name="username" class="form-control" id="id_username" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="id_password" class="form-label fw-bold">Parolă</label>
|
||||
<input type="password" name="password" class="form-control" id="id_password" required>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary fw-bold">Conectare</button>
|
||||
</div>
|
||||
<!-- Redirect URL ascuns pentru Django -->
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
from .models import Directie, Serviciu, Compartiment, Employee
|
||||
|
||||
@@ -33,6 +35,18 @@ class EmployeeAdmin(SimpleHistoryAdmin):
|
||||
"directie",
|
||||
"serviciu",
|
||||
"compartiment",
|
||||
"print_inventory_button",
|
||||
)
|
||||
list_filter = ("directie", "serviciu", "compartiment")
|
||||
search_fields = ("name", "username", "email")
|
||||
|
||||
def print_inventory_button(self, obj):
|
||||
if obj.pk:
|
||||
url = reverse("equipment:print_inventory", args=[obj.pk])
|
||||
return format_html(
|
||||
'<a class="button" href="{}" target="_blank" style="background-color: #417690; color: white; padding: 5px 10px; border-radius: 4px; text-decoration: none;">🖨️ Printează Fișa</a>',
|
||||
url,
|
||||
)
|
||||
return "-"
|
||||
|
||||
print_inventory_button.short_description = "Acțiune"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from django import forms
|
||||
from .models import Employee
|
||||
|
||||
|
||||
class EmployeeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Employee
|
||||
fields = [
|
||||
"name",
|
||||
"username",
|
||||
"role",
|
||||
"office_number",
|
||||
"email",
|
||||
"directie",
|
||||
"serviciu",
|
||||
"compartiment",
|
||||
"observations",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"username": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"role": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"office_number": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"directie": forms.Select(attrs={"class": "form-select"}),
|
||||
"serviciu": forms.Select(attrs={"class": "form-select"}),
|
||||
"compartiment": forms.Select(attrs={"class": "form-select"}),
|
||||
"observations": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.1 on 2026-08-26 11:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0002_historicalcompartiment_historicaldirectie_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='employee',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalemployee',
|
||||
name='observations',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Observații'),
|
||||
),
|
||||
]
|
||||
+14
-1
@@ -89,7 +89,9 @@ class Employee(models.Model):
|
||||
role = models.CharField(max_length=255, verbose_name="Function/role")
|
||||
office_number = models.CharField(max_length=50, verbose_name="Office number")
|
||||
email = models.EmailField(verbose_name="Email address")
|
||||
observations = models.TextField(null=True, blank=True, verbose_name="Observations")
|
||||
|
||||
# Keep only this single observations field
|
||||
observations = models.TextField(blank=True, null=True, verbose_name="Observații")
|
||||
|
||||
# Organizational Placement - Employees can be placed at any of the three levels
|
||||
directie = models.ForeignKey(
|
||||
@@ -116,6 +118,17 @@ class Employee(models.Model):
|
||||
related_name="employees",
|
||||
verbose_name="Compartiment",
|
||||
)
|
||||
|
||||
@property
|
||||
def get_active_inventory(self):
|
||||
"""Returns a dictionary of all active equipment and certificates assigned to the user."""
|
||||
return {
|
||||
"computers": self.computers.filter(status="activ"),
|
||||
"monitors": self.monitors.filter(status="activ"),
|
||||
"peripherals": self.peripherals.filter(status="activ"),
|
||||
"certificates": self.digital_certificates.filter(is_canceled=False),
|
||||
}
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<div class="modal-header bg-dark text-white">
|
||||
<h5 class="modal-title">{% if employee %}Editează Angajat{% else %}Adaugă Angajat Nou{% endif %}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form hx-post="{% if employee %}{% url 'users:employee_edit' employee.pk %}{% else %}{% url 'users:employee_create' %}{% endif %}"
|
||||
hx-target="#modal-content-area">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Nume complet</label>
|
||||
{{ form.name }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Username</label>
|
||||
{{ form.username }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Funcție</label>
|
||||
{{ form.role }}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">Nr. Birou</label>
|
||||
{{ form.office_number }}
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Email</label>
|
||||
{{ form.email }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Direcție</label>
|
||||
{{ form.directie }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Serviciu</label>
|
||||
{{ form.serviciu }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Compartiment</label>
|
||||
{{ form.compartiment }}
|
||||
</div>
|
||||
<div class="col-md-12 mt-3">
|
||||
<label class="form-label fw-bold">Observații</label>
|
||||
{{ form.observations }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anulează</button>
|
||||
<button type="submit" class="btn btn-primary">Salvează</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,31 @@
|
||||
{% for emp in employees %}
|
||||
<tr>
|
||||
<td class="align-middle fw-medium">{{ emp.name }}</td>
|
||||
<td class="align-middle">{{ emp.username }}</td>
|
||||
<td class="align-middle">{{ emp.role }}</td>
|
||||
<td class="align-middle">
|
||||
{% if emp.directie %}<strong>{{ emp.directie.name }}</strong><br>{% endif %}
|
||||
{% if emp.serviciu %}<span class="text-muted">{{ emp.serviciu.name }}</span><br>{% endif %}
|
||||
{% if emp.compartiment %}<small class="text-secondary">{{ emp.compartiment.name }}</small>{% endif %}
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<!-- Edit Button -->
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||
hx-get="{% url 'users:employee_edit' emp.id %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal" data-bs-target="#form-modal">
|
||||
✏️ Editează
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Print Button -->
|
||||
<a href="{% url 'equipment:print_inventory' emp.id %}" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
🖨️ Fișă
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">Nu au fost găsiți angajați conform criteriilor de căutare.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Angajați</h2>
|
||||
|
||||
<div class="d-flex gap-3 w-50 justify-content-end">
|
||||
<!-- HTMX Live Search Input -->
|
||||
<input class="form-control w-50" type="search"
|
||||
name="search" placeholder="Caută angajat..."
|
||||
hx-get="{% url 'users:user_list' %}"
|
||||
hx-trigger="input changed delay:400ms, search"
|
||||
hx-target="#user-table-body">
|
||||
|
||||
<!-- Add Button (Triggers Modal) -->
|
||||
{% if user.is_staff %}
|
||||
<button class="btn btn-success fw-bold"
|
||||
hx-get="{% url 'users:employee_create' %}"
|
||||
hx-target="#modal-content-area"
|
||||
data-bs-toggle="modal" data-bs-target="#form-modal">
|
||||
+ Adaugă Angajat
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Nume</th>
|
||||
<th>Username</th>
|
||||
<th>Funcție</th>
|
||||
<th>Structură (Dir / Serv / Comp)</th>
|
||||
<th class="text-center">Acțiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Added hx-trigger="employeeListChanged from:body" so it refreshes on save -->
|
||||
<tbody id="user-table-body" hx-get="{% url 'users:user_list' %}" hx-trigger="employeeListChanged from:body">
|
||||
{% include 'users/partials/user_table.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Empty Bootstrap Modal Container -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content" id="modal-content-area">
|
||||
<!-- Form will be injected here by HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script to automatically close the modal when HTMX triggers 'closeModal' -->
|
||||
<script>
|
||||
document.body.addEventListener('closeModal', function() {
|
||||
var modalEl = document.getElementById('form-modal');
|
||||
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
+43
-1
@@ -1,3 +1,45 @@
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import Directie, Serviciu, Compartiment, Employee
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class UsersModelTests(TestCase):
|
||||
def setUp(self):
|
||||
self.directie = Directie.objects.create(name="Direcția Economică")
|
||||
self.serviciu = Serviciu.objects.create(
|
||||
name="Serviciul Contabilitate", directie=self.directie
|
||||
)
|
||||
|
||||
def test_compartiment_validation_valid(self):
|
||||
# A Compartiment assigned to a Serviciu should pass validation
|
||||
comp = Compartiment(name="Compartiment Salarizare", serviciu=self.serviciu)
|
||||
comp.full_clean() # Validates the model without saving to DB
|
||||
self.assertEqual(comp.serviciu, self.serviciu)
|
||||
|
||||
def test_compartiment_validation_missing_parent(self):
|
||||
# A Compartiment with no parent should raise a ValidationError
|
||||
comp = Compartiment(name="Compartiment Orfan")
|
||||
with self.assertRaises(ValidationError):
|
||||
comp.full_clean()
|
||||
|
||||
def test_compartiment_validation_multiple_parents(self):
|
||||
# A Compartiment assigned to BOTH a Serviciu and a Directie should fail
|
||||
comp = Compartiment(
|
||||
name="Compartiment Dublu Subordonat",
|
||||
directie=self.directie,
|
||||
serviciu=self.serviciu,
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
comp.full_clean()
|
||||
|
||||
def test_employee_creation(self):
|
||||
emp = Employee.objects.create(
|
||||
name="Test User",
|
||||
username="testuser",
|
||||
role="Inspector",
|
||||
office_number="12",
|
||||
email="test@primariaroman.ro",
|
||||
directie=self.directie,
|
||||
)
|
||||
self.assertEqual(emp.name, "Test User")
|
||||
self.assertEqual(str(emp), "Test User (testuser)")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "users"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.user_list, name="user_list"),
|
||||
path("new/", views.employee_form, name="employee_create"),
|
||||
path("<int:pk>/edit/", views.employee_form, name="employee_edit"),
|
||||
]
|
||||
+53
-2
@@ -1,3 +1,54 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.db.models import Q
|
||||
from .models import Employee
|
||||
from .forms import EmployeeForm
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@login_required
|
||||
def user_list(request):
|
||||
query = request.GET.get("search", "")
|
||||
employees = Employee.objects.all().order_by("name")
|
||||
|
||||
if query:
|
||||
# Filter by name, username, or role (case-insensitive)
|
||||
employees = employees.filter(
|
||||
Q(name__icontains=query)
|
||||
| Q(username__icontains=query)
|
||||
| Q(role__icontains=query)
|
||||
)
|
||||
|
||||
context = {"employees": employees}
|
||||
|
||||
# If the request comes from HTMX, only return the table rows
|
||||
if request.headers.get("HX-Request"):
|
||||
return render(request, "users/partials/user_table.html", context)
|
||||
|
||||
# Otherwise, return the full page
|
||||
return render(request, "users/user_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def employee_form(request, pk=None):
|
||||
if pk:
|
||||
employee = get_object_or_404(Employee, pk=pk)
|
||||
else:
|
||||
employee = None
|
||||
|
||||
if request.method == "POST":
|
||||
form = EmployeeForm(request.POST, instance=employee)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# Return a 204 No Content response, but trigger custom HTMX events
|
||||
response = HttpResponse(status=204)
|
||||
response["HX-Trigger"] = "employeeListChanged, closeModal"
|
||||
return response
|
||||
else:
|
||||
form = EmployeeForm(instance=employee)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"users/partials/employee_form.html",
|
||||
{"form": form, "employee": employee},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user