from odoo.exceptions import UserError from odoo.tests.common import TransactionCase class TestFeeInvoice(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.year = cls.env["mc.academic.year"].create({ "name": "TEST-INV-2026-27", "date_start": "2026-06-01", "date_end": "2027-04-30", }) cls.term1 = cls.env["mc.academic.term"].create({ "name": "TEST INV Term 1", "year_id": cls.year.id, "date_start": "2026-06-01", "date_end": "2026-09-30", }) cls.term2 = cls.env["mc.academic.term"].create({ "name": "TEST INV Term 2", "year_id": cls.year.id, "date_start": "2026-10-01", "date_end": "2026-12-31", }) cls.program = cls.env["mc.program"].create({ "name": "TEST INV Program", "code": "TEST-INV-P1", "sequence_no": 1, "display_label": "Test Grade", }) cls.batch = cls.env["mc.batch"].create({ "name": "TEST INV Batch", "program_id": cls.program.id, "year_id": cls.year.id, }) cls.category_tuition = cls.env["mc.fee.category"].create({"name": "TEST INV Tuition"}) cls.category_lab = cls.env["mc.fee.category"].create({"name": "TEST INV Lab"}) cls.structure = cls.env["mc.fee.structure"].create({ "program_id": cls.program.id, "year_id": cls.year.id, "line_ids": [ (0, 0, {"category_id": cls.category_tuition.id, "amount": 1000}), (0, 0, {"category_id": cls.category_lab.id, "amount": 500}), ], }) # Total 1500. Term 1 gets 40%, Term 2 gets 60%, so the Term 2 # invoice generated in tests should be 900 before any concession. cls.schedule = cls.env["mc.fee.schedule"].create({"structure_id": cls.structure.id}) cls.env["mc.fee.schedule.line"].create({ "schedule_id": cls.schedule.id, "term_id": cls.term1.id, "due_date": "2026-06-15", "percentage": 40, }) cls.env["mc.fee.schedule.line"].create({ "schedule_id": cls.schedule.id, "term_id": cls.term2.id, "due_date": "2026-10-15", "percentage": 60, }) student_partner = cls.env["res.partner"].create({"name": "Fee Test Student"}) cls.student = cls.env["mc.student"].create({ "partner_id": student_partner.id, "name": "Fee Test Student", }) guardian_partner = cls.env["res.partner"].create({"name": "Fee Test Guardian"}) cls.guardian = cls.env["mc.guardian"].create({ "partner_id": guardian_partner.id, "name": "Fee Test Guardian", }) cls.env["mc.student.guardian"].create({ "student_id": cls.student.id, "guardian_id": cls.guardian.id, "relationship": "father", "is_primary": True, }) cls.enrollment = cls.env["mc.enrollment"].create({ "student_id": cls.student.id, "program_id": cls.program.id, "batch_id": cls.batch.id, "year_id": cls.year.id, "state": "active", }) def _generate(self, term): wizard = self.env["mc.fee.invoice.generate.wizard"].create({ "enrollment_id": self.enrollment.id, "term_id": term.id, }) action = wizard.action_generate() return self.env["account.move"].browse(action["res_id"]) def test_invoice_amount_scaled_by_term_percentage(self): invoice = self._generate(self.term2) self.assertEqual(invoice.amount_total, 900) # 60% of 1500 self.assertEqual(invoice.mc_student_id, self.student) self.assertEqual(invoice.mc_enrollment_id, self.enrollment) # Bills the guardian, not the child, so stock portal invoice # visibility (partner_id-based) works with no extra record rule. self.assertEqual(invoice.partner_id, self.guardian.partner_id) def test_invoice_has_one_line_per_category(self): invoice = self._generate(self.term1) self.assertEqual(len(invoice.invoice_line_ids), 2) self.assertEqual(invoice.amount_total, 600) # 40% of 1500 def test_percent_concession_recalculates_invoice(self): self.env["mc.fee.concession"].create({ "student_id": self.student.id, "concession_type": "sibling", "computation": "percent", "value": 15, "reason": "Test sibling concession", "approver_id": self.env.uid, }) invoice = self._generate(self.term2) # 900 gross, 15% off = 765. self.assertEqual(invoice.amount_total, 765) concession_lines = invoice.invoice_line_ids.filtered(lambda l: l.price_subtotal < 0) self.assertEqual(len(concession_lines), 1) self.assertEqual(concession_lines.price_subtotal, -135) self.assertIn("Test sibling concession", concession_lines.name) def test_fixed_concession_recalculates_invoice(self): self.env["mc.fee.concession"].create({ "student_id": self.student.id, "concession_type": "need_based", "computation": "fixed", "value": 200, "reason": "Test fixed concession", "approver_id": self.env.uid, }) invoice = self._generate(self.term2) self.assertEqual(invoice.amount_total, 700) # 900 - 200 def test_inactive_concession_not_applied(self): concession = self.env["mc.fee.concession"].create({ "student_id": self.student.id, "concession_type": "merit", "computation": "percent", "value": 50, "reason": "Test inactive concession", "approver_id": self.env.uid, }) concession.active = False invoice = self._generate(self.term2) self.assertEqual(invoice.amount_total, 900) def test_generating_without_matching_schedule_line_raises(self): other_term = self.env["mc.academic.term"].create({ "name": "TEST INV Term 3", "year_id": self.year.id, "date_start": "2027-01-01", "date_end": "2027-04-30", }) with self.assertRaises(UserError): self._generate(other_term) def test_generating_with_incomplete_schedule_raises(self): # Total is only 40% for this structure - must be caught at # generation time, not silently under-billed. program2 = self.env["mc.program"].create({ "name": "TEST INV Program 2", "code": "TEST-INV-P2", "sequence_no": 2, "display_label": "Test Grade 2", }) batch2 = self.env["mc.batch"].create({ "name": "TEST INV Batch 2", "program_id": program2.id, "year_id": self.year.id, }) structure2 = self.env["mc.fee.structure"].create({ "program_id": program2.id, "year_id": self.year.id, "line_ids": [(0, 0, {"category_id": self.category_tuition.id, "amount": 1000})], }) schedule2 = self.env["mc.fee.schedule"].create({"structure_id": structure2.id}) self.env["mc.fee.schedule.line"].create({ "schedule_id": schedule2.id, "term_id": self.term1.id, "due_date": "2026-06-15", "percentage": 40, }) enrollment2 = self.env["mc.enrollment"].create({ "student_id": self.student.id, "program_id": program2.id, "batch_id": batch2.id, "year_id": self.year.id, "state": "draft", }) wizard = self.env["mc.fee.invoice.generate.wizard"].create({ "enrollment_id": enrollment2.id, "term_id": self.term1.id, }) with self.assertRaises(UserError): wizard.action_generate()