O1: complete the gate - program, subject, room, batch, people, enrollment

Rounds out mc_education_base with the remaining O1 models: mc.program,
mc.subject, mc.room, mc.batch, mc.teacher, mc.student, mc.guardian,
mc.student.guardian, and mc.enrollment - the spine everything else in
the O1 table (attendance, timetable, exam, portal) is built against.

Two rules get the same "DB constraint is the real guarantee, the ORM
does a friendly pre-check" treatment as academic.year.is_current:

  - At most one primary guardian per student (mc.student.guardian):
    partial unique index on student_id WHERE is_primary, plus a
    create/write toggle.
  - At most one Active enrollment per student per year
    (mc.enrollment): partial unique index on (student_id, year_id)
    WHERE state='active'. This one is CLAUDE.md's flagship rule
    ("Enforce as a database constraint, not application logic").

Two more real bugs surfaced by testing against a live odoo:19.0
container rather than trusting the code by inspection:

  - The partial unique index fires at INSERT/UPDATE time, before
    @api.constrains ever runs - so a naive "index + constrains for a
    friendly message" design never reaches the friendly message, the
    raw IntegrityError wins the race. Fixed by pre-checking for a
    conflict in create()/write() before calling super(), with
    @api.constrains kept only as a backstop for batch creates.
  - create() issues a direct SQL INSERT that does not wait for
    unrelated pending writes in the ORM cache (e.g. withdrawing one
    enrollment right before creating its replacement, in the same
    method) - needs an explicit self.env.flush_all() first, same
    lesson as the is_current toggle.

Also caught before it became a permanent test flake: the enrollment
and academic-calendar tests originally hardcoded the same year names
("2025-26", "2026-27") and program code ("G8") as the demo data.
Passed in isolation, failed as soon as demo data was loaded first -
so verification here included a combined
`--without-demo=False --test-enable` run, matching what CI actually
does, not just an isolated test-tagged run. Renamed to TEST-prefixed
fixtures.

Security access rows added for all new models across the four
internal groups (Administrator: full CRUD everywhere; Staff: full
CRUD on the people/enrollment models that are front office's daily
job, read-only on academic structure; Teacher/Accountant: read-only
across the board, narrower record rules land with the modules that
need them - attendance, exam, portal). No portal-group access yet;
that is O7's job once explicit ownership-scoped record rules exist -
granting it now without those rules would be exactly the "identifier
supplied by the client" hole CLAUDE.md's standing security rule
warns about.

Demo data populates the shared/DEMO_SCRIPT.md cast: Meera Krishnan as
primary guardian of both Aditya (Grade 8-A) and Ananya (Grade 5-B) -
the multi-child guardian view the script calls "the single most
convincing portal feature" - plus Arun Prakash as Grade 8-A's class
teacher. Verified by querying the resulting database directly, not
just by the install succeeding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-09-11 09:58:36 -04:00
parent 1e5ac14510
commit 709bd2eca7
33 changed files with 1263 additions and 10 deletions

View File

