Versiunea funcțională EUMI

This commit is contained in:
2026-08-31 16:10:46 +03:00
parent 75bcef3095
commit ce5028a27f
58 changed files with 4271 additions and 98 deletions
+79 -33
View File
@@ -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")
+292
View File
@@ -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)'),
),
]
@@ -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
View File
@@ -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 &bull; 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 &bull; 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 &ndash; 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
View File
@@ -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)
+86
View File
@@ -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
View File
@@ -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