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>
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
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",
|
|
})
|