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>
70 lines
2.9 KiB
Python
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,
|
|
}
|