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 class CustomUserSerializer(serializers.ModelSerializer): name = serializers.CharField(source='__str__') class Meta: model = CustomUser fields = ['id', 'name'] class EquipmentInstanceSerializer(serializers.ModelSerializer): name = serializers.CharField(source='equipment.name') class Meta: model = EquipmentInstance fields = ['id', 'name'] class TransactionSerializer(serializers.HyperlinkedModelSerializer): borrower = serializers.SlugRelatedField( many=False, slug_field='id', queryset=CustomUser.objects.all(), required=True, allow_null=False) teacher = serializers.SlugRelatedField( many=False, slug_field='id', queryset=CustomUser.objects.all(), required=True, allow_null=False) equipments = serializers.SlugRelatedField( many=True, slug_field='id', queryset=EquipmentInstance.objects.all(), required=True) timestamp = serializers.DateTimeField( format="%m-%d-%Y %I:%M %p", read_only=True) transaction_status = serializers.ChoiceField( choices=Transaction.TRANSACTION_STATUS_CHOICES) class Meta: model = Transaction fields = ['id', 'borrower', 'teacher', 'equipments', 'transaction_status', 'timestamp'] read_only_fields = ['id', 'timestamp'] def to_representation(self, instance): rep = super().to_representation(instance) rep['borrower'] = CustomUserSerializer(instance.borrower).data rep['teacher'] = CustomUserSerializer(instance.teacher).data rep['equipments'] = EquipmentInstanceSerializer( instance.equipments, many=True).data return rep # 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 validated_data['borrower'] = user # All created transactions will be labelled as Pending validated_data['transaction_status'] = 'Pending Approval' # If no teacher responsible for the borrow transaction is selected, raise an error if 'teacher' not in validated_data: raise serializers.ValidationError( "You have not selected a teacher responsible for your borrow transaction") # If no borrower responsible for the borrow transaction is selected, raise an error if 'borrower' not in validated_data: raise serializers.ValidationError( "No borrower assigned for this transaction!") # 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 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 is not a valid teacher") # 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 is not a teacher.") # If there are no equipments specified, raise an error if 'equipments' in validated_data and validated_data['equipments'] == []: raise serializers.ValidationError( "You cannot create a transaction without any equipments selected" ) # 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, transaction_status__in=['Pending Approval', 'Approved', 'Borrowed', 'With Breakages: Pending Resolution', 'Returned: Pending Checking']) if existing__pending_transactions.exists(): raise serializers.ValidationError( f"Cannot add Equipment ID:{equipment.id}. It is still part of a non-finalized 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.is_technician and 'transaction_status' in validated_data and validated_data['transaction_status'] != instance.transaction_status: raise serializers.ValidationError( "You are not a teacher or technician. You do not have permission to change the status of transactions" ) # 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.transaction_status: raise serializers.ValidationError( "You do not have permission to change the status of a transaction" ) # Do not allow changes to equipments on created transactions if 'equipments' in validated_data and instance.equipments != validated_data['equipments']: raise serializers.ValidationError( "You cannot change the equipments of an already created transaction" ) # For already finalized/done transactions (Rejected or Finalized ones) # Do not allow any changes to already finalized transactions if instance.transaction_status in ['Rejected', 'Finalized']: raise serializers.ValidationError( "Unable to update rejected or finalized transaction. Please create a new one" ) # Check if the update involves the transaction status if 'transaction_status' in validated_data: # For Pending transactions # If not changing to Approved or Rejected, throw an error if instance.transaction_status == "Pending" and (validated_data['transaction_status'] != "Approved" or validated_data['transaction_status'] != "Rejected"): raise serializers.ValidationError( "A pending transaction can only change to Approved or Rejected" ) # For Approved transactions, # If not changing to Borrowed or Cancelled, throw an error # Already approved transactions can only be moved to Borrowed or Cancelled if instance.transaction_status == "Approved" and (validated_data['transaction_status'] != "Borrowed" or validated_data != "Cancelled"): raise serializers.ValidationError( "An already approved transaction can only changed to Borrowed (On borrow) or Cancelled" ) # For Borrowed transactions, # If not changing to returned, throw an error # Borrowed transactions that can only be changed to returned, pending checking for broken items if instance.transaction_status == "Borrowed" and (validated_data['transaction_status'] != "Finalized" or validated_data != "With Breakages: Pending Resolution"): raise serializers.ValidationError( "A borrowed transaction can only changed to status of Finalized or With Breakages: Pending Resolution" ) # For Return: Pending Checking transactions, # If not changing to With Breakages: Pending Resolution or Finalized, throw an error # Returned transactions can only be finalized without any issues or be marked as with breakages if instance.transaction_status == "Returned: Pending Checking" and (validated_data['transaction_status'] != "Finalized" or validated_data != "With Breakages: Pending Resolution"): raise serializers.ValidationError( "A borrowed transaction can only changed to status of Finalized or With Breakages: Pending Resolution" ) # For transactions with pending breakage resolutions, # Do not allow updating of status. It should be updated within its respective breakage report # If it has been resolved there, this field will automatically update to Finalized if instance.transaction_status == "With Breakages: Pending Resolution": raise serializers.ValidationError( "A transaction with pending breakage resolutions must be updated or resolved in its respective breakage report" ) # If there are no issues and a transaction changes from Approved to Borrowed, label the selected equipment's statuses as Borrowed if instance.transaction_status == "Approved" and validated_data['transaction_status'] == "Borrowed": equipments = validated_data.get('equipments', []) for equipment in equipments: equipment.transaction_status = 'Borrowed' equipment.save() return super().update(validated_data) # If the transaction changes from Borrowed to Finalized and there are no breakages, label the selected equipment's statuses as Available again from Borrowed if instance.transaction_status == "Borrowed" and validated_data['transaction_status'] == "Finalized": equipments = validated_data.get('equipments', []) for equipment in equipments: equipment.transaction_status = 'Available' equipment.save() return super().update(validated_data) # If the transaction changes from Borrowed to With Breakages, we create a Breakage Report instance if instance.transaction_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)