@ -9,13 +9,29 @@
"data": [ "data": [
"security/mc_education_security.xml", "security/mc_education_security.xml",
"security/ir.model.access.csv", "security/ir.model.access.csv",
"data/mc_sequence_data.xml",
"views/mc_academic_year_views.xml", "views/mc_academic_year_views.xml",
"views/mc_academic_term_views.xml", "views/mc_academic_term_views.xml",
"views/mc_room_views.xml",
"views/mc_program_views.xml",
"views/mc_subject_views.xml",
"views/mc_teacher_views.xml",
"views/mc_batch_views.xml",
"views/mc_student_views.xml",
"views/mc_guardian_views.xml",
"views/mc_enrollment_views.xml",
"views/mc_education_menus.xml", "views/mc_education_menus.xml",
], ],
"demo": [ "demo": [
"demo/mc_academic_year_demo.xml", "demo/mc_academic_year_demo.xml",
"demo/mc_academic_term_demo.xml", "demo/mc_academic_term_demo.xml",
"demo/mc_room_demo.xml",
"demo/mc_program_demo.xml",
"demo/mc_subject_demo.xml",
"demo/mc_teacher_demo.xml",
"demo/mc_batch_demo.xml",
"demo/mc_people_demo.xml",
"demo/mc_enrollment_demo.xml",
], ],
"installable": True, "installable": True,
"application": True, "application": True,

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Configurable per company: add another ir.sequence with the same
code and a specific company_id to override the format for one
school without touching code. -->
<record id="seq_mc_student_admission_no" model="ir.sequence">
<field name="name">Student Admission Number</field>
<field name="code">mc.student.admission_no</field>
<field name="prefix">ADM%(year)s</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="demo_batch_grade8_a" model="mc.batch">
<field name="name">Grade 8-A</field>
<field name="program_id" ref="demo_program_grade8"/>
<field name="year_id" ref="demo_year_2026_27"/>
<field name="class_teacher_id" ref="demo_employee_arun_prakash"/>
<field name="room_id" ref="demo_room_101"/>
<field name="capacity">40</field>
</record>
<record id="demo_batch_grade5_b" model="mc.batch">
<field name="name">Grade 5-B</field>
<field name="program_id" ref="demo_program_grade5"/>
<field name="year_id" ref="demo_year_2026_27"/>
<field name="room_id" ref="demo_room_102"/>
<field name="capacity">35</field>
</record>
</odoo>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="demo_enrollment_aditya" model="mc.enrollment">
<field name="student_id" ref="demo_student_aditya_krishnan"/>
<field name="program_id" ref="demo_program_grade8"/>
<field name="batch_id" ref="demo_batch_grade8_a"/>
<field name="year_id" ref="demo_year_2026_27"/>
<field name="state">active</field>
<field name="roll_no">12</field>
<field name="date_enrolled">2026-06-01</field>
</record>
<record id="demo_enrollment_ananya" model="mc.enrollment">
<field name="student_id" ref="demo_student_ananya_krishnan"/>
<field name="program_id" ref="demo_program_grade5"/>
<field name="batch_id" ref="demo_batch_grade5_b"/>
<field name="year_id" ref="demo_year_2026_27"/>
<field name="state">active</field>
<field name="roll_no">7</field>
<field name="date_enrolled">2026-06-01</field>
</record>
</odoo>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Cast from shared/DEMO_SCRIPT.md: Meera Krishnan has two children,
Aditya (Grade 8-A) and Ananya (Grade 5-B) - the multi-child
guardian view is the single most convincing portal feature and
where sibling-linking bugs surface, so it's demo data from O1
on, not bolted on later in O7. -->
<record id="demo_partner_meera_krishnan" model="res.partner">
<field name="name">Meera Krishnan</field>
<field name="email">parent@demo.school</field>
<field name="phone">+91 98765 43210</field>
<field name="city">Coimbatore</field>
</record>
<record id="demo_guardian_meera_krishnan" model="mc.guardian">
<field name="partner_id" ref="demo_partner_meera_krishnan"/>
<field name="name">Meera Krishnan</field>
<field name="occupation">Software Engineer</field>
<field name="phone">+91 98765 43210</field>
<field name="email">parent@demo.school</field>
</record>
<record id="demo_partner_aditya_krishnan" model="res.partner">
<field name="name">Aditya Krishnan</field>
<field name="email">student@demo.school</field>
<field name="city">Coimbatore</field>
</record>
<record id="demo_student_aditya_krishnan" model="mc.student">
<field name="partner_id" ref="demo_partner_aditya_krishnan"/>
<field name="name">Aditya Krishnan</field>
<field name="dob">2013-04-12</field>
<field name="gender">male</field>
<field name="admission_date">2020-06-01</field>
<field name="status">active</field>
<field name="blood_group">o+</field>
</record>
<record id="demo_partner_ananya_krishnan" model="res.partner">
<field name="name">Ananya Krishnan</field>
<field name="city">Coimbatore</field>
</record>
<record id="demo_student_ananya_krishnan" model="mc.student">
<field name="partner_id" ref="demo_partner_ananya_krishnan"/>
<field name="name">Ananya Krishnan</field>
<field name="dob">2016-09-23</field>
<field name="gender">female</field>
<field name="admission_date">2022-06-01</field>
<field name="status">active</field>
<field name="blood_group">o+</field>
</record>
<record id="demo_link_meera_aditya" model="mc.student.guardian">
<field name="student_id" ref="demo_student_aditya_krishnan"/>
<field name="guardian_id" ref="demo_guardian_meera_krishnan"/>
<field name="relationship">mother</field>
<field name="is_primary">True</field>
</record>
<record id="demo_link_meera_ananya" model="mc.student.guardian">
<field name="student_id" ref="demo_student_ananya_krishnan"/>
<field name="guardian_id" ref="demo_guardian_meera_krishnan"/>
<field name="relationship">mother</field>
<field name="is_primary">True</field>
</record>
</odoo>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="demo_program_grade5" model="mc.program">
<field name="name">Grade 5 CBSE</field>
<field name="code">G5</field>
<field name="sequence_no">5</field>
<field name="display_label">Grade 5</field>
<field name="board">CBSE</field>
</record>
<record id="demo_program_grade8" model="mc.program">
<field name="name">Grade 8 CBSE</field>
<field name="code">G8</field>
<field name="sequence_no">8</field>
<field name="display_label">Grade 8</field>
<field name="board">CBSE</field>
</record>
</odoo>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="demo_room_101" model="mc.room">
<field name="name">Room 101</field>
<field name="building">Main Block</field>
<field name="type">classroom</field>
<field name="capacity">40</field>
</record>
<record id="demo_room_102" model="mc.room">
<field name="name">Room 102</field>
<field name="building">Main Block</field>
<field name="type">classroom</field>
<field name="capacity">35</field>
</record>
</odoo>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="demo_subject_math" model="mc.subject">
<field name="name">Mathematics</field>
<field name="code">MATH</field>
<field name="program_ids" eval="[(6, 0, [ref('demo_program_grade5'), ref('demo_program_grade8')])]"/>
</record>
<record id="demo_subject_english" model="mc.subject">
<field name="name">English</field>
<field name="code">ENG</field>
<field name="program_ids" eval="[(6, 0, [ref('demo_program_grade5'), ref('demo_program_grade8')])]"/>
</record>
<record id="demo_subject_science" model="mc.subject">
<field name="name">Science</field>
<field name="code">SCI</field>
<field name="program_ids" eval="[(6, 0, [ref('demo_program_grade5'), ref('demo_program_grade8')])]"/>
</record>
<record id="demo_subject_social_studies" model="mc.subject">
<field name="name">Social Studies</field>
<field name="code">SST</field>
<field name="program_ids" eval="[(6, 0, [ref('demo_program_grade5'), ref('demo_program_grade8')])]"/>
</record>
<record id="demo_subject_hindi" model="mc.subject">
<field name="name">Hindi</field>
<field name="code">HIN</field>
<field name="is_elective">True</field>
<field name="program_ids" eval="[(6, 0, [ref('demo_program_grade5'), ref('demo_program_grade8')])]"/>
</record>
</odoo>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Cast from shared/DEMO_SCRIPT.md. res.users/login creation is O9's
job (demo rehearsal), not this base module - this only creates the
employee and academic-attribute records the rest of O1 hangs off. -->
<record id="demo_employee_arun_prakash" model="hr.employee">
<field name="name">Arun Prakash</field>
<field name="work_email">teacher@demo.school</field>
<field name="job_title">Mathematics Teacher</field>
</record>
<record id="demo_teacher_arun_prakash" model="mc.teacher">
<field name="employee_id" ref="demo_employee_arun_prakash"/>
<field name="subject_ids" eval="[(6, 0, [ref('demo_subject_math')])]"/>
<field name="max_weekly_periods">30</field>
</record>
</odoo>

View File

@ -1,2 +1,11 @@
from . import mc_academic_year from . import mc_academic_year
from . import mc_academic_term from . import mc_academic_term
from . import mc_room
from . import mc_program
from . import mc_subject
from . import mc_teacher
from . import mc_batch
from . import mc_student
from . import mc_guardian
from . import mc_student_guardian
from . import mc_enrollment

View File

@ -0,0 +1,24 @@
from odoo import fields, models
class McBatch(models.Model):
_name = "mc.batch"
_description = "Batch"
_order = "year_id desc, program_id, name"
_rec_name = "name"
name = fields.Char(string="Name", required=True, help='e.g. "Grade 8-A".')
program_id = fields.Many2one(
"mc.program", string="Program", required=True, ondelete="restrict",
)
year_id = fields.Many2one(
"mc.academic.year", string="Academic Year", required=True, ondelete="restrict",
)
class_teacher_id = fields.Many2one("hr.employee", string="Class Teacher", ondelete="restrict")
capacity = fields.Integer(string="Capacity")
room_id = fields.Many2one("mc.room", string="Room", ondelete="restrict")
_name_year_uniq = models.Constraint(
"unique(name, year_id)",
"A batch with this name already exists for this academic year.",
)

View File

