diff --git a/equipment_tracker/breakages/admin.py b/equipment_tracker/breakages/admin.py index 8c38f3f..aae3ac1 100644 --- a/equipment_tracker/breakages/admin.py +++ b/equipment_tracker/breakages/admin.py @@ -1,3 +1,9 @@ from django.contrib import admin +from .models import BreakageReport -# Register your models here. + +class BreakageReportAdmin(admin.ModelAdmin): + list_display = ('id', 'transaction', 'resolved', 'timestamp') + + +admin.site.register(BreakageReport, BreakageReportAdmin) diff --git a/equipment_tracker/breakages/migrations/0001_initial.py b/equipment_tracker/breakages/migrations/0001_initial.py new file mode 100644 index 0000000..de2644c --- /dev/null +++ b/equipment_tracker/breakages/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.7 on 2023-12-08 15:33 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('transactions', '0001_initial'), + ('equipments', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='BreakageReport', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('resolved', models.BooleanField(default=False)), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('equipments', models.ManyToManyField(to='equipments.equipmentinstance')), + ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='transactions.transaction')), + ], + ), + ] diff --git a/equipment_tracker/breakages/models.py b/equipment_tracker/breakages/models.py index 71a8362..f05457b 100644 --- a/equipment_tracker/breakages/models.py +++ b/equipment_tracker/breakages/models.py @@ -1,3 +1,46 @@ from django.db import models +from accounts.models import CustomUser +from transactions.models import Transaction +from equipments.models import EquipmentInstance +from django.utils.timezone import now -# Create your models here. + +class BreakageReport(models.Model): + transaction = models.ForeignKey( + Transaction, on_delete=models.CASCADE) + equipments = models.ManyToManyField(EquipmentInstance) + resolved = models.BooleanField(default=False) + timestamp = models.DateTimeField(default=now, editable=False) + + def __str__(self): + return f"Breakage report for transaction #{self.transaction.id} by {self.transaction.borrower} under {self.transaction.teacher}" + + def save(self, *args, **kwargs): + # Check if the instance is being updated + if not self._state.adding: + # Check if all associated equipment instances have status "Working" + all_working = all( + eq.status == 'Working' for eq in self.equipments.all()) + + # If all equipment instances are working + if all_working: + # set resolved field to True + self.resolved = True + # set the status of the associated transaction to "Finalized" + self.transaction.status = 'Finalized' + self.transaction.save() + + # Then save the instance again to reflect the changes + super().save(*args, **kwargs) + # If not, set the resolved field to False + else: + if (self.resolved != False or self.transaction.status != 'With Breakages: Pending Resolution'): + # set resolved field to False + self.resolved = False + # set the status of the associated transaction to still be pending + self.transaction.status = 'With Breakages: Pending Resolution' + self.transaction.save() + # Then save the instance again to reflect the changes + super().save(*args, **kwargs) + else: + super().save(*args, **kwargs) diff --git a/equipment_tracker/breakages/serializers.py b/equipment_tracker/breakages/serializers.py new file mode 100644 index 0000000..4a2c9c4 --- /dev/null +++ b/equipment_tracker/breakages/serializers.py @@ -0,0 +1,67 @@ +from rest_framework import serializers +from accounts.models import CustomUser +from equipments.models import EquipmentInstance +from equipments.serializers import EquipmentInstanceSerializer +from .models import Transaction +from accounts.models import CustomUser +from config.settings import DEBUG +from .models import BreakageReport + + +class BreakageReportSerializer(serializers.HyperlinkedModelSerializer): + transaction = serializers.SlugRelatedField( + many=False, slug_field='id', queryset=Transaction.objects.all(), required=True) + + equipments = serializers.SlugRelatedField( + many=True, slug_field='id', queryset=EquipmentInstance.objects.all()) + + class Meta: + model = BreakageReport + fields = ['id', 'transaction', 'equipments', 'status', 'timestamp'] + read_only_fields = ['id', 'timestamp'] + + def update(self, instance, validated_data): + transaction = validated_data.get('transaction') + equipments = validated_data.get('equipments') + user = self.context['request'].user + + if 'transaction' in validated_data: + raise serializers.ValidationError({ + 'equipments': 'You cannot change the associated transaction for a breakage report after it has been created' + }) + + if 'equipments' in validated_data: + raise serializers.ValidationError({ + 'equipments': 'You cannot change the equipments in a breakage report after it has been created' + }) + + if not DEBUG: + if not user.is_teacher and 'status' in validated_data and validated_data['status'] != instance.status: + raise serializers.ValidationError( + "You do not have permission to change the status of a breakage report" + ) + return super().update(instance, validated_data) + + def create(self, instance, validated_data): + transaction = validated_data.get('transaction') + equipments = validated_data.get('equipments') + user = self.context['request'].user + if transaction is None: + raise serializers.ValidationError({ + 'equipments': 'Please selected a transaction' + }) + if equipments is None: + raise serializers.ValidationError({ + 'equipments': 'Please select equipments covered by the breakage report' + }) + for equipment in equipments: + if equipment not in transaction.equipments.all(): + raise serializers.ValidationError({ + 'equipments': 'All equipments must be associated with the specified transaction' + }) + if not DEBUG: + if not user.is_teacher and 'status' in validated_data and validated_data['status'] != instance.status: + raise serializers.ValidationError( + "You do not have permission to create a breakage report" + ) + return super().create(validated_data) diff --git a/equipment_tracker/config/settings.py b/equipment_tracker/config/settings.py index 72867d5..b38116d 100644 --- a/equipment_tracker/config/settings.py +++ b/equipment_tracker/config/settings.py @@ -74,6 +74,7 @@ INSTALLED_APPS = [ 'accounts', 'equipments', 'transactions', + 'breakages', ] MIDDLEWARE = [ diff --git a/equipment_tracker/equipments/choices.py b/equipment_tracker/equipments/choices.py deleted file mode 100644 index cbad9b3..0000000 --- a/equipment_tracker/equipments/choices.py +++ /dev/null @@ -1,11 +0,0 @@ -EQUIPMENT_CATEGORY_CHOICES = ( - ('Glassware', 'Glassware'), - ('Miscellaneous', 'Miscellaneous') -) - - -EQUIPMENT_INSTANCE_STATUS_CHOICES = ( - ('Working', 'Working'), - ('Broken', 'Broken'), - ('Borrowed', 'Borrowed'), -) diff --git a/equipment_tracker/equipments/serializers.py b/equipment_tracker/equipments/serializers.py index 45793a7..e878d27 100644 --- a/equipment_tracker/equipments/serializers.py +++ b/equipment_tracker/equipments/serializers.py @@ -3,7 +3,7 @@ from .models import Equipment, EquipmentInstance from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from django.db.models import F - +from breakages.models import BreakageReport # -- Equipment Serializers @@ -147,7 +147,25 @@ class EquipmentInstanceSerializer(serializers.HyperlinkedModelSerializer): # Forbid user from changing equipment field once the instance is already created # Ignore any changes to 'equipment' validated_data.pop('equipment', None) - return super().update(instance, validated_data) + + # This is for Breakage Report handling + # First we update the EquipmentInstance + instance = super().update(instance, validated_data) + # Then we check if the EquipmentInstance has an associated BreakageReport which is still pending + associated_breakage_report = BreakageReport.objects.filter( + equipments=instance, resolved=False).first() + # If there is one + if associated_breakage_report: + # Check if all the equipments of the currently associated BreakageReport are "Working" + all_working = all( + eq.status == 'Working' for eq in associated_breakage_report.equipments.all()) + + # If all the equipments are "Working", set Breakage Report to be resolved (resolved=True) + if all_working: + associated_breakage_report.resolved = True + associated_breakage_report.save() + + return instance # Do not allow users that are not technicians to delete equipment instances def delete(self, instance): diff --git a/equipment_tracker/transactions/migrations/0002_transaction_remarks.py b/equipment_tracker/transactions/migrations/0002_transaction_remarks.py new file mode 100644 index 0000000..d2b8464 --- /dev/null +++ b/equipment_tracker/transactions/migrations/0002_transaction_remarks.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2023-12-08 15:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transactions', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='transaction', + name='remarks', + field=models.TextField(max_length=512, null=True), + ), + ] diff --git a/equipment_tracker/transactions/models.py b/equipment_tracker/transactions/models.py index 4125bf1..21180c1 100644 --- a/equipment_tracker/transactions/models.py +++ b/equipment_tracker/transactions/models.py @@ -27,6 +27,7 @@ class Transaction(models.Model): borrower = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, related_name='borrowed_transactions') + remarks = models.TextField(max_length=512, null=True) teacher = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, related_name='teacher_transactions') equipments = models.ManyToManyField(EquipmentInstance) diff --git a/equipment_tracker/transactions/serializers.py b/equipment_tracker/transactions/serializers.py index e3d13c4..b472689 100644 --- a/equipment_tracker/transactions/serializers.py +++ b/equipment_tracker/transactions/serializers.py @@ -1,7 +1,8 @@ -from rest_framework import serializers +from rest_framework import serializers, exceptions from accounts.models import CustomUser from equipments.models import EquipmentInstance from .models import Transaction +from breakages.models import BreakageReport from accounts.models import CustomUser from config.settings import DEBUG @@ -22,6 +23,11 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): 'equipments', 'transaction_status', 'timestamp'] read_only_fields = ['id', 'timestamp'] + # Do not allow deletion of transactions + def delete(self): + raise exceptions.ValidationError( + "Deletion of transactions is not allowed. Please opt to cancel a transaction or finalize it") + def create(self, validated_data): # Any transactions created will be associated with the one sending the POST/CREATE request user = self.context['request'].user @@ -40,23 +46,23 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): raise serializers.ValidationError( "No borrower assigned for this transaction!") - # If the user in the teacher field != actually a teacher, raise an error + # If the user in the teacher field is not actually a teacher, raise an error borrower = validated_data.get('borrower') if borrower and borrower.is_teacher or borrower.is_technician: raise serializers.ValidationError( "The borrower must be a student. Not a teacher or techician") - # If the user in the teacher field != actually a teacher, raise an error + # If the user in the teacher field is not actually a teacher, raise an error teacher = validated_data.get('teacher') if teacher and not teacher.is_teacher: raise serializers.ValidationError( - "The assigned teacher != a valid teacher") + "The assigned teacher is not a valid teacher") - # If the user in the teacher field != actually a teacher, raise an error + # If the user in the teacher field is not actually a teacher, raise an error teacher = validated_data.get('teacher') if teacher and not teacher.is_teacher: raise serializers.ValidationError( - "The specified user != a teacher.") + "The specified user is not a teacher.") # If there are no equipments specified, raise an error if 'equipments' in validated_data and validated_data['equipments'] == []: @@ -64,27 +70,27 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): "You cannot create a transaction without any equipments selected" ) - return super().create(validated_data) - - def update(self, instance, validated_data): - user = self.context['request'].user - equipments = validated_data['equipments'] - # Check if any of the equipment instances are already in a non-finalized transaction + equipments = validated_data['equipments'] for equipment in equipments: existing__pending_transactions = Transaction.objects.filter( equipments=equipment, status__in=['Pending', 'Approved', 'Borrowed', 'With Breakages: Pending Resolution']) if existing__pending_transactions.exists(): raise serializers.ValidationError( - f"Equipment {equipment.id} is still part of a non-finalized transaction") + f"Cannot add Equipment #{equipment.id}. It is still part of a non-finalized transaction") - # If user != a teacher or a technician, forbid them from changing the status of a transaction + return super().create(validated_data) + + def update(self, instance, validated_data): + user = self.context['request'].user + + # If user is not a teacher or a technician, forbid them from changing the status of a transaction if not user.is_teacher and not user.technician and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.status: raise serializers.ValidationError( "You are not a teacher or technician. You do not have permission to change the status of transactions" ) - # If user != a teacher, forbid them from changing the status of a transaction + # If user is not a teacher, forbid them from changing the status of a transaction if not user.is_teacher and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.status: raise serializers.ValidationError( "You do not have permission to change the status of a transaction" @@ -151,6 +157,20 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): equipment.status = 'Borrowed' equipment.save() return super().update(validated_data) - # Changing equipment status of broken items when returned is handled in breakage reports + # If the transaction changes from Borrowed to Finalized and there are no breakages, label the selected equipment's statuses as Working again from Borrowed + if instance.status == 'Borrowed' and validated_data['transaction_status'] == 'Finalized': + equipments = validated_data.get('equipments', []) + for equipment in equipments: + equipment.status = 'Working' + equipment.save() + return super().update(validated_data) + # If the transaction changes from Borrowed to With Breakages, we create a Breakage Report instance + if instance.status == 'Borrowed' and validated_data['transaction_status'] == 'Finalized': + BreakageReport.objects.create( + transaction=instance, + equipments=instance.equipments.all(), + resolved=False + ) + # Changing equipment status of broken items when there are breakages is handled in breakage reports return super().update(instance, validated_data)