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__') course = serializers.CharField() class Meta: model = CustomUser fields = ['id', 'name', 'course'] class EquipmentInstanceSerializer(serializers.ModelSerializer): name = serializers.CharField(source='equipment.name') class Meta: model = EquipmentInstance fields = ['id', 'name', 'status'] 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) subject = serializers.CharField(required=True, allow_null=False) additional_members = serializers.CharField( required=False, allow_null=True, allow_blank=True) remarks = serializers.CharField( required=False, allow_null=True, allow_blank=True) consumables = serializers.CharField( required=False, allow_null=True, allow_blank=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', 'subject', 'equipments', 'remarks', 'transaction_status', 'additional_members', 'consumables', '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") # Create the transaction if there are no issues transaction = super().create(validated_data) # Get the equipments from the newly created transaction equipments = transaction.equipments.all() # Iterate through each of those equipment instances and change their status field to "Pending" # This updates the status field of all equipment instances in a single query EquipmentInstance.objects.filter( id__in=[equipment.id for equipment in equipments]).update(status='Pending') return transaction def update(self, instance, validated_data): user = self.context['request'].user # User Validation # If user is not a teacher or a technician (ie a student), 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.get('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 the user is a teacher but is not assigned to the current transaction, forbid them from changing anything in the transaction if user.is_teacher and instance.teacher != user: raise serializers.ValidationError( "You are not the assigned teacher for this transaction" ) # If the user is a teacher and is updating a transaction with values other than Approved or Rejected, forbid them from doing so (Only technicians can update to other statuses) if user.is_teacher and not (validated_data.get('transaction_status') == 'Rejected' or validated_data.get('transaction_status') == 'Approved'): raise serializers.ValidationError( "Teachers can only mark assigned transactions as Approved or Rejected. Please consult with a Technician should you wish to process this request" ) # Equipment Instances Validation # Do not allow changes to equipments on created transactions if 'equipments' in validated_data: raise serializers.ValidationError( "You cannot change the equipments of an already created transaction" ) # Subject Validation # Do not allow changes to subject on created transactions if 'subject' in validated_data: raise serializers.ValidationError( "You cannot change the subject of an already created transaction" ) # Transaction Status Validation # Check if the update involves changing the transaction status if 'transaction_status' in validated_data and validated_data.get('transaction_status') != instance.transaction_status: # For already finalized/done transactions (Rejected or Finalized ones) # Do not allow any changes to status for 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" ) # For Pending Approval transactions # If not changing to Approved or Rejected, throw an error if instance.transaction_status == "Pending Approval" and not (validated_data.get('transaction_status') == "Approved" or validated_data.get('transaction_status') == "Rejected"): raise serializers.ValidationError( "A pending transaction can only change to Approved or Rejected" ) # If a transaction goes from Pending Approval to Rejected, reset the status of related equipment instances so that they can be included in new transactions if instance.transaction_status == "Pending Approval" and validated_data.get('transaction_status') == "Rejected": equipments = instance.equipments.all() # Iterate through each of those equipment instances and change their status field to "Available" # This updates the status field of all equipment instances in a single query EquipmentInstance.objects.filter( id__in=[equipment.id for equipment in equipments]).update(status='Available') return super().update(instance, validated_data) # 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 not (validated_data.get('transaction_status') == "Borrowed" or validated_data.get('transaction_status') == "Cancelled"): raise serializers.ValidationError( "An already approved transaction can only changed to Borrowed or Cancelled" ) # If the transaction somehow gets Cancelled after being Approved, label the selected equipment's statuses as Available again if instance.transaction_status == "Approved" and validated_data.get('transaction_status') == "Cancelled": equipments = instance.equipments.all() # Iterate through each of those equipment instances and change their status field to "Available" # This updates the status field of all equipment instances in a single query EquipmentInstance.objects.filter( id__in=[equipment.id for equipment in equipments]).update(status='Available') return super().update(instance, validated_data) # 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.get('transaction_status') == "Borrowed": equipments = instance.equipments.all() # Iterate through each of those equipment instances and change their status field to "Borrowed" # This updates the status field of all equipment instances in a single query EquipmentInstance.objects.filter( id__in=[equipment.id for equipment in equipments]).update(status='Borrowed') return super().update(instance, validated_data) # For Borrowed transactions, # If not changing to Returned: Pending Checking, throw an error # Borrowed transactions that are Returned must be checked first upon returning (thus needing Returned: Pending Checking) if instance.transaction_status == "Borrowed" and not validated_data.get('transaction_status') == "Returned: Pending Checking": 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 not (validated_data.get('transaction_status') == "Finalized" or validated_data.get('transaction_status') == "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 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 == "Returned: Pending Checking" and validated_data.get('transaction_status') == "Finalized": equipments = instance.equipments.all() # Iterate through each of those equipment instances and change their status field to "Available" # This updates the status field of all equipment instances in a single query EquipmentInstance.objects.filter( id__in=[equipment.id for equipment in equipments]).update(status='Available') return super().update(instance, validated_data) # If the transaction changes from Returned: Pending Checking to With Breakages, we create a Breakage Report instance if instance.transaction_status == "Returned: Pending Checking" and validated_data.get('transaction_status') == "With Breakages: Pending Resolution": equipments = instance.equipments.all() report = BreakageReport.objects.create( transaction=instance, resolved=False ) report.equipments.set(equipments) return super().update(instance, validated_data) # Changing equipment status of broken items when there are breakages is handled in breakage reports return super().update(instance, validated_data)