@ -0,0 +1,112 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class McEnrollment(models.Model):
_name = "mc.enrollment"
_inherit = ["mail.thread"]
_description = "Enrollment"
_order = "year_id desc, student_id"
student_id = fields.Many2one(
"mc.student", string="Student", required=True, ondelete="restrict",
)
program_id = fields.Many2one(
"mc.program", string="Program", required=True, ondelete="restrict",
)
batch_id = fields.Many2one(
"mc.batch", string="Batch", required=True, ondelete="restrict",
)
year_id = fields.Many2one(
"mc.academic.year", string="Academic Year", required=True, ondelete="restrict",
)
state = fields.Selection(
[
("draft", "Draft"),
("active", "Active"),
("completed", "Completed"),
("withdrawn", "Withdrawn"),
("transferred", "Transferred"),
],
string="Status", default="draft", required=True, tracking=True,
)
roll_no = fields.Char(string="Roll No.", tracking=True)
date_enrolled = fields.Date(string="Date Enrolled", default=fields.Date.context_today)
@api.depends("student_id.name", "year_id.name", "batch_id.name")
def _compute_display_name(self):
for enrollment in self:
enrollment.display_name = "%s - %s (%s)" % (
enrollment.student_id.name or "?",
enrollment.batch_id.name or "?",
enrollment.year_id.name or "?",
)
def init(self):
# The spine of the whole domain model: a student has at most one
# Active enrollment per academic year. This MUST be a database
# constraint, not application logic - CLAUDE.md is explicit about
# this one. This partial unique index is the actual guarantee.
# Because it fires synchronously at INSERT/UPDATE time, it runs
# BEFORE @api.constrains ever gets a chance to - a naive
# "index + constrains for a friendly message" design is not
# enough, the raw IntegrityError wins that race. create()/write()
# below pre-check and raise the friendly message first for the
# common single-record case; @api.constrains stays as a backstop
# for batch creates where sibling rows in the same vals_list
# can't see each other yet at pre-check time.
self.env.cr.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS mc_enrollment_one_active_per_student_per_year "
"ON mc_enrollment (student_id, year_id) WHERE state = 'active'"
)
def _check_no_conflicting_active_enrollment(self, student_id, year_id, exclude_id=None):
domain = [
("student_id", "=", student_id),
("year_id", "=", year_id),
("state", "=", "active"),
]
if exclude_id:
domain.append(("id", "!=", exclude_id))
duplicate = self.search(domain, limit=1)
if duplicate:
raise ValidationError(_(
"%(student)s already has an active enrollment for %(year)s.",
student=duplicate.student_id.name,
year=duplicate.year_id.name,
))
@api.constrains("student_id", "year_id", "state")
def _check_one_active_enrollment_per_year(self):
for enrollment in self:
if enrollment.state == "active":
self._check_no_conflicting_active_enrollment(
enrollment.student_id.id, enrollment.year_id.id,
exclude_id=enrollment.id,
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("state") == "active":
self._check_no_conflicting_active_enrollment(
vals.get("student_id"), vals.get("year_id"),
)
# A state change on another enrollment earlier in the same
# transaction (e.g. withdrawing the old one before enrolling the
# new one) sits in the ORM cache until flushed. create() issues a
# direct SQL INSERT that will not wait for it, so without this
# flush the partial unique index can still see the old row as
# active and reject a perfectly legitimate re-enrollment.
self.env.flush_all()
return super().create(vals_list)
def write(self, vals):
if vals.get("state") == "active":
for enrollment in self:
self._check_no_conflicting_active_enrollment(
vals.get("student_id", enrollment.student_id.id),
vals.get("year_id", enrollment.year_id.id),
exclude_id=enrollment.id,
)
return super().write(vals)

View File

@ -0,0 +1,25 @@
from odoo import fields, models
class McGuardian(models.Model):
_name = "mc.guardian"
_inherit = ["mail.thread"]
_description = "Guardian"
_order = "name"
_rec_name = "name"
partner_id = fields.Many2one(
"res.partner", string="Contact", required=True, ondelete="restrict",
)
name = fields.Char(string="Name", required=True, tracking=True)
occupation = fields.Char(string="Occupation")
phone = fields.Char(string="Phone")
email = fields.Char(string="Email")
student_link_ids = fields.One2many(
"mc.student.guardian", "guardian_id", string="Children",
)
_partner_uniq = models.Constraint(
"unique(partner_id)",
"This contact already has a guardian record.",
)

View File

@ -0,0 +1,31 @@
from odoo import fields, models
class McProgram(models.Model):
_name = "mc.program"
_description = "Program"
# sequence_no drives sort order everywhere - never sort by display_label.
# "Class X" sorts alphabetically between "Class I" and "Class XI".
_order = "sequence_no, name"
_rec_name = "name"
name = fields.Char(string="Name", required=True, help='Full label, e.g. "Grade 8 CBSE".')
code = fields.Char(string="Code", required=True)
sequence_no = fields.Integer(
string="Sequence", required=True, default=10,
help="Drives sort order everywhere. Never sort by display label.",
)
display_label = fields.Char(
string="Display Label", required=True,
help='School-facing label, e.g. "Grade 8", "Class VIII", "Year 8".',
)
board = fields.Char(string="Board", help="e.g. CBSE, ICSE, IB, Cambridge, Ontario.")
company_id = fields.Many2one(
"res.company", string="Company", required=True,
default=lambda self: self.env.company,
)
_code_company_uniq = models.Constraint(
"unique(code, company_id)",
"A program with this code already exists for this company.",
)

View File

@ -0,0 +1,23 @@
from odoo import fields, models
class McRoom(models.Model):
_name = "mc.room"
_description = "Room"
_order = "building, name"
_rec_name = "name"
name = fields.Char(string="Name", required=True)
capacity = fields.Integer(string="Capacity")
building = fields.Char(string="Building")
type = fields.Selection(
[
("classroom", "Classroom"),
("lab", "Laboratory"),
("library", "Library"),
("auditorium", "Auditorium"),
("sports", "Sports"),
("other", "Other"),
],
string="Type", default="classroom",
)

View File

@ -0,0 +1,66 @@
from odoo import api, fields, models
class McStudent(models.Model):
_name = "mc.student"
_inherit = ["mail.thread"]
_description = "Student"
_order = "name"
_rec_name = "name"
partner_id = fields.Many2one(
"res.partner", string="Contact", required=True, ondelete="restrict",
)
admission_no = fields.Char(string="Admission No.", copy=False, tracking=True)
name = fields.Char(string="Name", required=True, tracking=True)
dob = fields.Date(string="Date of Birth")
gender = fields.Selection(
[("male", "Male"), ("female", "Female"), ("other", "Other")],
string="Gender",
)
admission_date = fields.Date(string="Admission Date", default=fields.Date.context_today)
photo = fields.Binary(string="Photo", attachment=True)
status = fields.Selection(
[
("active", "Active"),
("inactive", "Inactive"),
("alumni", "Alumni"),
("withdrawn", "Withdrawn"),
],
string="Status", default="active", required=True, tracking=True,
)
blood_group = fields.Selection(
[
("a+", "A+"), ("a-", "A-"),
("b+", "B+"), ("b-", "B-"),
("ab+", "AB+"), ("ab-", "AB-"),
("o+", "O+"), ("o-", "O-"),
],
string="Blood Group",
)
address_id = fields.Many2one(
"res.partner", string="Address",
help="A specific address contact for this student, if different from the guardian's.",
)
guardian_link_ids = fields.One2many(
"mc.student.guardian", "student_id", string="Guardians",
)
enrollment_ids = fields.One2many("mc.enrollment", "student_id", string="Enrollments")
_partner_uniq = models.Constraint(
"unique(partner_id)",
"This contact already has a student record.",
)
_admission_no_uniq = models.Constraint(
"unique(admission_no)",
"A student with this admission number already exists.",
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get("admission_no"):
vals["admission_no"] = self.env["ir.sequence"].next_by_code(
"mc.student.admission_no"
)
return super().create(vals_list)

View File

@ -0,0 +1,80 @@
from odoo import api, fields, models
class McStudentGuardian(models.Model):
_name = "mc.student.guardian"
_description = "Student Guardian Link"
_order = "student_id, is_primary desc"
student_id = fields.Many2one(
"mc.student", string="Student", required=True, ondelete="cascade",
)
guardian_id = fields.Many2one(
"mc.guardian", string="Guardian", required=True, ondelete="cascade",
)
relationship = fields.Selection(
[
("father", "Father"),
("mother", "Mother"),
("legal_guardian", "Legal Guardian"),
("other", "Other"),
],
string="Relationship", required=True,
)
is_primary = fields.Boolean(
string="Primary", default=False,
help="Notices go to the primary guardian only.",
)
_student_guardian_uniq = models.Constraint(
"unique(student_id, guardian_id)",
"This guardian is already linked to this student.",
)
def init(self):
# At most one primary guardian per student, enforced at the database
# level - the same belt-and-suspenders pattern as
# mc.academic.year.is_current. See that model for why both the
# ORM toggle and the partial index exist.
self.env.cr.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS mc_student_guardian_one_primary_per_student "
"ON mc_student_guardian (student_id) WHERE is_primary = true"
)
@api.depends("student_id.name", "guardian_id.name", "relationship")
def _compute_display_name(self):
relationship_labels = dict(self._fields["relationship"].selection)
for link in self:
label = relationship_labels.get(link.relationship, "")
link.display_name = "%s -> %s (%s)" % (
link.guardian_id.name or "?", link.student_id.name or "?", label,
)
def _unset_other_primary_links(self):
for link in self:
others = self.search([
("id", "!=", link.id),
("student_id", "=", link.student_id.id),
("is_primary", "=", True),
])
if others:
others.write({"is_primary": False})
@api.model_create_multi
def create(self, vals_list):
# Unset the existing primary guardian for each affected student
# BEFORE inserting, and flush immediately - create() issues a
# direct SQL INSERT that will not wait for this pending write.
for vals in vals_list:
if vals.get("is_primary") and vals.get("student_id"):
self.search([
("student_id", "=", vals["student_id"]),
("is_primary", "=", True),
]).write({"is_primary": False})
self.env.flush_all()
return super().create(vals_list)
def write(self, vals):
if vals.get("is_primary"):
self._unset_other_primary_links()
return super().write(vals)

View File

@ -0,0 +1,18 @@
from odoo import fields, models
class McSubject(models.Model):
_name = "mc.subject"
_description = "Subject"
_order = "name"
_rec_name = "name"
name = fields.Char(string="Name", required=True)
code = fields.Char(string="Code", required=True)
program_ids = fields.Many2many("mc.program", string="Programs")
is_elective = fields.Boolean(string="Elective", default=False)
_code_uniq = models.Constraint(
"unique(code)",
"A subject with this code already exists.",
)

View File

@ -0,0 +1,21 @@
from odoo import fields, models
class McTeacher(models.Model):
_name = "mc.teacher"
_description = "Teacher"
_order = "name"
_rec_name = "name"
employee_id = fields.Many2one(
"hr.employee", string="Employee", required=True, ondelete="cascade",
help="A teacher is an employee. This record only carries academic attributes.",
)
name = fields.Char(related="employee_id.name", string="Name", store=True, readonly=True)
subject_ids = fields.Many2many("mc.subject", string="Subjects Qualified to Teach")
max_weekly_periods = fields.Integer(string="Max Weekly Periods", default=30)
_employee_uniq = models.Constraint(
"unique(employee_id)",
"This employee already has a teacher record.",
)

View File

@ -3,3 +3,39 @@ access_mc_academic_year_administrator,mc.academic.year.administrator,model_mc_ac
access_mc_academic_year_staff,mc.academic.year.staff,model_mc_academic_year,group_school_staff,1,0,0,0 access_mc_academic_year_staff,mc.academic.year.staff,model_mc_academic_year,group_school_staff,1,0,0,0
access_mc_academic_term_administrator,mc.academic.term.administrator,model_mc_academic_term,group_school_administrator,1,1,1,1 access_mc_academic_term_administrator,mc.academic.term.administrator,model_mc_academic_term,group_school_administrator,1,1,1,1
access_mc_academic_term_staff,mc.academic.term.staff,model_mc_academic_term,group_school_staff,1,0,0,0 access_mc_academic_term_staff,mc.academic.term.staff,model_mc_academic_term,group_school_staff,1,0,0,0
access_mc_room_administrator,mc.room.administrator,model_mc_room,group_school_administrator,1,1,1,1
access_mc_room_staff,mc.room.staff,model_mc_room,group_school_staff,1,0,0,0
access_mc_room_teacher,mc.room.teacher,model_mc_room,group_teacher,1,0,0,0
access_mc_room_accountant,mc.room.accountant,model_mc_room,group_accountant,1,0,0,0
access_mc_program_administrator,mc.program.administrator,model_mc_program,group_school_administrator,1,1,1,1
access_mc_program_staff,mc.program.staff,model_mc_program,group_school_staff,1,0,0,0
access_mc_program_teacher,mc.program.teacher,model_mc_program,group_teacher,1,0,0,0
access_mc_program_accountant,mc.program.accountant,model_mc_program,group_accountant,1,0,0,0
access_mc_subject_administrator,mc.subject.administrator,model_mc_subject,group_school_administrator,1,1,1,1
access_mc_subject_staff,mc.subject.staff,model_mc_subject,group_school_staff,1,0,0,0
access_mc_subject_teacher,mc.subject.teacher,model_mc_subject,group_teacher,1,0,0,0
access_mc_subject_accountant,mc.subject.accountant,model_mc_subject,group_accountant,1,0,0,0
access_mc_teacher_administrator,mc.teacher.administrator,model_mc_teacher,group_school_administrator,1,1,1,1
access_mc_teacher_staff,mc.teacher.staff,model_mc_teacher,group_school_staff,1,0,0,0
access_mc_teacher_teacher,mc.teacher.teacher,model_mc_teacher,group_teacher,1,0,0,0
access_mc_teacher_accountant,mc.teacher.accountant,model_mc_teacher,group_accountant,1,0,0,0
access_mc_batch_administrator,mc.batch.administrator,model_mc_batch,group_school_administrator,1,1,1,1
access_mc_batch_staff,mc.batch.staff,model_mc_batch,group_school_staff,1,1,1,0
access_mc_batch_teacher,mc.batch.teacher,model_mc_batch,group_teacher,1,0,0,0
access_mc_batch_accountant,mc.batch.accountant,model_mc_batch,group_accountant,1,0,0,0
access_mc_student_administrator,mc.student.administrator,model_mc_student,group_school_administrator,1,1,1,1
access_mc_student_staff,mc.student.staff,model_mc_student,group_school_staff,1,1,1,0
access_mc_student_teacher,mc.student.teacher,model_mc_student,group_teacher,1,0,0,0
access_mc_student_accountant,mc.student.accountant,model_mc_student,group_accountant,1,0,0,0
access_mc_guardian_administrator,mc.guardian.administrator,model_mc_guardian,group_school_administrator,1,1,1,1
access_mc_guardian_staff,mc.guardian.staff,model_mc_guardian,group_school_staff,1,1,1,0
access_mc_guardian_teacher,mc.guardian.teacher,model_mc_guardian,group_teacher,1,0,0,0
access_mc_guardian_accountant,mc.guardian.accountant,model_mc_guardian,group_accountant,1,0,0,0
access_mc_student_guardian_administrator,mc.student.guardian.administrator,model_mc_student_guardian,group_school_administrator,1,1,1,1
access_mc_student_guardian_staff,mc.student.guardian.staff,model_mc_student_guardian,group_school_staff,1,1,1,0
access_mc_student_guardian_teacher,mc.student.guardian.teacher,model_mc_student_guardian,group_teacher,1,0,0,0
access_mc_student_guardian_accountant,mc.student.guardian.accountant,model_mc_student_guardian,group_accountant,1,0,0,0
access_mc_enrollment_administrator,mc.enrollment.administrator,model_mc_enrollment,group_school_administrator,1,1,1,1
access_mc_enrollment_staff,mc.enrollment.staff,model_mc_enrollment,group_school_staff,1,1,1,0
access_mc_enrollment_teacher,mc.enrollment.teacher,model_mc_enrollment,group_teacher,1,0,0,0
access_mc_enrollment_accountant,mc.enrollment.accountant,model_mc_enrollment,group_accountant,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_mc_academic_year_staff mc.academic.year.staff model_mc_academic_year group_school_staff 1 0 0 0
4 access_mc_academic_term_administrator mc.academic.term.administrator model_mc_academic_term group_school_administrator 1 1 1 1
5 access_mc_academic_term_staff mc.academic.term.staff model_mc_academic_term group_school_staff 1 0 0 0
6 access_mc_room_administrator mc.room.administrator model_mc_room group_school_administrator 1 1 1 1
7 access_mc_room_staff mc.room.staff model_mc_room group_school_staff 1 0 0 0
8 access_mc_room_teacher mc.room.teacher model_mc_room group_teacher 1 0 0 0
9 access_mc_room_accountant mc.room.accountant model_mc_room group_accountant 1 0 0 0
10 access_mc_program_administrator mc.program.administrator model_mc_program group_school_administrator 1 1 1 1
11 access_mc_program_staff mc.program.staff model_mc_program group_school_staff 1 0 0 0
12 access_mc_program_teacher mc.program.teacher model_mc_program group_teacher 1 0 0 0
13 access_mc_program_accountant mc.program.accountant model_mc_program group_accountant 1 0 0 0
14 access_mc_subject_administrator mc.subject.administrator model_mc_subject group_school_administrator 1 1 1 1
15 access_mc_subject_staff mc.subject.staff model_mc_subject group_school_staff 1 0 0 0
16 access_mc_subject_teacher mc.subject.teacher model_mc_subject group_teacher 1 0 0 0
17 access_mc_subject_accountant mc.subject.accountant model_mc_subject group_accountant 1 0 0 0
18 access_mc_teacher_administrator mc.teacher.administrator model_mc_teacher group_school_administrator 1 1 1 1
19 access_mc_teacher_staff mc.teacher.staff model_mc_teacher group_school_staff 1 0 0 0
20 access_mc_teacher_teacher mc.teacher.teacher model_mc_teacher group_teacher 1 0 0 0
21 access_mc_teacher_accountant mc.teacher.accountant model_mc_teacher group_accountant 1 0 0 0
22 access_mc_batch_administrator mc.batch.administrator model_mc_batch group_school_administrator 1 1 1 1
23 access_mc_batch_staff mc.batch.staff model_mc_batch group_school_staff 1 1 1 0
24 access_mc_batch_teacher mc.batch.teacher model_mc_batch group_teacher 1 0 0 0
25 access_mc_batch_accountant mc.batch.accountant model_mc_batch group_accountant 1 0 0 0
26 access_mc_student_administrator mc.student.administrator model_mc_student group_school_administrator 1 1 1 1
27 access_mc_student_staff mc.student.staff model_mc_student group_school_staff 1 1 1 0
28 access_mc_student_teacher mc.student.teacher model_mc_student group_teacher 1 0 0 0
29 access_mc_student_accountant mc.student.accountant model_mc_student group_accountant 1 0 0 0
30 access_mc_guardian_administrator mc.guardian.administrator model_mc_guardian group_school_administrator 1 1 1 1
31 access_mc_guardian_staff mc.guardian.staff model_mc_guardian group_school_staff 1 1 1 0
32 access_mc_guardian_teacher mc.guardian.teacher model_mc_guardian group_teacher 1 0 0 0
33 access_mc_guardian_accountant mc.guardian.accountant model_mc_guardian group_accountant 1 0 0 0
34 access_mc_student_guardian_administrator mc.student.guardian.administrator model_mc_student_guardian group_school_administrator 1 1 1 1
35 access_mc_student_guardian_staff mc.student.guardian.staff model_mc_student_guardian group_school_staff 1 1 1 0
36 access_mc_student_guardian_teacher mc.student.guardian.teacher model_mc_student_guardian group_teacher 1 0 0 0
37 access_mc_student_guardian_accountant mc.student.guardian.accountant model_mc_student_guardian group_accountant 1 0 0 0
38 access_mc_enrollment_administrator mc.enrollment.administrator model_mc_enrollment group_school_administrator 1 1 1 1
39 access_mc_enrollment_staff mc.enrollment.staff model_mc_enrollment group_school_staff 1 1 1 0
40 access_mc_enrollment_teacher mc.enrollment.teacher model_mc_enrollment group_teacher 1 0 0 0
41 access_mc_enrollment_accountant mc.enrollment.accountant model_mc_enrollment group_accountant 1 0 0 0

View File

@ -1 +1,3 @@
from . import test_academic_calendar from . import test_academic_calendar
from . import test_people
from . import test_enrollment

View File

@ -4,13 +4,17 @@ from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger from odoo.tools import mute_logger
# Names are deliberately distinct from the demo data's "2025-26"/"2026-27"
# (see demo/mc_academic_year_demo.xml) - these tests must pass whether or
# not demo data is loaded, and mc.academic.year.name is unique per company.
class TestAcademicCalendar(TransactionCase): class TestAcademicCalendar(TransactionCase):
def test_setting_current_on_create_unsets_previous(self): def test_setting_current_on_create_unsets_previous(self):
Year = self.env["mc.academic.year"] Year = self.env["mc.academic.year"]
year_a = Year.create({ year_a = Year.create({
"name": "2025-26", "name": "TEST-2025-26",
"date_start": "2025-06-01", "date_start": "2025-06-01",
"date_end": "2026-04-30", "date_end": "2026-04-30",
"is_current": True, "is_current": True,
@ -18,7 +22,7 @@ class TestAcademicCalendar(TransactionCase):
self.assertTrue(year_a.is_current) self.assertTrue(year_a.is_current)
year_b = Year.create({ year_b = Year.create({
"name": "2026-27", "name": "TEST-2026-27",
"date_start": "2026-06-01", "date_start": "2026-06-01",
"date_end": "2027-04-30", "date_end": "2027-04-30",
"is_current": True, "is_current": True,
@ -30,13 +34,13 @@ class TestAcademicCalendar(TransactionCase):
def test_setting_current_on_write_unsets_previous(self): def test_setting_current_on_write_unsets_previous(self):
Year = self.env["mc.academic.year"] Year = self.env["mc.academic.year"]
year_a = Year.create({ year_a = Year.create({
"name": "2025-26", "name": "TEST-2025-26",
"date_start": "2025-06-01", "date_start": "2025-06-01",
"date_end": "2026-04-30", "date_end": "2026-04-30",
"is_current": True, "is_current": True,
}) })
year_b = Year.create({ year_b = Year.create({
"name": "2026-27", "name": "TEST-2026-27",
"date_start": "2026-06-01", "date_start": "2026-06-01",
"date_end": "2027-04-30", "date_end": "2027-04-30",
"is_current": False, "is_current": False,
@ -54,7 +58,7 @@ class TestAcademicCalendar(TransactionCase):
# that writes around it (direct SQL, a future bug in the toggle logic). # that writes around it (direct SQL, a future bug in the toggle logic).
Year = self.env["mc.academic.year"] Year = self.env["mc.academic.year"]
Year.create({ Year.create({
"name": "2025-26", "name": "TEST-2025-26",
"date_start": "2025-06-01", "date_start": "2025-06-01",
"date_end": "2026-04-30", "date_end": "2026-04-30",
"is_current": True, "is_current": True,
@ -64,21 +68,21 @@ class TestAcademicCalendar(TransactionCase):
self.env.cr.execute( self.env.cr.execute(
"INSERT INTO mc_academic_year " "INSERT INTO mc_academic_year "
"(name, date_start, date_end, is_current, company_id, create_uid, write_uid, create_date, write_date) " "(name, date_start, date_end, is_current, company_id, create_uid, write_uid, create_date, write_date) "
"VALUES ('2026-27', '2026-06-01', '2027-04-30', true, %s, %s, %s, now(), now())", "VALUES ('TEST-2026-27', '2026-06-01', '2027-04-30', true, %s, %s, %s, now(), now())",
(self.env.company.id, self.env.uid, self.env.uid), (self.env.company.id, self.env.uid, self.env.uid),
) )
def test_year_end_before_start_raises(self): def test_year_end_before_start_raises(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.env["mc.academic.year"].create({ self.env["mc.academic.year"].create({
"name": "Bad Year", "name": "TEST-Bad-Year",
"date_start": "2026-06-01", "date_start": "2026-06-01",
"date_end": "2026-05-01", "date_end": "2026-05-01",
}) })
def test_term_end_before_start_raises(self): def test_term_end_before_start_raises(self):
year = self.env["mc.academic.year"].create({ year = self.env["mc.academic.year"].create({
"name": "2026-27", "name": "TEST-2026-27",
"date_start": "2026-06-01", "date_start": "2026-06-01",
"date_end": "2027-04-30", "date_end": "2027-04-30",
}) })
@ -92,7 +96,7 @@ class TestAcademicCalendar(TransactionCase):
def test_term_must_fall_within_year(self): def test_term_must_fall_within_year(self):
year = self.env["mc.academic.year"].create({ year = self.env["mc.academic.year"].create({
"name": "2026-27", "name": "TEST-2026-27",
"date_start": "2026-06-01", "date_start": "2026-06-01",
"date_end": "2027-04-30", "date_end": "2027-04-30",
}) })
@ -107,7 +111,7 @@ class TestAcademicCalendar(TransactionCase):
@mute_logger("odoo.sql_db") @mute_logger("odoo.sql_db")
def test_duplicate_term_name_in_same_year_rejected(self): def test_duplicate_term_name_in_same_year_rejected(self):
year = self.env["mc.academic.year"].create({ year = self.env["mc.academic.year"].create({
"name": "2026-27", "name": "TEST-2026-27",
"date_start": "2026-06-01", "date_start": "2026-06-01",
"date_end": "2027-04-30", "date_end": "2027-04-30",
}) })

View File

@ -0,0 +1,86 @@
from psycopg2 import IntegrityError
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger
class TestEnrollment(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Names/codes are deliberately distinct from demo data (see
# demo/mc_program_demo.xml, demo/mc_academic_year_demo.xml) -
# these tests must pass whether or not demo data is loaded.
cls.year = cls.env["mc.academic.year"].create({
"name": "TEST-2026-27",
"date_start": "2026-06-01",
"date_end": "2027-04-30",
})
cls.other_year = cls.env["mc.academic.year"].create({
"name": "TEST-2027-28",
"date_start": "2027-06-01",
"date_end": "2028-04-30",
})
cls.program = cls.env["mc.program"].create({
"name": "Test Grade 8 CBSE", "code": "TEST-G8", "sequence_no": 8,
"display_label": "Test Grade 8",
})
cls.batch_a = cls.env["mc.batch"].create({
"name": "Grade 8-A", "program_id": cls.program.id, "year_id": cls.year.id,
})
cls.batch_b = cls.env["mc.batch"].create({
"name": "Grade 8-B", "program_id": cls.program.id, "year_id": cls.year.id,
})
partner = cls.env["res.partner"].create({"name": "Test Student"})
cls.student = cls.env["mc.student"].create({
"partner_id": partner.id, "name": "Test Student",
})
def _enroll(self, batch, year, state="active", roll_no="1"):
return self.env["mc.enrollment"].create({
"student_id": self.student.id,
"program_id": self.program.id,
"batch_id": batch.id,
"year_id": year.id,
"state": state,
"roll_no": roll_no,
})
def test_admission_no_auto_generated(self):
self.assertTrue(self.student.admission_no)
self.assertIn("ADM", self.student.admission_no)
def test_second_active_enrollment_same_year_rejected(self):
self._enroll(self.batch_a, self.year)
with self.assertRaises(ValidationError):
self._enroll(self.batch_b, self.year)
@mute_logger("odoo.sql_db")
def test_db_index_backs_the_rule_even_if_orm_check_is_bypassed(self):
# The @api.constrains gives a friendly message; the partial unique
# index is what actually guarantees the rule. Prove the index is
# there by writing around the ORM check.
self._enroll(self.batch_a, self.year)
with self.assertRaises(IntegrityError):
with self.cr.savepoint():
self.env.cr.execute(
"INSERT INTO mc_enrollment "
"(student_id, program_id, batch_id, year_id, state, create_uid, write_uid, create_date, write_date) "
"VALUES (%s, %s, %s, %s, 'active', %s, %s, now(), now())",
(self.student.id, self.program.id, self.batch_b.id, self.year.id,
self.env.uid, self.env.uid),
)
def test_active_enrollment_allowed_in_different_year(self):
self._enroll(self.batch_a, self.year)
# Should not raise: different academic year, no conflict.
self._enroll(self.batch_a, self.other_year)
def test_withdrawing_then_re_enrolling_active_is_allowed(self):
first = self._enroll(self.batch_a, self.year)
first.state = "withdrawn"
# Should not raise: the only active row for this student+year was
# just withdrawn, so a new active enrollment is legitimate.
self._enroll(self.batch_b, self.year)

View File

@ -0,0 +1,72 @@
from psycopg2 import IntegrityError
from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger
class TestPeople(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.student_partner = cls.env["res.partner"].create({"name": "Test Student"})
cls.student = cls.env["mc.student"].create({
"partner_id": cls.student_partner.id, "name": "Test Student",
})
cls.guardian_a_partner = cls.env["res.partner"].create({"name": "Guardian A"})
cls.guardian_a = cls.env["mc.guardian"].create({
"partner_id": cls.guardian_a_partner.id, "name": "Guardian A",
})
cls.guardian_b_partner = cls.env["res.partner"].create({"name": "Guardian B"})
cls.guardian_b = cls.env["mc.guardian"].create({
"partner_id": cls.guardian_b_partner.id, "name": "Guardian B",
})
def test_setting_primary_on_create_unsets_previous(self):
Link = self.env["mc.student.guardian"]
link_a = Link.create({
"student_id": self.student.id, "guardian_id": self.guardian_a.id,
"relationship": "mother", "is_primary": True,
})
link_b = Link.create({
"student_id": self.student.id, "guardian_id": self.guardian_b.id,
"relationship": "father", "is_primary": True,
})
self.assertFalse(link_a.is_primary, "A second primary guardian must unset the first.")
self.assertTrue(link_b.is_primary)
def test_setting_primary_on_write_unsets_previous(self):
Link = self.env["mc.student.guardian"]
link_a = Link.create({
"student_id": self.student.id, "guardian_id": self.guardian_a.id,
"relationship": "mother", "is_primary": True,
})
link_b = Link.create({
"student_id": self.student.id, "guardian_id": self.guardian_b.id,
"relationship": "father", "is_primary": False,
})
link_b.write({"is_primary": True})
self.assertFalse(link_a.is_primary)
self.assertTrue(link_b.is_primary)
@mute_logger("odoo.sql_db")
def test_duplicate_student_guardian_link_rejected(self):
Link = self.env["mc.student.guardian"]
Link.create({
"student_id": self.student.id, "guardian_id": self.guardian_a.id,
"relationship": "mother",
})
with self.assertRaises(IntegrityError):
with self.cr.savepoint():
Link.create({
"student_id": self.student.id, "guardian_id": self.guardian_a.id,
"relationship": "father",
})
@mute_logger("odoo.sql_db")
def test_partner_cannot_back_two_student_records(self):
with self.assertRaises(IntegrityError):
with self.cr.savepoint():
self.env["mc.student"].create({
"partner_id": self.student_partner.id, "name": "Duplicate",
})

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_batch_list" model="ir.ui.view">
<field name="name">mc.batch.list</field>
<field name="model">mc.batch</field>
<field name="arch" type="xml">
<list string="Batches">
<field name="year_id"/>
<field name="program_id"/>
<field name="name"/>
<field name="class_teacher_id"/>
<field name="room_id"/>
<field name="capacity"/>
</list>
</field>
</record>
<record id="view_mc_batch_form" model="ir.ui.view">
<field name="name">mc.batch.form</field>
<field name="model">mc.batch</field>
<field name="arch" type="xml">
<form string="Batch">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="Grade 8-A"/></h1>
</div>
<group>
<group>
<field name="program_id"/>
<field name="year_id"/>
<field name="capacity"/>
</group>
<group>
<field name="class_teacher_id"/>
<field name="room_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mc_batch" model="ir.actions.act_window">
<field name="name">Batches</field>
<field name="res_model">mc.batch</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -3,6 +3,27 @@
<menuitem id="menu_school_root" name="School" sequence="10" <menuitem id="menu_school_root" name="School" sequence="10"
groups="mc_education_base.group_school_staff"/> groups="mc_education_base.group_school_staff"/>
<!-- Operational: front office's daily work -->
<menuitem id="menu_school_people" name="People"
parent="menu_school_root" sequence="10"/>
<menuitem id="menu_mc_student" name="Students"
parent="menu_school_people"
action="action_mc_student" sequence="10"/>
<menuitem id="menu_mc_guardian" name="Guardians"
parent="menu_school_people"
action="action_mc_guardian" sequence="20"/>
<menuitem id="menu_mc_teacher" name="Teachers"
parent="menu_school_people"
action="action_mc_teacher" sequence="30"/>
<menuitem id="menu_mc_enrollment" name="Enrollment"
parent="menu_school_root"
action="action_mc_enrollment" sequence="20"/>
<!-- Configuration: administrator-only academic structure -->
<menuitem id="menu_school_configuration" name="Configuration" <menuitem id="menu_school_configuration" name="Configuration"
parent="menu_school_root" sequence="100" parent="menu_school_root" sequence="100"
groups="mc_education_base.group_school_administrator"/> groups="mc_education_base.group_school_administrator"/>
@ -14,4 +35,20 @@
<menuitem id="menu_mc_academic_term" name="Academic Terms" <menuitem id="menu_mc_academic_term" name="Academic Terms"
parent="menu_school_configuration" parent="menu_school_configuration"
action="action_mc_academic_term" sequence="20"/> action="action_mc_academic_term" sequence="20"/>
<menuitem id="menu_mc_program" name="Programs"
parent="menu_school_configuration"
action="action_mc_program" sequence="30"/>
<menuitem id="menu_mc_subject" name="Subjects"
parent="menu_school_configuration"
action="action_mc_subject" sequence="40"/>
<menuitem id="menu_mc_batch" name="Batches"
parent="menu_school_configuration"
action="action_mc_batch" sequence="50"/>
<menuitem id="menu_mc_room" name="Rooms"
parent="menu_school_configuration"
action="action_mc_room" sequence="60"/>
</odoo> </odoo>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_enrollment_list" model="ir.ui.view">
<field name="name">mc.enrollment.list</field>
<field name="model">mc.enrollment</field>
<field name="arch" type="xml">
<list string="Enrollments">
<field name="year_id"/>
<field name="student_id"/>
<field name="program_id"/>
<field name="batch_id"/>
<field name="roll_no"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<record id="view_mc_enrollment_form" model="ir.ui.view">
<field name="name">mc.enrollment.form</field>
<field name="model">mc.enrollment</field>
<field name="arch" type="xml">
<form string="Enrollment">
<header>
<field name="state" widget="statusbar" statusbar_visible="draft,active,completed"/>
</header>
<sheet>
<group>
<group>
<field name="student_id"/>
<field name="year_id"/>
</group>
<group>
<field name="program_id"/>
<field name="batch_id"/>
<field name="roll_no"/>
<field name="date_enrolled"/>
</group>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="action_mc_enrollment" model="ir.actions.act_window">
<field name="name">Enrollments</field>
<field name="res_model">mc.enrollment</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_guardian_list" model="ir.ui.view">
<field name="name">mc.guardian.list</field>
<field name="model">mc.guardian</field>
<field name="arch" type="xml">
<list string="Guardians">
<field name="name"/>
<field name="occupation"/>
<field name="phone"/>
<field name="email"/>
</list>
</field>
</record>
<record id="view_mc_guardian_form" model="ir.ui.view">
<field name="name">mc.guardian.form</field>
<field name="model">mc.guardian</field>
<field name="arch" type="xml">
<form string="Guardian">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<field name="partner_id"/>
<field name="occupation"/>
<field name="phone"/>
<field name="email"/>
</group>
<notebook>
<page string="Children">
<field name="student_link_ids">
<list editable="bottom">
<field name="student_id"/>
<field name="relationship"/>
<field name="is_primary"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="action_mc_guardian" model="ir.actions.act_window">
<field name="name">Guardians</field>
<field name="res_model">mc.guardian</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_program_list" model="ir.ui.view">
<field name="name">mc.program.list</field>
<field name="model">mc.program</field>
<field name="arch" type="xml">
<list string="Programs">
<field name="sequence_no" widget="handle"/>
<field name="display_label"/>
<field name="name"/>
<field name="code"/>
<field name="board"/>
<field name="company_id" groups="base.group_multi_company"/>
</list>
</field>
</record>
<record id="view_mc_program_form" model="ir.ui.view">
<field name="name">mc.program.form</field>
<field name="model">mc.program</field>
<field name="arch" type="xml">
<form string="Program">
<sheet>
<div class="oe_title">
<label for="display_label"/>
<h1><field name="display_label" placeholder="Grade 8"/></h1>
</div>
<group>
<group>
<field name="name"/>
<field name="code"/>
</group>
<group>
<field name="sequence_no"/>
<field name="board"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mc_program" model="ir.actions.act_window">
<field name="name">Programs</field>
<field name="res_model">mc.program</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_room_list" model="ir.ui.view">
<field name="name">mc.room.list</field>
<field name="model">mc.room</field>
<field name="arch" type="xml">
<list string="Rooms">
<field name="name"/>
<field name="building"/>
<field name="type"/>
<field name="capacity"/>
</list>
</field>
</record>
<record id="view_mc_room_form" model="ir.ui.view">
<field name="name">mc.room.form</field>
<field name="model">mc.room</field>
<field name="arch" type="xml">
<form string="Room">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<field name="building"/>
<field name="type"/>
<field name="capacity"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mc_room" model="ir.actions.act_window">
<field name="name">Rooms</field>
<field name="res_model">mc.room</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_student_list" model="ir.ui.view">
<field name="name">mc.student.list</field>
<field name="model">mc.student</field>
<field name="arch" type="xml">
<list string="Students">
<field name="admission_no"/>
<field name="name"/>
<field name="dob"/>
<field name="gender"/>
<field name="status"/>
</list>
</field>
</record>
<record id="view_mc_student_form" model="ir.ui.view">
<field name="name">mc.student.form</field>
<field name="model">mc.student</field>
<field name="arch" type="xml">
<form string="Student">
<sheet>
<field name="photo" widget="image" class="oe_avatar"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="admission_no" readonly="1"/>
<field name="partner_id"/>
<field name="dob"/>
<field name="gender"/>
</group>
<group>
<field name="admission_date"/>
<field name="status"/>
<field name="blood_group"/>
<field name="address_id"/>
</group>
</group>
<notebook>
<page string="Guardians">
<field name="guardian_link_ids">
<list editable="bottom">
<field name="guardian_id"/>
<field name="relationship"/>
<field name="is_primary"/>
</list>
</field>
</page>
<page string="Enrollments">
<field name="enrollment_ids" readonly="1">
<list>
<field name="year_id"/>
<field name="program_id"/>
<field name="batch_id"/>
<field name="roll_no"/>
<field name="state"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="action_mc_student" model="ir.actions.act_window">
<field name="name">Students</field>
<field name="res_model">mc.student</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_subject_list" model="ir.ui.view">
<field name="name">mc.subject.list</field>
<field name="model">mc.subject</field>
<field name="arch" type="xml">
<list string="Subjects">
<field name="name"/>
<field name="code"/>
<field name="program_ids" widget="many2many_tags"/>
<field name="is_elective"/>
</list>
</field>
</record>
<record id="view_mc_subject_form" model="ir.ui.view">
<field name="name">mc.subject.form</field>
<field name="model">mc.subject</field>
<field name="arch" type="xml">
<form string="Subject">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<field name="code"/>
<field name="is_elective"/>
<field name="program_ids" widget="many2many_tags"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mc_subject" model="ir.actions.act_window">
<field name="name">Subjects</field>
<field name="res_model">mc.subject</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mc_teacher_list" model="ir.ui.view">
<field name="name">mc.teacher.list</field>
<field name="model">mc.teacher</field>
<field name="arch" type="xml">
<list string="Teachers">
<field name="name"/>
<field name="subject_ids" widget="many2many_tags"/>
<field name="max_weekly_periods"/>
</list>
</field>
</record>
<record id="view_mc_teacher_form" model="ir.ui.view">
<field name="name">mc.teacher.form</field>
<field name="model">mc.teacher</field>
<field name="arch" type="xml">
<form string="Teacher">
<sheet>
<div class="oe_title">
<label for="employee_id"/>
<h1><field name="employee_id"/></h1>
</div>
<group>
<field name="subject_ids" widget="many2many_tags"/>
<field name="max_weekly_periods"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mc_teacher" model="ir.actions.act_window">
<field name="name">Teachers</field>
<field name="res_model">mc.teacher</field>
<field name="view_mode">list,form</field>
</record>
</odoo>