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>
54 lines
1.9 KiB
Python
54 lines
1.9 KiB
Python
from odoo import fields, models
|
|
|
|
|
|
class McGradingScale(models.Model):
|
|
_name = "mc.grading.scale"
|
|
_description = "Grading Scale"
|
|
_order = "name"
|
|
_rec_name = "name"
|
|
|
|
name = fields.Char(
|
|
string="Name", required=True,
|
|
help="e.g. CBSE, ICSE, IB, Cambridge, Ontario, Percentage.",
|
|
)
|
|
interval_ids = fields.One2many("mc.grading.interval", "scale_id", string="Grade Intervals")
|
|
|
|
_name_uniq = models.Constraint(
|
|
"unique(name)",
|
|
"A grading scale with this name already exists.",
|
|
)
|
|
|
|
def get_grade_interval(self, percentage):
|
|
"""The grade for a percentage is the interval with the highest
|
|
threshold at or below it - e.g. "90 and above = A1", "80-89 = A2".
|
|
Grading rules live here, as data on mc.grading.interval, so a
|
|
different board is configuration, never a code change (O6 spec,
|
|
shared/DOMAIN_MODEL.md sec 5 - the moment you write `if board ==
|
|
"CBSE"` you have already gotten this wrong).
|
|
"""
|
|
self.ensure_one()
|
|
candidates = self.interval_ids.filtered(lambda i: percentage >= i.threshold)
|
|
return candidates.sorted("threshold", reverse=True)[:1]
|
|
|
|
|
|
class McGradingInterval(models.Model):
|
|
_name = "mc.grading.interval"
|
|
_description = "Grading Interval"
|
|
_order = "threshold desc"
|
|
_rec_name = "letter"
|
|
|
|
scale_id = fields.Many2one("mc.grading.scale", string="Scale", required=True, ondelete="cascade")
|
|
threshold = fields.Float(string="Threshold %", required=True)
|
|
letter = fields.Char(string="Letter", required=True)
|
|
point = fields.Float(string="Grade Point")
|
|
description = fields.Char(string="Description")
|
|
|
|
_scale_threshold_uniq = models.Constraint(
|
|
"unique(scale_id, threshold)",
|
|
"This scale already has an interval at this threshold.",
|
|
)
|
|
_threshold_range = models.Constraint(
|
|
"check(threshold >= 0 and threshold <= 100)",
|
|
"A threshold must be between 0 and 100.",
|
|
)
|