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:
parent
1e5ac14510
commit
709bd2eca7
@ -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,
|
||||||
|
|||||||
13
addons/mc_education_base/data/mc_sequence_data.xml
Normal file
13
addons/mc_education_base/data/mc_sequence_data.xml
Normal 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>
|
||||||
18
addons/mc_education_base/demo/mc_batch_demo.xml
Normal file
18
addons/mc_education_base/demo/mc_batch_demo.xml
Normal 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>
|
||||||
21
addons/mc_education_base/demo/mc_enrollment_demo.xml
Normal file
21
addons/mc_education_base/demo/mc_enrollment_demo.xml
Normal 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>
|
||||||
63
addons/mc_education_base/demo/mc_people_demo.xml
Normal file
63
addons/mc_education_base/demo/mc_people_demo.xml
Normal 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>
|
||||||
17
addons/mc_education_base/demo/mc_program_demo.xml
Normal file
17
addons/mc_education_base/demo/mc_program_demo.xml
Normal 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>
|
||||||
15
addons/mc_education_base/demo/mc_room_demo.xml
Normal file
15
addons/mc_education_base/demo/mc_room_demo.xml
Normal 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>
|
||||||
29
addons/mc_education_base/demo/mc_subject_demo.xml
Normal file
29
addons/mc_education_base/demo/mc_subject_demo.xml
Normal 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>
|
||||||
17
addons/mc_education_base/demo/mc_teacher_demo.xml
Normal file
17
addons/mc_education_base/demo/mc_teacher_demo.xml
Normal 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>
|
||||||
@ -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
|
||||||
|
|||||||
24
addons/mc_education_base/models/mc_batch.py
Normal file
24
addons/mc_education_base/models/mc_batch.py
Normal 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.",
|
||||||
|
)
|
||||||
112
addons/mc_education_base/models/mc_enrollment.py
Normal file
112
addons/mc_education_base/models/mc_enrollment.py
Normal 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)
|
||||||
25
addons/mc_education_base/models/mc_guardian.py
Normal file
25
addons/mc_education_base/models/mc_guardian.py
Normal 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.",
|
||||||
|
)
|
||||||
31
addons/mc_education_base/models/mc_program.py
Normal file
31
addons/mc_education_base/models/mc_program.py
Normal 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.",
|
||||||
|
)
|
||||||
23
addons/mc_education_base/models/mc_room.py
Normal file
23
addons/mc_education_base/models/mc_room.py
Normal 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",
|
||||||
|
)
|
||||||
66
addons/mc_education_base/models/mc_student.py
Normal file
66
addons/mc_education_base/models/mc_student.py
Normal 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)
|
||||||
80
addons/mc_education_base/models/mc_student_guardian.py
Normal file
80
addons/mc_education_base/models/mc_student_guardian.py
Normal 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)
|
||||||
18
addons/mc_education_base/models/mc_subject.py
Normal file
18
addons/mc_education_base/models/mc_subject.py
Normal 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.",
|
||||||
|
)
|
||||||
21
addons/mc_education_base/models/mc_teacher.py
Normal file
21
addons/mc_education_base/models/mc_teacher.py
Normal 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.",
|
||||||
|
)
|
||||||
@ -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 +1,3 @@
|
|||||||
from . import test_academic_calendar
|
from . import test_academic_calendar
|
||||||
|
from . import test_people
|
||||||
|
from . import test_enrollment
|
||||||
|
|||||||
@ -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",
|
||||||
})
|
})
|
||||||
|
|||||||
86
addons/mc_education_base/tests/test_enrollment.py
Normal file
86
addons/mc_education_base/tests/test_enrollment.py
Normal 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)
|
||||||
72
addons/mc_education_base/tests/test_people.py
Normal file
72
addons/mc_education_base/tests/test_people.py
Normal 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",
|
||||||
|
})
|
||||||
49
addons/mc_education_base/views/mc_batch_views.xml
Normal file
49
addons/mc_education_base/views/mc_batch_views.xml
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
50
addons/mc_education_base/views/mc_enrollment_views.xml
Normal file
50
addons/mc_education_base/views/mc_enrollment_views.xml
Normal 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>
|
||||||
54
addons/mc_education_base/views/mc_guardian_views.xml
Normal file
54
addons/mc_education_base/views/mc_guardian_views.xml
Normal 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>
|
||||||
49
addons/mc_education_base/views/mc_program_views.xml
Normal file
49
addons/mc_education_base/views/mc_program_views.xml
Normal 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>
|
||||||
41
addons/mc_education_base/views/mc_room_views.xml
Normal file
41
addons/mc_education_base/views/mc_room_views.xml
Normal 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>
|
||||||
75
addons/mc_education_base/views/mc_student_views.xml
Normal file
75
addons/mc_education_base/views/mc_student_views.xml
Normal 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>
|
||||||
41
addons/mc_education_base/views/mc_subject_views.xml
Normal file
41
addons/mc_education_base/views/mc_subject_views.xml
Normal 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>
|
||||||
39
addons/mc_education_base/views/mc_teacher_views.xml
Normal file
39
addons/mc_education_base/views/mc_teacher_views.xml
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user