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 from django.contrib.auth.decorators import login_required from django.shortcuts import render @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() total_monitors = Monitor.objects.count() total_peripherals = Peripheral.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] emp_hist = Employee.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, emp_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, "total_monitors": total_monitors, "total_peripherals": total_peripherals, "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 from datetime import datetime @login_required def maintenance_report(request): # Preluăm datele exact așa cum le introduce utilizatorul (ex: 20.05.2026 sau 20-05-2026) start_date_raw = request.GET.get("start_date") end_date_raw = request.GET.get("end_date") records = None # Funcție ajutătoare pentru a traduce formatul românesc în formatul bazei de date def parse_romanian_date(date_str): if not date_str: return None clean_str = str(date_str).strip() # Unificăm separatorii (transformăm cratima în punct pentru a verifica mai ușor) clean_str = clean_str.replace("-", ".") try: # Traducem din ZZ.LL.AAAA în AAAA-LL-ZZ parsed = datetime.strptime(clean_str, "%d.%m.%Y") return parsed.strftime("%Y-%m-%d") except ValueError: # Dacă deja este în formatul corect (ex. a fost introdus manual AAAA-LL-ZZ), îl lăsăm așa return clean_str # Convertim datele pentru baza de date start_date_db = parse_romanian_date(start_date_raw) end_date_db = parse_romanian_date(end_date_raw) if start_date_db and end_date_db: records = MaintenanceRecord.objects.filter( date__range=[start_date_db, end_date_db] ).order_by("-date") context = { "records": records, "start_date": start_date_raw, # Trimitem înapoi formatul original pentru a rămâne afișat în formular "end_date": end_date_raw, } return render(request, "equipment/maintenance_report.html", context)