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 from django.core.cache import cache 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): cache.delete('non_finalized_transactions') 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') q = super().update(instance, validated_data) cache.delete('available_equipment_instances') cache.delete('equipment_instances') return q # 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') q = super().update(instance, validated_data) cache.delete('available_equipment_instances') cache.delete('equipment_instances') return q # 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') q = super().update(instance, validated_data) cache.delete('available_equipment_instances') cache.delete('equipment_instances') return q # 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)