Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
_search_certificates,
_student_queryset,
build_certificate_context,
superscript_ordinal,
)
from .demand_letter import financial_year_for
from .fee_certificate import _is_concession, fee_structure_for, indian_currency
Expand Down Expand Up @@ -85,6 +86,16 @@ def _column_total(rows, key):
return sum((Decimal(row[key]) for row in rows), Decimal('0'))


def financial_year_choices(student, structure):
if not structure:
return []
choices = []
for odd in range(1, structure.semester_count + 1, 2):
_, label = financial_year_for(student, odd)
choices.append({'value': odd, 'label': label})
return choices


def render_fee_structure_pdf(context, structure, odd_semester, academic_year_label,
rows, reference_number, issued_on):
output = BytesIO()
Expand Down Expand Up @@ -163,7 +174,8 @@ def value_cell(text):
value_cell(f'Mr. {context["father_name"]}'), '', ''],
[label_cell('Roll No:'), value_cell(context['roll_number']),
label_cell('Branch:'), value_cell(context['discipline'])],
[label_cell('Semester:'), value_cell(context['semester_ordinal']),
[label_cell('Semester:'),
Paragraph(superscript_ordinal(context['semester_ordinal']), value_style),
label_cell('Programme:'), value_cell(context['programme_short'])],
[label_cell('Financial Year:'), value_cell(academic_year_label), '', ''],
],
Expand Down Expand Up @@ -276,13 +288,24 @@ def fee_structure_certificate_student(request):

rows = []
academic_year_label = ''
odd_semester = None
if structure is None:
batch_label = context.get('programme') or 'unrecognised'
errors.append(
f'No fee structure is recorded for {batch_label} programmes. '
'Add one before issuing this certificate.')
elif context['semester']:
odd_semester, _ = session_semesters(context['semester'])
else:
default_odd_semester = (
session_semesters(context['semester'])[0] if context['semester'] else 1)
valid_odds = list(range(1, structure.semester_count + 1, 2))
semester_param = request.query_params.get('semester')
try:
odd_semester = int(semester_param) if semester_param else default_odd_semester
except (TypeError, ValueError):
odd_semester = default_odd_semester
if odd_semester not in valid_odds:
odd_semester = (default_odd_semester if default_odd_semester in valid_odds
else valid_odds[0])
rows = build_structure_rows(student, structure, odd_semester)
_, academic_year_label = financial_year_for(student, odd_semester)

Expand All @@ -293,6 +316,8 @@ def fee_structure_certificate_student(request):

return Response({
'student': {**context, 'is_ready': not errors, 'validation_errors': errors},
'financial_years': financial_year_choices(student, structure),
'selected_semester': odd_semester,
'structure': {
'rows': rows,
'academic_year_label': academic_year_label,
Expand All @@ -318,6 +343,7 @@ def fee_structure_certificate_student(request):
@role_required(['acadadmin'])
def generate_fee_structure_pdf(request):
student_id = request.data.get('student_id')
semester_raw = request.data.get('semester')
if not student_id:
return Response({'error': 'student_id is required.'},
status=status.HTTP_400_BAD_REQUEST)
Expand All @@ -337,7 +363,19 @@ def generate_fee_structure_pdf(request):
{'error': 'No fee structure is recorded for this programme.'},
status=status.HTTP_400_BAD_REQUEST)

odd_semester, _ = session_semesters(context['semester'])
default_odd_semester = (
session_semesters(context['semester'])[0] if context['semester'] else 1)
try:
odd_semester = (int(semester_raw) if semester_raw not in (None, '')
else default_odd_semester)
except (TypeError, ValueError):
return Response({'error': 'Select a valid financial year.'},
status=status.HTTP_400_BAD_REQUEST)
if odd_semester not in range(1, structure.semester_count + 1, 2):
return Response(
{'error': 'Financial year is outside the published fee structure.'},
status=status.HTTP_400_BAD_REQUEST)

rows = build_structure_rows(student, structure, odd_semester)
_, academic_year_label = financial_year_for(student, odd_semester)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,52 @@ def test_concession_students_get_the_concession_figures(self):
self.assertEqual(tuition_row['sem1'], '0.00')


class FinancialYearSelectionTests(FeeStructureCertificateTestCase):
def test_financial_year_choices_cover_every_session(self):
choices = fsc.financial_year_choices(self.student, self.structure)
self.assertEqual([c['value'] for c in choices], [1, 3, 5, 7])
self.assertEqual(choices[0]['label'], '2026-27')
self.assertEqual(choices[3]['label'], '2029-30')

def test_student_endpoint_defaults_to_the_current_session(self):
student = self._student('26BCS3001', semester=7)
response = self.client.get(
'/academic-procedures/api/acad/fee-structure-certificate/student/',
{'roll_number': student.pk})
data = response.json()
self.assertEqual(data['selected_semester'], 7)
self.assertEqual(data['structure']['academic_year_label'], '2029-30')

def test_student_endpoint_honours_an_explicit_semester(self):
response = self.client.get(
'/academic-procedures/api/acad/fee-structure-certificate/student/',
{'roll_number': self.student.pk, 'semester': 5})
data = response.json()
self.assertEqual(data['selected_semester'], 5)
self.assertEqual(data['structure']['academic_year_label'], '2028-29')

def test_student_endpoint_falls_back_on_an_invalid_semester(self):
response = self.client.get(
'/academic-procedures/api/acad/fee-structure-certificate/student/',
{'roll_number': self.student.pk, 'semester': 4})
self.assertEqual(response.json()['selected_semester'], 1)

def test_pdf_generation_honours_an_explicit_semester(self):
response = self.client.post(
'/academic-procedures/api/acad/fee-structure-certificate/pdf/',
{'student_id': self.student.pk, 'semester': 5}, format='json')
self.assertEqual(response.status_code, 200)
certificate = BonafideCertificate.objects.get(
certificate_type='fee_structure')
self.assertEqual(certificate.purpose, 'Fee Structure 2028-29')

def test_pdf_generation_rejects_a_semester_outside_the_structure(self):
response = self.client.post(
'/academic-procedures/api/acad/fee-structure-certificate/pdf/',
{'student_id': self.student.pk, 'semester': 9}, format='json')
self.assertEqual(response.status_code, 400)


class IssuedCertificateTests(FeeStructureCertificateTestCase):
def _generate(self):
return self.client.post(
Expand Down
Loading