# 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)