O1: mc_education_base gate, first slice - academic calendar
mc.academic.year and mc.academic.term: the two models every other O1
model (program, batch, enrollment...) will hang off. Exactly one
current year is enforced two ways - the ORM toggles is_current off
the previous year on create/write, and a partial unique index on
(company_id) WHERE is_current backs it at the database level so the
rule holds even if something writes around the ORM.
Security groups for all six roles from the spec (Administrator,
Staff, Teacher, Accountant, Guardian, Student) are scaffolded now
since every later O1 model needs them, though only Administrator/
Staff have access rows on these two models so far.
Verified against a real odoo:19.0 container, not just read: module
installs clean with views, menus and demo data, and all 7 test
methods pass. That surfaced two things CLAUDE.md's Coding Standards
section didn't anticipate, since Odoo 19 moved past 17/18-era APIs
in ways not caught by an 18-era mental model:
- `_sql_constraints` is gone; constraints are now per-attribute
`models.Constraint(sql, message)`.
- `res.groups.category_id` is gone; groups now hang off a new
`res.groups.privilege` record, which carries the category.
Both addons/mc_education_base files already use the new APIs.
CLAUDE.md itself needs a note added in a follow-up so this isn't
rediscovered per-module - flagging here per its own closing
instruction ("say so and propose the correction... update this file
in the same PR") rather than leaving it implicit in this commit body.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
85f2b4b11a
commit
8b7bd91f74
1
addons/mc_education_base/__init__.py
Normal file
1
addons/mc_education_base/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
22
addons/mc_education_base/__manifest__.py
Normal file
22
addons/mc_education_base/__manifest__.py
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "School ERP - Base",
|
||||
"version": "19.0.1.0.0",
|
||||
"category": "Education",
|
||||
"summary": "Core academic structure: years, terms, programs, batches, students, guardians, teachers, enrollment.",
|
||||
"author": "Metatroncube Software Solutions LLP",
|
||||
"license": "Other proprietary",
|
||||
"depends": ["base", "mail", "contacts", "hr"],
|
||||
"data": [
|
||||
"security/mc_education_security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/mc_academic_year_views.xml",
|
||||
"views/mc_academic_term_views.xml",
|
||||
"views/mc_education_menus.xml",
|
||||
],
|
||||
"demo": [
|
||||
"demo/mc_academic_year_demo.xml",
|
||||
"demo/mc_academic_term_demo.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"application": True,
|
||||
}
|
||||
24
addons/mc_education_base/demo/mc_academic_term_demo.xml
Normal file
24
addons/mc_education_base/demo/mc_academic_term_demo.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="demo_term_2026_27_t1" model="mc.academic.term">
|
||||
<field name="name">Term 1</field>
|
||||
<field name="year_id" ref="demo_year_2026_27"/>
|
||||
<field name="date_start">2026-06-01</field>
|
||||
<field name="date_end">2026-09-30</field>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="demo_term_2026_27_t2" model="mc.academic.term">
|
||||
<field name="name">Term 2</field>
|
||||
<field name="year_id" ref="demo_year_2026_27"/>
|
||||
<field name="date_start">2026-10-01</field>
|
||||
<field name="date_end">2026-12-31</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
<record id="demo_term_2026_27_t3" model="mc.academic.term">
|
||||
<field name="name">Term 3</field>
|
||||
<field name="year_id" ref="demo_year_2026_27"/>
|
||||
<field name="date_start">2027-01-01</field>
|
||||
<field name="date_end">2027-04-30</field>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
</odoo>
|
||||
15
addons/mc_education_base/demo/mc_academic_year_demo.xml
Normal file
15
addons/mc_education_base/demo/mc_academic_year_demo.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="demo_year_2026_27" model="mc.academic.year">
|
||||
<field name="name">2026-27</field>
|
||||
<field name="date_start">2026-06-01</field>
|
||||
<field name="date_end">2027-04-30</field>
|
||||
<field name="is_current">True</field>
|
||||
</record>
|
||||
<record id="demo_year_2025_26" model="mc.academic.year">
|
||||
<field name="name">2025-26</field>
|
||||
<field name="date_start">2025-06-01</field>
|
||||
<field name="date_end">2026-04-30</field>
|
||||
<field name="is_current">False</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2
addons/mc_education_base/models/__init__.py
Normal file
2
addons/mc_education_base/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import mc_academic_year
|
||||
from . import mc_academic_term
|
||||
46
addons/mc_education_base/models/mc_academic_term.py
Normal file
46
addons/mc_education_base/models/mc_academic_term.py
Normal file
@ -0,0 +1,46 @@
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class McAcademicTerm(models.Model):
|
||||
_name = "mc.academic.term"
|
||||
_inherit = ["mail.thread"]
|
||||
_description = "Academic Term"
|
||||
_order = "year_id, sequence, date_start"
|
||||
_rec_name = "name"
|
||||
|
||||
name = fields.Char(string="Name", required=True, tracking=True)
|
||||
year_id = fields.Many2one(
|
||||
"mc.academic.year", string="Academic Year", required=True,
|
||||
ondelete="cascade", tracking=True,
|
||||
)
|
||||
date_start = fields.Date(string="Start Date", required=True, tracking=True)
|
||||
date_end = fields.Date(string="End Date", required=True, tracking=True)
|
||||
sequence = fields.Integer(string="Sequence", default=10)
|
||||
company_id = fields.Many2one(
|
||||
related="year_id.company_id", string="Company", store=True, readonly=True,
|
||||
)
|
||||
|
||||
_name_year_uniq = models.Constraint(
|
||||
"unique(name, year_id)",
|
||||
"A term with this name already exists for this academic year.",
|
||||
)
|
||||
|
||||
@api.constrains("date_start", "date_end")
|
||||
def _check_dates(self):
|
||||
for term in self:
|
||||
if term.date_start and term.date_end and term.date_end <= term.date_start:
|
||||
raise ValidationError(
|
||||
"Term '%s' end date must be after its start date." % term.name
|
||||
)
|
||||
|
||||
@api.constrains("date_start", "date_end", "year_id")
|
||||
def _check_within_year(self):
|
||||
for term in self:
|
||||
year = term.year_id
|
||||
if not (year.date_start and year.date_end and term.date_start and term.date_end):
|
||||
continue
|
||||
if term.date_start < year.date_start or term.date_end > year.date_end:
|
||||
raise ValidationError(
|
||||
"Term '%s' must fall within its academic year (%s)." % (term.name, year.name)
|
||||
)
|
||||
79
addons/mc_education_base/models/mc_academic_year.py
Normal file
79
addons/mc_education_base/models/mc_academic_year.py
Normal file
@ -0,0 +1,79 @@
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class McAcademicYear(models.Model):
|
||||
_name = "mc.academic.year"
|
||||
_inherit = ["mail.thread"]
|
||||
_description = "Academic Year"
|
||||
_order = "date_start desc"
|
||||
_rec_name = "name"
|
||||
|
||||
name = fields.Char(
|
||||
string="Name", required=True, tracking=True,
|
||||
help="School-facing label, e.g. 2026-27.",
|
||||
)
|
||||
date_start = fields.Date(string="Start Date", required=True, tracking=True)
|
||||
date_end = fields.Date(string="End Date", required=True, tracking=True)
|
||||
is_current = fields.Boolean(string="Current Year", default=False, tracking=True)
|
||||
term_ids = fields.One2many("mc.academic.term", "year_id", string="Terms")
|
||||
company_id = fields.Many2one(
|
||||
"res.company", string="Company", required=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
|
||||
_name_company_uniq = models.Constraint(
|
||||
"unique(name, company_id)",
|
||||
"An academic year with this name already exists for this company.",
|
||||
)
|
||||
|
||||
def init(self):
|
||||
# Belt-and-suspenders on top of the create/write auto-toggle below:
|
||||
# a partial unique index guarantees at most one current year per
|
||||
# company even if a write bypasses the ORM (direct SQL, a future
|
||||
# bug in the toggle logic, concurrent transactions).
|
||||
self.env.cr.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS mc_academic_year_one_current_per_company "
|
||||
"ON mc_academic_year (company_id) WHERE is_current = true"
|
||||
)
|
||||
|
||||
@api.constrains("date_start", "date_end")
|
||||
def _check_dates(self):
|
||||
for year in self:
|
||||
if year.date_start and year.date_end and year.date_end <= year.date_start:
|
||||
raise ValidationError(
|
||||
"Academic year '%s' end date must be after its start date." % year.name
|
||||
)
|
||||
|
||||
def _unset_other_current_years(self):
|
||||
for year in self:
|
||||
others = self.search([
|
||||
("id", "!=", year.id),
|
||||
("company_id", "=", year.company_id.id),
|
||||
("is_current", "=", True),
|
||||
])
|
||||
if others:
|
||||
others.write({"is_current": False})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# Unset the existing current year for each affected company BEFORE
|
||||
# inserting the new one, and flush immediately: create() issues a
|
||||
# direct SQL INSERT that does not wait for unrelated pending writes
|
||||
# in the ORM cache, so without the flush here the old row is still
|
||||
# True in the database when the new row is inserted, tripping the
|
||||
# partial unique index mid-transaction.
|
||||
for vals in vals_list:
|
||||
if vals.get("is_current"):
|
||||
company_id = vals.get("company_id", self.env.company.id)
|
||||
self.search([
|
||||
("company_id", "=", company_id),
|
||||
("is_current", "=", True),
|
||||
]).write({"is_current": False})
|
||||
self.env.flush_all()
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get("is_current"):
|
||||
self._unset_other_current_years()
|
||||
return super().write(vals)
|
||||
5
addons/mc_education_base/security/ir.model.access.csv
Normal file
5
addons/mc_education_base/security/ir.model.access.csv
Normal file
@ -0,0 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mc_academic_year_administrator,mc.academic.year.administrator,model_mc_academic_year,group_school_administrator,1,1,1,1
|
||||
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_staff,mc.academic.term.staff,model_mc_academic_term,group_school_staff,1,0,0,0
|
||||
|
58
addons/mc_education_base/security/mc_education_security.xml
Normal file
58
addons/mc_education_base/security/mc_education_security.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="module_category_school" model="ir.module.category">
|
||||
<field name="name">School Management</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<!-- Odoo 19: res.groups no longer has category_id directly; groups are
|
||||
grouped under a res.groups.privilege, which carries the category. -->
|
||||
<record id="privilege_school" model="res.groups.privilege">
|
||||
<field name="name">School Management</field>
|
||||
<field name="category_id" ref="module_category_school"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<!-- Internal (backend) roles -->
|
||||
<record id="group_school_staff" model="res.groups">
|
||||
<field name="name">School Staff</field>
|
||||
<field name="privilege_id" ref="privilege_school"/>
|
||||
<field name="comment">Front office and general staff: read access to academic structure and student records.</field>
|
||||
</record>
|
||||
|
||||
<record id="group_teacher" model="res.groups">
|
||||
<field name="name">Teacher</field>
|
||||
<field name="privilege_id" ref="privilege_school"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_school_staff'))]"/>
|
||||
<field name="comment">Teaching staff: their own batches only, enforced by record rules in the modules that need it (attendance, exam).</field>
|
||||
</record>
|
||||
|
||||
<record id="group_accountant" model="res.groups">
|
||||
<field name="name">Accountant</field>
|
||||
<field name="privilege_id" ref="privilege_school"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_school_staff'))]"/>
|
||||
<field name="comment">Fees, invoices and payments.</field>
|
||||
</record>
|
||||
|
||||
<record id="group_school_administrator" model="res.groups">
|
||||
<field name="name">School Administrator</field>
|
||||
<field name="privilege_id" ref="privilege_school"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_school_staff'))]"/>
|
||||
<field name="comment">Full read/write on the academic structure: years, terms, programs, batches.</field>
|
||||
</record>
|
||||
|
||||
<!-- Portal roles -->
|
||||
<record id="group_guardian" model="res.groups">
|
||||
<field name="name">Guardian</field>
|
||||
<field name="privilege_id" ref="privilege_school"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="comment">Portal access is scoped by mc.student.guardian links, never by holding this group alone. See CLAUDE.md sec 3.</field>
|
||||
</record>
|
||||
|
||||
<record id="group_student" model="res.groups">
|
||||
<field name="name">Student</field>
|
||||
<field name="privilege_id" ref="privilege_school"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="comment">Portal access to the student's own record only, enforced by record rules.</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
addons/mc_education_base/tests/__init__.py
Normal file
1
addons/mc_education_base/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import test_academic_calendar
|
||||
127
addons/mc_education_base/tests/test_academic_calendar.py
Normal file
127
addons/mc_education_base/tests/test_academic_calendar.py
Normal file
@ -0,0 +1,127 @@
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestAcademicCalendar(TransactionCase):
|
||||
|
||||
def test_setting_current_on_create_unsets_previous(self):
|
||||
Year = self.env["mc.academic.year"]
|
||||
year_a = Year.create({
|
||||
"name": "2025-26",
|
||||
"date_start": "2025-06-01",
|
||||
"date_end": "2026-04-30",
|
||||
"is_current": True,
|
||||
})
|
||||
self.assertTrue(year_a.is_current)
|
||||
|
||||
year_b = Year.create({
|
||||
"name": "2026-27",
|
||||
"date_start": "2026-06-01",
|
||||
"date_end": "2027-04-30",
|
||||
"is_current": True,
|
||||
})
|
||||
|
||||
self.assertFalse(year_a.is_current, "Creating a new current year must unset the old one.")
|
||||
self.assertTrue(year_b.is_current)
|
||||
|
||||
def test_setting_current_on_write_unsets_previous(self):
|
||||
Year = self.env["mc.academic.year"]
|
||||
year_a = Year.create({
|
||||
"name": "2025-26",
|
||||
"date_start": "2025-06-01",
|
||||
"date_end": "2026-04-30",
|
||||
"is_current": True,
|
||||
})
|
||||
year_b = Year.create({
|
||||
"name": "2026-27",
|
||||
"date_start": "2026-06-01",
|
||||
"date_end": "2027-04-30",
|
||||
"is_current": False,
|
||||
})
|
||||
|
||||
year_b.write({"is_current": True})
|
||||
|
||||
self.assertFalse(year_a.is_current)
|
||||
self.assertTrue(year_b.is_current)
|
||||
|
||||
@mute_logger("odoo.sql_db")
|
||||
def test_two_current_years_violate_db_index_when_orm_bypassed(self):
|
||||
# The write()/create() override auto-toggles is_current through the ORM.
|
||||
# The partial unique index is the actual enforcement layer for anything
|
||||
# that writes around it (direct SQL, a future bug in the toggle logic).
|
||||
Year = self.env["mc.academic.year"]
|
||||
Year.create({
|
||||
"name": "2025-26",
|
||||
"date_start": "2025-06-01",
|
||||
"date_end": "2026-04-30",
|
||||
"is_current": True,
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
with self.cr.savepoint():
|
||||
self.env.cr.execute(
|
||||
"INSERT INTO mc_academic_year "
|
||||
"(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())",
|
||||
(self.env.company.id, self.env.uid, self.env.uid),
|
||||
)
|
||||
|
||||
def test_year_end_before_start_raises(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env["mc.academic.year"].create({
|
||||
"name": "Bad Year",
|
||||
"date_start": "2026-06-01",
|
||||
"date_end": "2026-05-01",
|
||||
})
|
||||
|
||||
def test_term_end_before_start_raises(self):
|
||||
year = self.env["mc.academic.year"].create({
|
||||
"name": "2026-27",
|
||||
"date_start": "2026-06-01",
|
||||
"date_end": "2027-04-30",
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env["mc.academic.term"].create({
|
||||
"name": "Term 1",
|
||||
"year_id": year.id,
|
||||
"date_start": "2026-09-30",
|
||||
"date_end": "2026-06-01",
|
||||
})
|
||||
|
||||
def test_term_must_fall_within_year(self):
|
||||
year = self.env["mc.academic.year"].create({
|
||||
"name": "2026-27",
|
||||
"date_start": "2026-06-01",
|
||||
"date_end": "2027-04-30",
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env["mc.academic.term"].create({
|
||||
"name": "Term 1",
|
||||
"year_id": year.id,
|
||||
"date_start": "2026-05-01", # before the year starts
|
||||
"date_end": "2026-09-30",
|
||||
})
|
||||
|
||||
@mute_logger("odoo.sql_db")
|
||||
def test_duplicate_term_name_in_same_year_rejected(self):
|
||||
year = self.env["mc.academic.year"].create({
|
||||
"name": "2026-27",
|
||||
"date_start": "2026-06-01",
|
||||
"date_end": "2027-04-30",
|
||||
})
|
||||
self.env["mc.academic.term"].create({
|
||||
"name": "Term 1",
|
||||
"year_id": year.id,
|
||||
"date_start": "2026-06-01",
|
||||
"date_end": "2026-09-30",
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
with self.cr.savepoint():
|
||||
self.env["mc.academic.term"].create({
|
||||
"name": "Term 1",
|
||||
"year_id": year.id,
|
||||
"date_start": "2026-10-01",
|
||||
"date_end": "2026-12-31",
|
||||
})
|
||||
44
addons/mc_education_base/views/mc_academic_term_views.xml
Normal file
44
addons/mc_education_base/views/mc_academic_term_views.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_mc_academic_term_list" model="ir.ui.view">
|
||||
<field name="name">mc.academic.term.list</field>
|
||||
<field name="model">mc.academic.term</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Academic Terms">
|
||||
<field name="year_id"/>
|
||||
<field name="name"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_mc_academic_term_form" model="ir.ui.view">
|
||||
<field name="name">mc.academic.term.form</field>
|
||||
<field name="model">mc.academic.term</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Academic Term">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="Term 1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="year_id"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mc_academic_term" model="ir.actions.act_window">
|
||||
<field name="name">Academic Terms</field>
|
||||
<field name="res_model">mc.academic.term</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
60
addons/mc_education_base/views/mc_academic_year_views.xml
Normal file
60
addons/mc_education_base/views/mc_academic_year_views.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_mc_academic_year_list" model="ir.ui.view">
|
||||
<field name="name">mc.academic.year.list</field>
|
||||
<field name="model">mc.academic.year</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Academic Years">
|
||||
<field name="name"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="is_current" widget="boolean_toggle"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_mc_academic_year_form" model="ir.ui.view">
|
||||
<field name="name">mc.academic.year.form</field>
|
||||
<field name="model">mc.academic.year</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Academic Year">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="2026-27"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="is_current"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Terms">
|
||||
<field name="term_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mc_academic_year" model="ir.actions.act_window">
|
||||
<field name="name">Academic Years</field>
|
||||
<field name="res_model">mc.academic.year</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
17
addons/mc_education_base/views/mc_education_menus.xml
Normal file
17
addons/mc_education_base/views/mc_education_menus.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_school_root" name="School" sequence="10"
|
||||
groups="mc_education_base.group_school_staff"/>
|
||||
|
||||
<menuitem id="menu_school_configuration" name="Configuration"
|
||||
parent="menu_school_root" sequence="100"
|
||||
groups="mc_education_base.group_school_administrator"/>
|
||||
|
||||
<menuitem id="menu_mc_academic_year" name="Academic Years"
|
||||
parent="menu_school_configuration"
|
||||
action="action_mc_academic_year" sequence="10"/>
|
||||
|
||||
<menuitem id="menu_mc_academic_term" name="Academic Terms"
|
||||
parent="menu_school_configuration"
|
||||
action="action_mc_academic_term" sequence="20"/>
|
||||
</odoo>
|
||||
Loading…
x
Reference in New Issue
Block a user