2026-09-15 10:47:19 +05:30

279 lines
11 KiB
Python

# pyrefly: ignore [missing-import]
from odoo.tests.common import TransactionCase
# pyrefly: ignore [missing-import]
from odoo.exceptions import ValidationError
class TestStudentFinance(TransactionCase):
def setUp(self):
super(TestStudentFinance, self).setUp()
# Base settings
self.company = self.env.ref('base.main_company')
# Setup master data
self.campus = self.env['education.campus'].create({
'name': 'Finance Test Campus',
'code': 'FTC',
})
self.department = self.env['education.department'].create({
'name': 'Finance Test Dept',
'code': 'FTD',
'campus_id': self.campus.id,
})
self.level = self.env['education.academic.level'].create({
'name': 'Finance Test Level',
'code': 'FTL',
})
self.program = self.env['education.program'].create({
'name': 'Finance Test Program',
'code': 'FTP',
'department_id': self.department.id,
'level_id': self.level.id,
})
self.year = self.env['education.academic.year'].create({
'name': 'Finance Test Year',
'code': 'FTY26',
'start_date': '2026-06-01',
'end_date': '2027-05-31',
})
self.term = self.env['education.academic.term'].create({
'name': 'Finance Test Term 1',
'code': 'FTT1',
'academic_year_id': self.year.id,
'start_date': '2026-06-01',
'end_date': '2026-12-31',
})
self.batch = self.env['education.batch'].create({
'name': 'Finance Test Batch 2026',
'code': 'FTB26',
})
self.semester = self.env['education.semester'].create({
'name': 'Finance Test Semester 1',
'code': 'FTS1',
})
self.section = self.env['education.section'].create({
'name': 'Finance Test Section A',
'code': 'FTSA',
})
# Create student
self.student = self.env['education.student'].create({
'first_name': 'John',
'last_name': 'Doe',
'campus_id': self.campus.id,
'department_id': self.department.id,
'program_id': self.program.id,
'level_id': self.level.id,
'academic_year_id': self.year.id,
'academic_term_id': self.term.id,
'batch_id': self.batch.id,
'semester_id': self.semester.id,
'section_id': self.section.id,
})
# Create Fee Config
self.category = self.env['education.fee.category'].create({
'name': 'Finance Test Tuition Category',
'code': 'FTUT',
})
self.fee_type = self.env['education.fee.type'].create({
'name': 'Finance Test Tuition Fee',
'category_id': self.category.id,
'amount': 1000.0,
})
self.fee_type_lab = self.env['education.fee.type'].create({
'name': 'Finance Test Lab Fee',
'category_id': self.category.id,
'amount': 500.0,
})
self.fee_structure = self.env['education.fee.structure'].create({
'name': 'Finance Test Year 1 Structure',
'academic_year_id': self.year.id,
'level_id': self.level.id,
})
self.fee_line = self.env['education.fee.structure.line'].create({
'fee_structure_id': self.fee_structure.id,
'fee_type_id': self.fee_type.id,
'amount': 1000.0,
})
self.fee_line_lab = self.env['education.fee.structure.line'].create({
'fee_structure_id': self.fee_structure.id,
'fee_type_id': self.fee_type_lab.id,
'amount': 500.0,
})
def test_fee_assignment_and_ledger(self):
"""Test assigning fees to a student creates ledger debits and updates metrics"""
assignment = self.env['education.fee.assignment'].create({
'student_id': self.student.id,
'academic_year_id': self.year.id,
'semester_id': self.semester.id,
'fee_structure_id': self.fee_structure.id,
})
# Verify initial metrics
self.assertEqual(self.student.outstanding_amount, 0.0)
# Confirm Assignment
assignment.action_confirm()
self.student.invalidate_recordset()
# Ledger entry verification
ledger_lines = self.env['education.student.ledger'].search([('student_id', '=', self.student.id)], order='date asc, id asc')
# We have 2 lines from the structure
self.assertEqual(len(ledger_lines), 2)
self.assertEqual(ledger_lines[0].debit, 1000.0)
self.assertEqual(ledger_lines[1].debit, 500.0)
# Verify running balance
self.assertEqual(ledger_lines[0].balance, 1000.0)
self.assertEqual(ledger_lines[1].balance, 1500.0)
# Financial metrics check
self.assertEqual(self.student.total_fees, 1500.0)
self.assertEqual(self.student.outstanding_amount, 1500.0)
self.assertEqual(self.student.payment_status, 'overdue')
def test_partial_and_multiple_payments(self):
"""Test registering partial and multiple payments updates outstanding dues and running balance"""
assignment = self.env['education.fee.assignment'].create({
'student_id': self.student.id,
'academic_year_id': self.year.id,
'semester_id': self.semester.id,
'fee_structure_id': self.fee_structure.id,
})
assignment.action_confirm()
# First payment: partial (500.0)
payment1 = self.env['education.student.payment'].create({
'student_id': self.student.id,
'amount': 500.0,
'payment_method': 'cash',
'receipt_number': 'TEST-PARTIAL-1',
})
payment1.action_confirm()
self.student.invalidate_recordset()
self.assertEqual(self.student.total_paid, 500.0)
self.assertEqual(self.student.outstanding_amount, 1000.0)
self.assertEqual(self.student.payment_status, 'partial')
# Verify running balances
ledger_lines = self.env['education.student.ledger'].search([('student_id', '=', self.student.id)], order='date asc, id asc')
# 2 debit lines + 1 credit line = 3 lines total
self.assertEqual(len(ledger_lines), 3)
self.assertEqual(ledger_lines[2].balance, 1000.0)
# Second payment: full remaining (1000.0)
payment2 = self.env['education.student.payment'].create({
'student_id': self.student.id,
'amount': 1000.0,
'payment_method': 'online',
'receipt_number': 'TEST-PARTIAL-2',
})
payment2.action_confirm()
self.student.invalidate_recordset()
self.assertEqual(self.student.total_paid, 1500.0)
self.assertEqual(self.student.outstanding_amount, 0.0)
self.assertEqual(self.student.payment_status, 'fully_paid')
ledger_lines_all = self.env['education.student.ledger'].search([('student_id', '=', self.student.id)], order='date asc, id asc')
self.assertEqual(len(ledger_lines_all), 4)
self.assertEqual(ledger_lines_all[3].balance, 0.0)
def test_overpayment_validation(self):
"""Test that paying more than outstanding raises validation error"""
assignment = self.env['education.fee.assignment'].create({
'student_id': self.student.id,
'academic_year_id': self.year.id,
'semester_id': self.semester.id,
'fee_structure_id': self.fee_structure.id,
})
assignment.action_confirm()
self.student.invalidate_recordset()
payment = self.env['education.student.payment'].create({
'student_id': self.student.id,
'amount': 2000.0,
'payment_method': 'cash',
'receipt_number': 'TEST-OVERPAY',
})
with self.assertRaises(ValidationError):
payment.action_confirm()
def test_scholarships_and_discounts(self):
"""Test applying discounts and scholarships reduces dues and updates ledger running balance"""
assignment = self.env['education.fee.assignment'].create({
'student_id': self.student.id,
'academic_year_id': self.year.id,
'semester_id': self.semester.id,
'fee_structure_id': self.fee_structure.id,
})
assignment.action_confirm()
# Apply Discount
discount = self.env['education.student.discount'].create({
'student_id': self.student.id,
'type': 'fixed',
'amount': 200.0,
'reason': 'Merit Discount',
})
discount.action_confirm()
self.student.invalidate_recordset()
self.assertEqual(self.student.outstanding_amount, 1300.0)
# Apply Scholarship
scholarship = self.env['education.student.scholarship'].create({
'student_id': self.student.id,
'scholarship_name': 'Sponsor Grant',
'type': 'percentage',
'amount': 10.0, # 10% of 1300 = 130
})
scholarship.action_confirm()
self.student.invalidate_recordset()
self.assertEqual(self.student.outstanding_amount, 1170.0)
def test_refunds(self):
"""Test refund creates outstanding ledger entries to increase the running balance"""
assignment = self.env['education.fee.assignment'].create({
'student_id': self.student.id,
'academic_year_id': self.year.id,
'semester_id': self.semester.id,
'fee_structure_id': self.fee_structure.id,
})
assignment.action_confirm()
payment = self.env['education.student.payment'].create({
'student_id': self.student.id,
'amount': 1000.0,
'payment_method': 'cash',
'receipt_number': 'TEST-REFUND-PAY',
})
payment.action_confirm()
self.student.invalidate_recordset()
self.assertEqual(self.student.outstanding_amount, 500.0)
# Try to refund more than paid
with self.assertRaises(ValidationError):
self.env['education.student.refund'].create({
'payment_id': payment.id,
'amount': 1200.0,
'reason': 'Accidental Over-Refund',
})
# Valid refund of 500
refund_pass = self.env['education.student.refund'].create({
'payment_id': payment.id,
'amount': 500.0,
'reason': 'Course Withdrawal Partial',
})
refund_pass.action_approve()
self.student.invalidate_recordset()
# Outstanding should increase by 500 (from 500 to 1000)
self.assertEqual(self.student.outstanding_amount, 1000.0)