metatroncubeswdev c77fb3d0e5 O6: mc_education_exam - grading scales as data, grid entry, report card PDF
Serves Demo Scene 6. mc.grading.scale + mc.grading.interval are pure
data (threshold/letter/point/description rows) - CBSE, ICSE, IB,
Cambridge, Ontario and a plain percentage scale all shipped as data
in data/mc_grading_scale_data.xml, noupdate="1" so a school's own
edits survive a module upgrade. mc.mark.grade resolves via
mc.program.grading_scale_id (new field on mc.program, since grading
board is a per-program concept already carrying "board" from O1) -
the moment this needed an `if board == "CBSE"` anywhere, per
shared/DOMAIN_MODEL.md sec 5, it would have been the wrong design;
verified instead that swapping a program's scale recomputes an
existing mark's grade with zero code involved
(test_changing_scale_recomputes_existing_marks).

Grid mark entry ("whole class on one screen, tab between fields,
keyboard only") is a plain editable list on mc.mark, filtered to one
exam - that native Odoo behavior already gives real Tab-key
navigation with no custom widget. mc.exam.action_enter_marks()
pre-creates a row per actively-enrolled student before opening it
(mirroring O4's bulk-attendance idempotency: creating it again never
duplicates or resets an already-entered mark).

The report card is a real rendered PDF, not just a template read by
inspection - rendered it for Aditya via odoo shell
(report._render_qweb_pdf), pulled the bytes out of the container, and
read the actual PDF: one A4 page, no clipped columns, correct grades
per subject matching the CBSE scale (92->A1, 85->A2, 78->B1, 67->B2,
58->C1), signature block, term correctly resolved. That last part
exposed a real logic bug before it shipped: the term-resolution
method originally preferred "today's date" over "the term with
marks", which would have shown an empty Term 1 when generating the
card *today* even though the demo's graded marks are all in Term 2 -
a report card is generated to review a term's results, typically
after that term's exams are done (often during the *next* term), so
marks-with-data now wins over the calendar, falling back to today's
date only for a student with nothing graded yet.

Attendance summary on the report degrades to None when
mc_education_attendance isn't installed (checked directly, not
assumed) - this module depends on mc_education_base only, matching
"each mc_education_* module installs independently" (CLAUDE.md sec
1.4), while still showing the real summary when both are installed
together for the actual demo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 13:16:49 -04:00

70 lines
2.9 KiB
Python

from odoo import fields, models
class McStudent(models.Model):
_inherit = "mc.student"
def _get_report_card_term(self):
"""The term a report card prints for: the most recent term this
student actually has marks in, or - nothing graded yet - whichever
term's date range covers today. Marks win over the calendar
deliberately: a report card is generated to show a term's *results*,
typically reviewed after that term's exams are done and marks
entered (which is often during the *next* term, not the graded
term itself) - resolving strictly by today's date would show an
empty current term instead of the graded one. Lets "Print Report
Card" work with no extra picker on the common path, per Demo
Scene 6 ("As Principal, generate Aditya's report card" - one
click, no term-selection wizard).
"""
self.ensure_one()
enrollment = self._get_report_card_enrollment()
if not enrollment:
return self.env["mc.academic.term"]
marks = self.env["mc.mark"].search([("student_id", "=", self.id)])
terms_with_marks = marks.exam_id.term_id.filtered(
lambda t: t.year_id == enrollment.year_id
)
if terms_with_marks:
return terms_with_marks.sorted("date_start", reverse=True)[:1]
today = fields.Date.context_today(self)
return self.env["mc.academic.term"].search([
("year_id", "=", enrollment.year_id.id),
("date_start", "<=", today), ("date_end", ">=", today),
], limit=1)
def _get_report_card_enrollment(self):
self.ensure_one()
return self.env["mc.enrollment"].search([
("student_id", "=", self.id), ("state", "=", "active"),
], limit=1)
def _get_report_card_marks(self, term):
self.ensure_one()
return self.env["mc.mark"].search([
("student_id", "=", self.id), ("exam_id.term_id", "=", term.id),
])
def _get_report_card_attendance_summary(self, term):
"""Attendance summary is optional: mc_education_exam does not
depend on mc_education_attendance (each mc_education_* module
installs independently, CLAUDE.md sec 1.4), so this degrades to
None rather than failing when attendance isn't installed.
"""
self.ensure_one()
try:
attendance_model = self.env["mc.attendance"]
except KeyError:
return None
records = attendance_model.search([
("student_id", "=", self.id),
("date", ">=", term.date_start), ("date", "<=", term.date_end),
])
total = len(records)
present = len(records.filtered(lambda a: a.state in ("present", "late")))
return {
"total": total,
"present": present,
"percentage": (present / total * 100) if total else 0.0,
}