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>
62 lines
2.6 KiB
Python
62 lines
2.6 KiB
Python
from odoo import api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class McExam(models.Model):
|
|
_name = "mc.exam"
|
|
_inherit = ["mail.thread"]
|
|
_description = "Exam"
|
|
_order = "date desc"
|
|
_rec_name = "display_name"
|
|
|
|
name = fields.Char(string="Name", required=True)
|
|
term_id = fields.Many2one("mc.academic.term", string="Term", required=True, ondelete="restrict")
|
|
batch_id = fields.Many2one("mc.batch", string="Batch", required=True, ondelete="restrict")
|
|
subject_id = fields.Many2one("mc.subject", string="Subject", required=True, ondelete="restrict")
|
|
max_marks = fields.Float(string="Max Marks", required=True, default=100)
|
|
pass_marks = fields.Float(string="Pass Marks", required=True, default=35)
|
|
date = fields.Date(string="Date", required=True)
|
|
mark_ids = fields.One2many("mc.mark", "exam_id", string="Marks")
|
|
|
|
_batch_subject_term_uniq = models.Constraint(
|
|
"unique(batch_id, subject_id, term_id, name)",
|
|
"An exam with this name already exists for this batch, subject and term.",
|
|
)
|
|
|
|
@api.depends("name", "batch_id.name", "subject_id.name")
|
|
def _compute_display_name(self):
|
|
for exam in self:
|
|
exam.display_name = "%s - %s (%s)" % (
|
|
exam.name, exam.batch_id.name or "?", exam.subject_id.name or "?",
|
|
)
|
|
|
|
@api.constrains("max_marks", "pass_marks")
|
|
def _check_marks_positive(self):
|
|
for exam in self:
|
|
if exam.max_marks <= 0:
|
|
raise ValidationError("Max marks must be greater than zero.")
|
|
if exam.pass_marks < 0 or exam.pass_marks > exam.max_marks:
|
|
raise ValidationError("Pass marks must be between 0 and max marks.")
|
|
|
|
def action_enter_marks(self):
|
|
self.ensure_one()
|
|
enrollments = self.env["mc.enrollment"].search([
|
|
("batch_id", "=", self.batch_id.id), ("state", "=", "active"),
|
|
])
|
|
existing_student_ids = set(self.mark_ids.mapped("student_id").ids)
|
|
to_create = [
|
|
{"exam_id": self.id, "student_id": enrollment.student_id.id, "marks_obtained": 0}
|
|
for enrollment in enrollments
|
|
if enrollment.student_id.id not in existing_student_ids
|
|
]
|
|
if to_create:
|
|
self.env["mc.mark"].create(to_create)
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"name": "Enter Marks - %s" % self.display_name,
|
|
"res_model": "mc.mark",
|
|
"view_mode": "list",
|
|
"domain": [("exam_id", "=", self.id)],
|
|
"context": {"default_exam_id": self.id},
|
|
}
|