diff --git a/Z_comenzi.txt b/Z_comenzi.txt index 691e659..8148d2f 100644 --- a/Z_comenzi.txt +++ b/Z_comenzi.txt @@ -25,5 +25,24 @@ $ git commit -m "conf_initial" $ docker compose up -d -$ docker compose exec web python3.13 manage.py createsuperuser +docker compose exec web python manage.py migrate + +$ docker compose exec web python manage.py createsuperuser + +docker compose exec web python manage.py startapp users +docker compose exec web python manage.py startapp equipment +docker compose exec web python manage.py startapp interventions + +sudo chown -R $USER: . + +docker compose exec web python manage.py makemigrations users +docker compose exec web python manage.py migrate + +docker compose exec web python -m pip install django-simple-history + +docker compose exec web python manage.py makemigrations +docker compose exec web python manage.py migrate + +docker compose exec web python manage.py makemigrations equipment +docker compose exec web python manage.py migrate diff --git a/equipment/__init__.py b/equipment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/equipment/admin.py b/equipment/admin.py new file mode 100644 index 0000000..de1ca76 --- /dev/null +++ b/equipment/admin.py @@ -0,0 +1,110 @@ +from django.contrib import admin +from simple_history.admin import SimpleHistoryAdmin +from .models import ( + Computer, + Monitor, + Peripheral, + DigitalCertificate, + MaintenanceRecord, + DecommissionRecord, +) + + +# --- Inlines for Computer --- +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 + + +# --- 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 + + +# --- 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 + + +@admin.register(Computer) +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] + + +@admin.register(Monitor) +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] + + +@admin.register(Peripheral) +class PeripheralAdmin(SimpleHistoryAdmin): + list_display = ( + "equipment_code", + "name", + "peripheral_type", + "status", + "assigned_to", + ) + list_filter = ("status", "peripheral_type", "entry_date", "assigned_to") + search_fields = ("equipment_code", "name", "serial_number") + inlines = [MaintenanceInlinePeripheral, DecommissionInlinePeripheral] + + +@admin.register(DigitalCertificate) +class DigitalCertificateAdmin(SimpleHistoryAdmin): + list_display = ( + "get_certificate_type_display", + "assigned_to", + "issue_date", + "expiration_date", + "is_canceled", + ) + list_filter = ("certificate_type", "is_canceled", "issue_date") + search_fields = ("assigned_to__name", "assigned_to__username") + + +# Optionally register the records independently so they can be searched globally +@admin.register(MaintenanceRecord) +class MaintenanceRecordAdmin(SimpleHistoryAdmin): + list_display = ("date", "operation_type", "performed_by") + list_filter = ("date", "operation_type") + search_fields = ("description", "performed_by") + + +@admin.register(DecommissionRecord) +class DecommissionRecordAdmin(SimpleHistoryAdmin): + list_display = ("document_number", "date", "reason") + list_filter = ("date",) + search_fields = ("document_number", "reason") diff --git a/equipment/apps.py b/equipment/apps.py new file mode 100644 index 0000000..ac98343 --- /dev/null +++ b/equipment/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class EquipmentConfig(AppConfig): + name = 'equipment' diff --git a/equipment/migrations/0001_initial.py b/equipment/migrations/0001_initial.py new file mode 100644 index 0000000..75c0575 --- /dev/null +++ b/equipment/migrations/0001_initial.py @@ -0,0 +1,196 @@ +# Generated by Django 6.1 on 2026-08-18 12: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): + + initial = True + + dependencies = [ + ('users', '0002_historicalcompartiment_historicaldirectie_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Computer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('equipment_code', models.CharField(blank=True, max_length=50, unique=True, verbose_name='Cod Echipament')), + ('entry_date', models.DateField(default=datetime.date.today, verbose_name='Data intrării')), + ('oem_name', models.CharField(max_length=255, verbose_name='Producător (OEM)')), + ('motherboard', models.CharField(max_length=255, verbose_name='Placă de bază')), + ('cpu', models.CharField(max_length=255, verbose_name='Procesor (CPU)')), + ('ram', models.CharField(max_length=100, verbose_name='Memorie RAM')), + ('storage', models.CharField(max_length=100, verbose_name='Stocare (HDD/SSD)')), + ('optical_drive', models.CharField(blank=True, max_length=100, null=True, verbose_name='Unitate optică')), + ('gpu', models.CharField(max_length=255, verbose_name='Placă video (GPU)')), + ('mac_address', models.CharField(max_length=50, verbose_name='Adresă MAC')), + ('os_license', models.CharField(max_length=255, verbose_name='Licență OS')), + ('additional_licenses', models.TextField(blank=True, null=True, verbose_name='Licențe adiționale')), + ('observations', models.TextField(blank=True, null=True, verbose_name='Observații')), + ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='computers', to='users.employee', verbose_name='Atribuit către')), + ], + options={ + 'verbose_name': 'Computer / Laptop', + 'verbose_name_plural': 'Computere / Laptopuri', + }, + ), + migrations.CreateModel( + name='DigitalCertificate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certificate_type', models.CharField(choices=[('cloud', 'Cloud'), ('usb', 'USB Token')], max_length=50, verbose_name='Tip Certificat')), + ('issue_date', models.DateField(verbose_name='Data emiterii')), + ('expiration_date', models.DateField(verbose_name='Data expirării')), + ('is_canceled', models.BooleanField(default=False, verbose_name='Anulat?')), + ('assigned_to', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='digital_certificates', to='users.employee', verbose_name='Titular Certificat')), + ], + options={ + 'verbose_name': 'Certificat Digital', + 'verbose_name_plural': 'Certificate Digitale', + }, + ), + migrations.CreateModel( + name='HistoricalComputer', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('equipment_code', models.CharField(blank=True, db_index=True, max_length=50, verbose_name='Cod Echipament')), + ('entry_date', models.DateField(default=datetime.date.today, verbose_name='Data intrării')), + ('oem_name', models.CharField(max_length=255, verbose_name='Producător (OEM)')), + ('motherboard', models.CharField(max_length=255, verbose_name='Placă de bază')), + ('cpu', models.CharField(max_length=255, verbose_name='Procesor (CPU)')), + ('ram', models.CharField(max_length=100, verbose_name='Memorie RAM')), + ('storage', models.CharField(max_length=100, verbose_name='Stocare (HDD/SSD)')), + ('optical_drive', models.CharField(blank=True, max_length=100, null=True, verbose_name='Unitate optică')), + ('gpu', models.CharField(max_length=255, verbose_name='Placă video (GPU)')), + ('mac_address', models.CharField(max_length=50, verbose_name='Adresă MAC')), + ('os_license', models.CharField(max_length=255, verbose_name='Licență OS')), + ('additional_licenses', models.TextField(blank=True, null=True, verbose_name='Licențe adiționale')), + ('observations', 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)), + ('assigned_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Atribuit către')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Computer / Laptop', + 'verbose_name_plural': 'historical Computere / Laptopuri', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalDigitalCertificate', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('certificate_type', models.CharField(choices=[('cloud', 'Cloud'), ('usb', 'USB Token')], max_length=50, verbose_name='Tip Certificat')), + ('issue_date', models.DateField(verbose_name='Data emiterii')), + ('expiration_date', models.DateField(verbose_name='Data expirării')), + ('is_canceled', models.BooleanField(default=False, verbose_name='Anulat?')), + ('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)), + ('assigned_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Titular Certificat')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Certificat Digital', + 'verbose_name_plural': 'historical Certificate Digitale', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalMonitor', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('equipment_code', models.CharField(blank=True, db_index=True, max_length=50, verbose_name='Cod Echipament')), + ('entry_date', models.DateField(default=datetime.date.today, verbose_name='Data intrării')), + ('name', models.CharField(max_length=255, verbose_name='Nume Monitor')), + ('serial_number', models.CharField(max_length=100, verbose_name='Număr Serial')), + ('screen_diagonal', models.CharField(max_length=50, verbose_name='Diagonală (inch)')), + ('observations', 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)), + ('assigned_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Atribuit către')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Monitor', + 'verbose_name_plural': 'historical Monitoare', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalPeripheral', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('equipment_code', models.CharField(blank=True, db_index=True, max_length=50, verbose_name='Cod Echipament')), + ('entry_date', models.DateField(default=datetime.date.today, verbose_name='Data intrării')), + ('peripheral_type', models.CharField(choices=[('printer', 'Imprimantă'), ('ups', 'UPS'), ('scanner', 'Scanner'), ('copier', 'Copiator'), ('ethernet', 'Adaptor Ethernet'), ('audio', 'Boxe Audio'), ('hdd_ext', 'HDD Extern'), ('webcam', 'Cameră Web'), ('other', 'Altul')], max_length=50, verbose_name='Tip Periferic')), + ('name', models.CharField(max_length=255, verbose_name='Nume Echipament')), + ('serial_number', models.CharField(max_length=100, verbose_name='Număr Serial')), + ('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)), + ('assigned_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Atribuit către')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Periferic', + 'verbose_name_plural': 'historical Periferice', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='Monitor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('equipment_code', models.CharField(blank=True, max_length=50, unique=True, verbose_name='Cod Echipament')), + ('entry_date', models.DateField(default=datetime.date.today, verbose_name='Data intrării')), + ('name', models.CharField(max_length=255, verbose_name='Nume Monitor')), + ('serial_number', models.CharField(max_length=100, verbose_name='Număr Serial')), + ('screen_diagonal', models.CharField(max_length=50, verbose_name='Diagonală (inch)')), + ('observations', models.TextField(blank=True, null=True, verbose_name='Observații')), + ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='monitors', to='users.employee', verbose_name='Atribuit către')), + ], + options={ + 'verbose_name': 'Monitor', + 'verbose_name_plural': 'Monitoare', + }, + ), + migrations.CreateModel( + name='Peripheral', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('equipment_code', models.CharField(blank=True, max_length=50, unique=True, verbose_name='Cod Echipament')), + ('entry_date', models.DateField(default=datetime.date.today, verbose_name='Data intrării')), + ('peripheral_type', models.CharField(choices=[('printer', 'Imprimantă'), ('ups', 'UPS'), ('scanner', 'Scanner'), ('copier', 'Copiator'), ('ethernet', 'Adaptor Ethernet'), ('audio', 'Boxe Audio'), ('hdd_ext', 'HDD Extern'), ('webcam', 'Cameră Web'), ('other', 'Altul')], max_length=50, verbose_name='Tip Periferic')), + ('name', models.CharField(max_length=255, verbose_name='Nume Echipament')), + ('serial_number', models.CharField(max_length=100, verbose_name='Număr Serial')), + ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='peripherals', to='users.employee', verbose_name='Atribuit către')), + ], + options={ + 'verbose_name': 'Periferic', + 'verbose_name_plural': 'Periferice', + }, + ), + ] diff --git a/equipment/migrations/0002_alter_computer_equipment_code_alter_computer_gpu_and_more.py b/equipment/migrations/0002_alter_computer_equipment_code_alter_computer_gpu_and_more.py new file mode 100644 index 0000000..b8203e8 --- /dev/null +++ b/equipment/migrations/0002_alter_computer_equipment_code_alter_computer_gpu_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 6.1 on 2026-08-18 12:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('equipment', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='computer', + name='equipment_code', + field=models.CharField(max_length=50, unique=True, verbose_name='Cod Echipament'), + ), + migrations.AlterField( + model_name='computer', + name='gpu', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Placă video (GPU)'), + ), + migrations.AlterField( + model_name='computer', + name='oem_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Producător (OEM)'), + ), + migrations.AlterField( + model_name='historicalcomputer', + name='equipment_code', + field=models.CharField(db_index=True, max_length=50, verbose_name='Cod Echipament'), + ), + migrations.AlterField( + model_name='historicalcomputer', + name='gpu', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Placă video (GPU)'), + ), + migrations.AlterField( + model_name='historicalcomputer', + name='oem_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Producător (OEM)'), + ), + migrations.AlterField( + model_name='historicalmonitor', + name='equipment_code', + field=models.CharField(db_index=True, max_length=50, verbose_name='Cod Echipament'), + ), + migrations.AlterField( + model_name='historicalperipheral', + name='equipment_code', + field=models.CharField(db_index=True, max_length=50, verbose_name='Cod Echipament'), + ), + migrations.AlterField( + model_name='monitor', + name='equipment_code', + field=models.CharField(max_length=50, unique=True, verbose_name='Cod Echipament'), + ), + migrations.AlterField( + model_name='peripheral', + name='equipment_code', + field=models.CharField(max_length=50, unique=True, verbose_name='Cod Echipament'), + ), + ] diff --git a/equipment/migrations/0003_computer_status_historicalcomputer_status_and_more.py b/equipment/migrations/0003_computer_status_historicalcomputer_status_and_more.py new file mode 100644 index 0000000..36255e4 --- /dev/null +++ b/equipment/migrations/0003_computer_status_historicalcomputer_status_and_more.py @@ -0,0 +1,130 @@ +# Generated by Django 6.1 on 2026-08-18 12:55 + +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', '0002_alter_computer_equipment_code_alter_computer_gpu_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='computer', + name='status', + field=models.CharField(choices=[('activ', 'Activ'), ('casat', 'Casat (Decommissioned)')], default='activ', max_length=20, verbose_name='Stare'), + ), + migrations.AddField( + model_name='historicalcomputer', + name='status', + field=models.CharField(choices=[('activ', 'Activ'), ('casat', 'Casat (Decommissioned)')], default='activ', max_length=20, verbose_name='Stare'), + ), + migrations.AddField( + model_name='historicalmonitor', + name='status', + field=models.CharField(choices=[('activ', 'Activ'), ('casat', 'Casat (Decommissioned)')], default='activ', max_length=20, verbose_name='Stare'), + ), + migrations.AddField( + model_name='historicalperipheral', + name='status', + field=models.CharField(choices=[('activ', 'Activ'), ('casat', 'Casat (Decommissioned)')], default='activ', max_length=20, verbose_name='Stare'), + ), + migrations.AddField( + model_name='monitor', + name='status', + field=models.CharField(choices=[('activ', 'Activ'), ('casat', 'Casat (Decommissioned)')], default='activ', max_length=20, verbose_name='Stare'), + ), + migrations.AddField( + model_name='peripheral', + name='status', + field=models.CharField(choices=[('activ', 'Activ'), ('casat', 'Casat (Decommissioned)')], default='activ', max_length=20, verbose_name='Stare'), + ), + migrations.CreateModel( + name='DecommissionRecord', + 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 casării')), + ('document_number', models.CharField(max_length=100, verbose_name='Număr document (Proces Verbal)')), + ('reason', models.TextField(verbose_name='Motivul casării (Defect, Uzat moral, etc.)')), + ('computer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='decommission_log', to='equipment.computer')), + ('monitor', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='decommission_log', to='equipment.monitor')), + ('peripheral', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='decommission_log', to='equipment.peripheral')), + ], + options={ + 'verbose_name': 'Înregistrare Casare', + 'verbose_name_plural': 'Înregistrări Casare', + }, + ), + migrations.CreateModel( + name='HistoricalDecommissionRecord', + 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 casării')), + ('document_number', models.CharField(max_length=100, verbose_name='Număr document (Proces Verbal)')), + ('reason', models.TextField(verbose_name='Motivul casării (Defect, Uzat moral, etc.)')), + ('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)), + ('computer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='equipment.computer')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('monitor', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='equipment.monitor')), + ('peripheral', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='equipment.peripheral')), + ], + options={ + 'verbose_name': 'historical Înregistrare Casare', + 'verbose_name_plural': 'historical Înregistrări Casare', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalMaintenanceRecord', + 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 intervenției')), + ('operation_type', models.CharField(max_length=150, verbose_name='Tip operațiune (Curățare, Upgrade, etc.)')), + ('description', models.TextField(verbose_name='Descriere detaliată')), + ('performed_by', models.CharField(max_length=100, verbose_name='Efectuat de')), + ('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)), + ('computer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='equipment.computer')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('monitor', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='equipment.monitor')), + ('peripheral', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='equipment.peripheral')), + ], + options={ + 'verbose_name': 'historical Foaie de Mentenanță', + 'verbose_name_plural': 'historical Foi de Mentenanță', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='MaintenanceRecord', + 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 intervenției')), + ('operation_type', models.CharField(max_length=150, verbose_name='Tip operațiune (Curățare, Upgrade, etc.)')), + ('description', models.TextField(verbose_name='Descriere detaliată')), + ('performed_by', models.CharField(max_length=100, verbose_name='Efectuat de')), + ('computer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='maintenance_logs', to='equipment.computer')), + ('monitor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='maintenance_logs', to='equipment.monitor')), + ('peripheral', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='maintenance_logs', to='equipment.peripheral')), + ], + options={ + 'verbose_name': 'Foaie de Mentenanță', + 'verbose_name_plural': 'Foi de Mentenanță', + }, + ), + ] diff --git a/equipment/migrations/__init__.py b/equipment/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/equipment/models.py b/equipment/models.py new file mode 100644 index 0000000..65cc6e5 --- /dev/null +++ b/equipment/models.py @@ -0,0 +1,291 @@ +from django.db import models +from datetime import date +from simple_history.models import HistoricalRecords +from users.models import Employee +from django.core.exceptions import ValidationError + +EQUIPMENT_STATUS = [ + ("activ", "Activ"), + ("casat", "Casat (Decommissioned)"), +] + + +class Computer(models.Model): + # Unique ID and Entry + equipment_code = models.CharField( + max_length=50, unique=True, verbose_name="Cod Echipament" + ) + entry_date = models.DateField(default=date.today, verbose_name="Data intrării") + + # Hardware Specs + oem_name = models.CharField( + max_length=255, null=True, blank=True, verbose_name="Producător (OEM)" + ) + motherboard = models.CharField(max_length=255, verbose_name="Placă de bază") + cpu = models.CharField(max_length=255, verbose_name="Procesor (CPU)") + ram = models.CharField(max_length=100, verbose_name="Memorie RAM") + storage = models.CharField(max_length=100, verbose_name="Stocare (HDD/SSD)") + optical_drive = models.CharField( + max_length=100, blank=True, null=True, verbose_name="Unitate optică" + ) + gpu = models.CharField( + max_length=255, null=True, blank=True, verbose_name="Placă video (GPU)" + ) + mac_address = models.CharField(max_length=50, verbose_name="Adresă MAC") + + # Software & Misc + os_license = models.CharField(max_length=255, verbose_name="Licență OS") + additional_licenses = models.TextField( + blank=True, null=True, verbose_name="Licențe adiționale" + ) + observations = models.TextField(blank=True, null=True, verbose_name="Observații") + + # Custody Assignment + assigned_to = models.ForeignKey( + Employee, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="computers", + verbose_name="Atribuit către", + ) + status = models.CharField( + max_length=20, choices=EQUIPMENT_STATUS, default="activ", verbose_name="Stare" + ) + + history = HistoricalRecords() + + def __str__(self): + return f"{self.equipment_code} - {self.oem_name or 'Sistem Asamblat'}" + + class Meta: + verbose_name = "Computer / Laptop" + verbose_name_plural = "Computere / Laptopuri" + + +class Monitor(models.Model): + equipment_code = models.CharField( + max_length=50, unique=True, verbose_name="Cod Echipament" + ) + entry_date = models.DateField(default=date.today, verbose_name="Data intrării") + + name = models.CharField(max_length=255, verbose_name="Nume Monitor") + serial_number = models.CharField(max_length=100, verbose_name="Număr Serial") + screen_diagonal = models.CharField(max_length=50, verbose_name="Diagonală (inch)") + observations = models.TextField(blank=True, null=True, verbose_name="Observații") + + assigned_to = models.ForeignKey( + Employee, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="monitors", + verbose_name="Atribuit către", + ) + status = models.CharField( + max_length=20, choices=EQUIPMENT_STATUS, default="activ", verbose_name="Stare" + ) + + history = HistoricalRecords() + + def __str__(self): + return f"{self.equipment_code} - {self.name}" + + class Meta: + verbose_name = "Monitor" + verbose_name_plural = "Monitoare" + + +class Peripheral(models.Model): + PERIPHERAL_TYPES = [ + ("printer", "Imprimantă"), + ("ups", "UPS"), + ("scanner", "Scanner"), + ("copier", "Copiator"), + ("ethernet", "Adaptor Ethernet"), + ("audio", "Boxe Audio"), + ("hdd_ext", "HDD Extern"), + ("webcam", "Cameră Web"), + ("other", "Altul"), + ] + + equipment_code = models.CharField( + max_length=50, unique=True, verbose_name="Cod Echipament" + ) + entry_date = models.DateField(default=date.today, verbose_name="Data intrării") + + peripheral_type = models.CharField( + max_length=50, choices=PERIPHERAL_TYPES, verbose_name="Tip Periferic" + ) + name = models.CharField(max_length=255, verbose_name="Nume Echipament") + serial_number = models.CharField(max_length=100, verbose_name="Număr Serial") + + assigned_to = models.ForeignKey( + Employee, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="peripherals", + verbose_name="Atribuit către", + ) + status = models.CharField( + max_length=20, choices=EQUIPMENT_STATUS, default="activ", verbose_name="Stare" + ) + + history = HistoricalRecords() + + def __str__(self): + return f"{self.equipment_code} - {self.name} ({self.get_peripheral_type_display()})" + + class Meta: + verbose_name = "Periferic" + verbose_name_plural = "Periferice" + + +class DigitalCertificate(models.Model): + CERT_TYPES = [ + ("cloud", "Cloud"), + ("usb", "USB Token"), + ] + + certificate_type = models.CharField( + max_length=50, choices=CERT_TYPES, verbose_name="Tip Certificat" + ) + issue_date = models.DateField(verbose_name="Data emiterii") + expiration_date = models.DateField(verbose_name="Data expirării") + is_canceled = models.BooleanField(default=False, verbose_name="Anulat?") + + # Links directly to the employee + assigned_to = models.ForeignKey( + Employee, + on_delete=models.CASCADE, + related_name="digital_certificates", + verbose_name="Titular Certificat", + ) + + history = HistoricalRecords() + + def __str__(self): + return f"Certificat {self.get_certificate_type_display()} - {self.assigned_to.name}" + + class Meta: + verbose_name = "Certificat Digital" + verbose_name_plural = "Certificate Digitale" + + +class MaintenanceRecord(models.Model): + date = models.DateField(default=date.today, verbose_name="Data intervenției") + operation_type = models.CharField( + max_length=150, verbose_name="Tip operațiune (Curățare, Upgrade, etc.)" + ) + description = models.TextField(verbose_name="Descriere detaliată") + performed_by = models.CharField(max_length=100, verbose_name="Efectuat de") + + # Links to equipment (only one should be populated) + computer = models.ForeignKey( + Computer, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="maintenance_logs", + ) + monitor = models.ForeignKey( + Monitor, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="maintenance_logs", + ) + peripheral = models.ForeignKey( + Peripheral, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="maintenance_logs", + ) + + history = HistoricalRecords() + + def clean(self): + equipment_count = sum( + [bool(self.computer), bool(self.monitor), bool(self.peripheral)] + ) + if equipment_count != 1: + raise ValidationError( + "O înregistrare de mentenanță trebuie să fie legată de exact un singur echipament." + ) + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.date} - {self.operation_type}" + + class Meta: + verbose_name = "Foaie de Mentenanță" + verbose_name_plural = "Foi de Mentenanță" + + +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)" + ) + 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", + ) + monitor = models.OneToOneField( + Monitor, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="decommission_log", + ) + peripheral = models.OneToOneField( + Peripheral, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="decommission_log", + ) + + 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() + + def __str__(self): + return f"Casare {self.document_number} din {self.date}" + + class Meta: + verbose_name = "Înregistrare Casare" + verbose_name_plural = "Înregistrări Casare" diff --git a/equipment/tests.py b/equipment/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/equipment/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/equipment/views.py b/equipment/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/equipment/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/eumi__v4_project/settings.py b/eumi__v4_project/settings.py index 5c8dc38..d2ae6b8 100644 --- a/eumi__v4_project/settings.py +++ b/eumi__v4_project/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/6.1/ref/settings/ """ from pathlib import Path +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,62 +21,74 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-z(p$%wh6x^_9!1cgt%!s$w8k39(fox-^!iujxai42_wncja4%s' +SECRET_KEY = "django-insecure-z(p$%wh6x^_9!1cgt%!s$w8k39(fox-^!iujxai42_wncja4%s" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["localhost", "127.0.0.1", "0.0.0.0"] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + # third-party apps: + "simple_history", + # custom apps + "users", + "equipment", + "interventions", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + # Add this line to track the user making the changes: + "simple_history.middleware.HistoryRequestMiddleware", ] -ROOT_URLCONF = 'eumi__v4_project.urls' +ROOT_URLCONF = "eumi__v4_project.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'eumi__v4_project.wsgi.application' +WSGI_APPLICATION = "eumi__v4_project.wsgi.application" # Database # https://docs.djangoproject.com/en/6.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("DB_NAME", "djangodb"), + "USER": os.environ.get("DB_USER", "djangouser"), + "PASSWORD": os.environ.get("DB_PASSWORD", "djangopass"), + "HOST": os.environ.get("DB_HOST", "db"), + "PORT": os.environ.get("DB_PORT", "5432"), } } @@ -85,16 +98,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -102,9 +115,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/6.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "ro" -TIME_ZONE = 'UTC' +TIME_ZONE = "Europe/Bucharest" USE_I18N = True @@ -114,14 +127,14 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/6.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" # Email # https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration MAILERS = { - 'default': { - 'BACKEND': 'django.core.mail.backends.console.EmailBackend', + "default": { + "BACKEND": "django.core.mail.backends.console.EmailBackend", }, } diff --git a/interventions/__init__.py b/interventions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/interventions/admin.py b/interventions/admin.py new file mode 100644 index 0000000..7e73b1d --- /dev/null +++ b/interventions/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from simple_history.admin import SimpleHistoryAdmin +from .models import Intervention + + +@admin.register(Intervention) +class InterventionAdmin(SimpleHistoryAdmin): + list_display = ( + "id", + "title", + "status", + "priority", + "reported_by", + "assigned_to_it", + "created_at", + ) + list_filter = ("status", "priority", "created_at", "assigned_to_it") + search_fields = ("title", "description", "reported_by__name", "assigned_to_it") + readonly_fields = ("created_at", "updated_at") + + fieldsets = ( + ( + "Detalii Tichet", + {"fields": ("title", "description", "reported_by", "priority", "status")}, + ), + ( + "Asignare și Rezolvare", + {"fields": ("assigned_to_it", "resolution_notes", "resolved_at")}, + ), + ("Timpi", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}), + ) diff --git a/interventions/apps.py b/interventions/apps.py new file mode 100644 index 0000000..fef6ac9 --- /dev/null +++ b/interventions/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class InterventionsConfig(AppConfig): + name = 'interventions' diff --git a/interventions/migrations/0001_initial.py b/interventions/migrations/0001_initial.py new file mode 100644 index 0000000..1bfb375 --- /dev/null +++ b/interventions/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 6.1 on 2026-08-18 13:10 + +import django.db.models.deletion +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0002_historicalcompartiment_historicaldirectie_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalIntervention', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Titlu scurt / Subiect')), + ('description', models.TextField(verbose_name='Descrierea problemei')), + ('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Data raportării')), + ('updated_at', models.DateTimeField(blank=True, editable=False, verbose_name='Ultima actualizare')), + ('resolved_at', models.DateTimeField(blank=True, null=True, verbose_name='Data soluționării')), + ('status', models.CharField(choices=[('nou', 'Nou (Deschis)'), ('in_lucru', 'În lucru'), ('in_asteptare', 'În așteptare'), ('solutionat', 'Soluționat'), ('inchis', 'Închis')], default='nou', max_length=20, verbose_name='Stare')), + ('priority', models.CharField(choices=[('scazuta', 'Scăzută'), ('medie', 'Medie'), ('ridicata', 'Ridicată'), ('critica', 'Critică (Urgență)')], default='medie', max_length=20, verbose_name='Prioritate')), + ('assigned_to_it', models.CharField(blank=True, max_length=150, null=True, verbose_name='Atribuit către (IT)')), + ('resolution_notes', models.TextField(blank=True, null=True, verbose_name='Note de soluționare')), + ('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)), + ('reported_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.employee', verbose_name='Raportat de (Utilizator)')), + ], + options={ + 'verbose_name': 'historical Intervenție Tehnică', + 'verbose_name_plural': 'historical Intervenții Tehnice', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='Intervention', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Titlu scurt / Subiect')), + ('description', models.TextField(verbose_name='Descrierea problemei')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Data raportării')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Ultima actualizare')), + ('resolved_at', models.DateTimeField(blank=True, null=True, verbose_name='Data soluționării')), + ('status', models.CharField(choices=[('nou', 'Nou (Deschis)'), ('in_lucru', 'În lucru'), ('in_asteptare', 'În așteptare'), ('solutionat', 'Soluționat'), ('inchis', 'Închis')], default='nou', max_length=20, verbose_name='Stare')), + ('priority', models.CharField(choices=[('scazuta', 'Scăzută'), ('medie', 'Medie'), ('ridicata', 'Ridicată'), ('critica', 'Critică (Urgență)')], default='medie', max_length=20, verbose_name='Prioritate')), + ('assigned_to_it', models.CharField(blank=True, max_length=150, null=True, verbose_name='Atribuit către (IT)')), + ('resolution_notes', models.TextField(blank=True, null=True, verbose_name='Note de soluționare')), + ('reported_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reported_interventions', to='users.employee', verbose_name='Raportat de (Utilizator)')), + ], + options={ + 'verbose_name': 'Intervenție Tehnică', + 'verbose_name_plural': 'Intervenții Tehnice', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/interventions/migrations/__init__.py b/interventions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/interventions/models.py b/interventions/models.py new file mode 100644 index 0000000..5cbceeb --- /dev/null +++ b/interventions/models.py @@ -0,0 +1,65 @@ +from django.db import models +from simple_history.models import HistoricalRecords +from users.models import Employee + + +class Intervention(models.Model): + STATUS_CHOICES = [ + ("nou", "Nou (Deschis)"), + ("in_lucru", "În lucru"), + ("in_asteptare", "În așteptare"), + ("solutionat", "Soluționat"), + ("inchis", "Închis"), + ] + + PRIORITY_CHOICES = [ + ("scazuta", "Scăzută"), + ("medie", "Medie"), + ("ridicata", "Ridicată"), + ("critica", "Critică (Urgență)"), + ] + + title = models.CharField(max_length=255, verbose_name="Titlu scurt / Subiect") + description = models.TextField(verbose_name="Descrierea problemei") + + # Tracking the timeline + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Data raportării") + updated_at = models.DateTimeField(auto_now=True, verbose_name="Ultima actualizare") + resolved_at = models.DateTimeField( + null=True, blank=True, verbose_name="Data soluționării" + ) + + status = models.CharField( + max_length=20, choices=STATUS_CHOICES, default="nou", verbose_name="Stare" + ) + priority = models.CharField( + max_length=20, + choices=PRIORITY_CHOICES, + default="medie", + verbose_name="Prioritate", + ) + + # People involved + reported_by = models.ForeignKey( + Employee, + on_delete=models.CASCADE, + related_name="reported_interventions", + verbose_name="Raportat de (Utilizator)", + ) + assigned_to_it = models.CharField( + max_length=150, blank=True, null=True, verbose_name="Atribuit către (IT)" + ) + + resolution_notes = models.TextField( + blank=True, null=True, verbose_name="Note de soluționare" + ) + + history = HistoricalRecords() + + def __str__(self): + return f"Ticket #{self.id} - {self.title} ({self.get_status_display()})" + + class Meta: + verbose_name = "Intervenție Tehnică" + verbose_name_plural = "Intervenții Tehnice" + ordering = ["-created_at"] diff --git a/interventions/tests.py b/interventions/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/interventions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/interventions/views.py b/interventions/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/interventions/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/requirements.txt b/requirements.txt index ae3b4cf..6757a74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ asgiref==3.12.1 Django==6.1 psycopg2-binary==2.9.12 sqlparse==0.6.0 +django-simple-history + diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..c725e22 --- /dev/null +++ b/users/admin.py @@ -0,0 +1,38 @@ +from django.contrib import admin +from simple_history.admin import SimpleHistoryAdmin +from .models import Directie, Serviciu, Compartiment, Employee + + +@admin.register(Directie) +class DirectieAdmin(SimpleHistoryAdmin): + list_display = ("name",) + search_fields = ("name",) + + +@admin.register(Serviciu) +class ServiciuAdmin(SimpleHistoryAdmin): + list_display = ("name", "directie") + list_filter = ("directie",) + search_fields = ("name",) + + +@admin.register(Compartiment) +class CompartimentAdmin(SimpleHistoryAdmin): + list_display = ("name", "directie", "serviciu") + list_filter = ("directie", "serviciu") + search_fields = ("name",) + + +@admin.register(Employee) +class EmployeeAdmin(SimpleHistoryAdmin): + list_display = ( + "name", + "username", + "role", + "email", + "directie", + "serviciu", + "compartiment", + ) + list_filter = ("directie", "serviciu", "compartiment") + search_fields = ("name", "username", "email") diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..4ce1fab --- /dev/null +++ b/users/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + name = 'users' diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..67bba21 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# Generated by Django 6.1 on 2026-08-18 11:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Directie', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nume Direcție')), + ], + options={ + 'verbose_name': 'Direcție', + 'verbose_name_plural': 'Direcții', + }, + ), + migrations.CreateModel( + name='Compartiment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nume Compartiment')), + ('directie', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compartimente_directe', to='users.directie', verbose_name='Direcție (Direct)')), + ], + options={ + 'verbose_name': 'Compartiment', + 'verbose_name_plural': 'Compartimente', + }, + ), + migrations.CreateModel( + name='Serviciu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nume Serviciu')), + ('directie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='servicii', to='users.directie', verbose_name='Direcție')), + ], + options={ + 'verbose_name': 'Serviciu', + 'verbose_name_plural': 'Servicii', + }, + ), + migrations.CreateModel( + name='Employee', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('username', models.CharField(max_length=150, unique=True, verbose_name='User name')), + ('role', models.CharField(max_length=255, verbose_name='Function/role')), + ('office_number', models.CharField(max_length=50, verbose_name='Office number')), + ('email', models.EmailField(max_length=254, verbose_name='Email address')), + ('observations', models.TextField(blank=True, null=True, verbose_name='Observations')), + ('compartiment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employees', to='users.compartiment', verbose_name='Compartiment')), + ('directie', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employees', to='users.directie', verbose_name='Direcție')), + ('serviciu', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employees', to='users.serviciu', verbose_name='Serviciu')), + ], + options={ + 'verbose_name': 'Angajat / Utilizator', + 'verbose_name_plural': 'Angajați / Utilizatori', + }, + ), + migrations.AddField( + model_name='compartiment', + name='serviciu', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compartimente_subordonate', to='users.serviciu', verbose_name='Serviciu'), + ), + ] diff --git a/users/migrations/0002_historicalcompartiment_historicaldirectie_and_more.py b/users/migrations/0002_historicalcompartiment_historicaldirectie_and_more.py new file mode 100644 index 0000000..010ee17 --- /dev/null +++ b/users/migrations/0002_historicalcompartiment_historicaldirectie_and_more.py @@ -0,0 +1,104 @@ +# Generated by Django 6.1 on 2026-08-18 11:43 + +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 = [ + ('users', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalCompartiment', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nume Compartiment')), + ('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)), + ('directie', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.directie', verbose_name='Direcție (Direct)')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('serviciu', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.serviciu', verbose_name='Serviciu')), + ], + options={ + 'verbose_name': 'historical Compartiment', + 'verbose_name_plural': 'historical Compartimente', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalDirectie', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nume Direcție')), + ('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)), + ], + options={ + 'verbose_name': 'historical Direcție', + 'verbose_name_plural': 'historical Direcții', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalEmployee', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('username', models.CharField(db_index=True, max_length=150, verbose_name='User name')), + ('role', models.CharField(max_length=255, verbose_name='Function/role')), + ('office_number', models.CharField(max_length=50, verbose_name='Office number')), + ('email', models.EmailField(max_length=254, verbose_name='Email address')), + ('observations', models.TextField(blank=True, null=True, verbose_name='Observations')), + ('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)), + ('compartiment', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.compartiment', verbose_name='Compartiment')), + ('directie', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.directie', verbose_name='Direcție')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('serviciu', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.serviciu', verbose_name='Serviciu')), + ], + options={ + 'verbose_name': 'historical Angajat / Utilizator', + 'verbose_name_plural': 'historical Angajați / Utilizatori', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalServiciu', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Nume Serviciu')), + ('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)), + ('directie', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.directie', verbose_name='Direcție')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Serviciu', + 'verbose_name_plural': 'historical Servicii', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..8c0c38f --- /dev/null +++ b/users/models.py @@ -0,0 +1,126 @@ +from django.db import models +from django.core.exceptions import ValidationError +from simple_history.models import HistoricalRecords + + +class Directie(models.Model): + name = models.CharField(max_length=255, verbose_name="Nume Direcție") + history = HistoricalRecords() + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Direcție" + verbose_name_plural = "Direcții" + + +class Serviciu(models.Model): + name = models.CharField(max_length=255, verbose_name="Nume Serviciu") + # A Serviciu is directly assigned to a Direcție + directie = models.ForeignKey( + Directie, + on_delete=models.CASCADE, + related_name="servicii", + verbose_name="Direcție", + ) + history = HistoricalRecords() + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Serviciu" + verbose_name_plural = "Servicii" + + +class Compartiment(models.Model): + name = models.CharField(max_length=255, verbose_name="Nume Compartiment") + # A Compartiment can be part of a Serviciu or directly included in a Direcție + directie = models.ForeignKey( + Directie, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="compartimente_directe", + verbose_name="Direcție (Direct)", + ) + serviciu = models.ForeignKey( + Serviciu, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="compartimente_subordonate", + verbose_name="Serviciu", + ) + history = HistoricalRecords() + + def clean(self): + # Validation to ensure the Compartiment belongs to either a Direcție OR a Serviciu, but not both or neither. + if not self.directie and not self.serviciu: + raise ValidationError( + "Un compartiment trebuie să aparțină de o Direcție sau de un Serviciu." + ) + if self.directie and self.serviciu: + raise ValidationError( + "Un compartiment nu poate aparține simultan și de o Direcție și de un Serviciu." + ) + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Compartiment" + verbose_name_plural = "Compartimente" + + +class Employee(models.Model): + """ + Represents the 'Users' from the project specification who will hold equipment. + Named 'Employee' to avoid conflicts with Django's built-in auth.User model. + """ + + name = models.CharField(max_length=255, verbose_name="Name") + username = models.CharField(max_length=150, unique=True, verbose_name="User name") + role = models.CharField(max_length=255, verbose_name="Function/role") + office_number = models.CharField(max_length=50, verbose_name="Office number") + email = models.EmailField(verbose_name="Email address") + observations = models.TextField(null=True, blank=True, verbose_name="Observations") + + # Organizational Placement - Employees can be placed at any of the three levels + directie = models.ForeignKey( + Directie, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="employees", + verbose_name="Direcție", + ) + serviciu = models.ForeignKey( + Serviciu, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="employees", + verbose_name="Serviciu", + ) + compartiment = models.ForeignKey( + Compartiment, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="employees", + verbose_name="Compartiment", + ) + history = HistoricalRecords() + + def __str__(self): + return f"{self.name} ({self.username})" + + class Meta: + verbose_name = "Angajat / Utilizator" + verbose_name_plural = "Angajați / Utilizatori" diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/users/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.