Borrowing-TrackerBackend/equipment_tracker/transactions/serializers.py

274 lines
15 KiB
Python

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__')
class Meta:
model = CustomUser
fields = ['id', 'name', 'course', 'section']
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)