15 Commits

Author SHA1 Message Date
metatroncubeswdev
ff96bf0f81 O9: a real public website - Metatroncube's marketing homepage, and a second Odoo branding leak found while building it
CLAUDE.md's own O9 spec says branding applies to "backend, portal,
website and every QWeb report" - the website part was never actually
built. The public side of the site was still bare stock Odoo: an
empty homepage (website.homepage ships as a literal
`<div id="wrap" class="oe_structure oe_empty"/>`) and the admission
form as the only real content anywhere a visitor could land.

Per discussion: this is Metatroncube's own product marketing page, not
the fictional school's site - it stays on a fixed brand (navy/indigo,
Manrope + Work Sans, an illustrative dashboard preview panel), not the
per-company colours the admission form and portal already read from
res.company. Swapping the demo company for the O9 closing beat
re-skins the product's own screens; it must not also repaint
Metatroncube's own marketing collateral.

Real second bug found building this, not related to the homepage
content itself: rendering it surfaced a second, separate "Powered by
Odoo" badge - web.brand_promotion, called from web.frontend_layout's
shared footer - that is completely different from the one
mc_education_theme already hid on the portal sidebar (different
template, different markup, an <img> logo rather than text). This one
sits in the footer of every website AND portal page and had been live
on the admission form this whole time; nobody had reason to scroll to
the bottom of that page and look. Neutralizing the shared, reusable
template rather than website.layout specifically means every current
and future caller of web.brand_promotion is covered by one fix.

New dependencies this actually needs and the spec's one-line
"Depends: mc_education_base" missed: `website` (to inherit
website.homepage) and `web` (to inherit web.brand_promotion) - on top
of `portal`, already added for the same reason last time. Flagging
per CLAUDE.md's closing instruction rather than working around it
quietly again.

Testing note worth recording: website.homepage resolves through a
per-website "copy-on-write" view, and Odoo auto-creates a default
website (COW'd immediately) before this module even loads. Checking
that per-website combination via _get_combined_arch() inside a
--test-enable run that installs and tests everything in one
transaction can see a stale, pre-COW empty result purely from cache
timing internal to that one transaction - confirmed NOT a real
defect by checking the same thing as a fresh request against the
actual long-running server, which renders correctly every time.
Tests here check what's actually deterministic (this module's own
authored view content) plus a live HTTP round trip proving the route
itself serves successfully, rather than chase that harness artifact.

Full suite (all 9 modules + web_responsive) re-verified together:
0 failed, 0 error(s) of 85 tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 18:39:40 -04:00
metatroncubeswdev
bd9b449ded fix: create the four backend demo logins the demo script actually needs
shared/DEMO_SCRIPT.md's cast table names six logins. Two - the parent
and student portal accounts - were created in mc_education_portal.
The other four (Principal, Front Office, Teacher, Accountant) were
explicitly punted to O9: mc_education_base's demo/mc_teacher_demo.xml
says outright "res.users/login creation is O9's job (demo rehearsal),
not this base module." That job was never actually done - found only
now, while writing a demo handbook that needed to print real, working
credentials and I checked them against a live container before
printing anything.

Concretely this means a from-scratch rehearsal of the demo script
would have stalled at Scene 1: there was no way to log in as Rekha
Nair, Sunitha R, or David Fernandes at all, and logging in as
"Arun Prakash" would have meant either sharing the admin account or
creating an unlinked user that fails Scene 4's actual point - a
teacher seeing only their own batch, which the attendance record rule
resolves through batch_id.class_teacher_id.user_id. Arun's login is
now attached to the same hr.employee record mc_education_base's own
demo data already made Grade 8-A's class teacher, not a fresh one.

Real Odoo behaviour learned fixing this, worth recording since it
will bite again: demo data loads with noupdate=True so a school's own
edits survive a later module update, and that guard is checked by
xmlid, not by which file is doing the writing - a <record> in this
module trying to set an existing mc_education_base demo record's
field was silently skipped, no error, nothing in the log. <function>
calls the ORM directly and is the correct tool, but carries the same
guard one level up in Odoo's loader (convert.py's _tag_function skips
entirely unless the module is being freshly installed, mode == 'init'
- an update on an already-installed database is a no-op). That is
exactly the real story this product cares about (a clean machine,
installing the whole suite once), so it's the right fix going
forward - verified on a from-scratch install of all nine modules
together. The one already-running database that had this module
installed before this fix existed needed a one-off manual correction
outside the codebase, not a workaround inside it.

Added a regression test asserting all four logins exist in the right
group and that Arun's employee record and Grade 8-A's class_teacher_id
actually resolve to the same person - not just "a teacher login
exists somewhere."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 15:06:31 -04:00
metatroncubeswdev
736c0e8c10 O9: mc_education_theme - per-company branding, the closing beat
Serves O9's acceptance test directly: swap to the second demo school
by switching the active company. Every branding field this module
configures - logo, primary/secondary colour, name, currency (on the
brand-new company only, see below), address - already exists on stock
res.company; this module adds zero new model fields, only demo data
and one view override. Two full companies exist because
shared/DEMO_SCRIPT.md names both by design: St. Aloysius Public
School (Coimbatore, the rebranded default company) and Waterloo
Heights Academy (Ontario, a fresh second company).

Removed the one concrete, spec-named piece of visible Odoo branding:
portal.portal_record_sidebar's "Powered by Odoo" logo, which renders
on every portal document page including the fee invoices
mc_education_portal links to - inherited the template and dropped
that div. Verified by actually rendering the template
(ir.qweb._render) and asserting the string is gone, not just that the
xpath applied without error.

Vendored OCA web_responsive per CLAUDE.md sec 2 ("pin every
third-party addon to a commit SHA, never a branch") - pinned to
b96307a8953f3bae075b8f7d94f40f00c2e63d1f on OCA/web's 19.0 branch.
Correcting my own third_party/README.md from O0 while doing this:
`git subtree add` on the whole OCA/web repo pulls every addon in that
monorepo (23MB+) for the sake of one - updated the instructions there
to fetch the pinned commit and extract just the one addon's directory
instead. Verified installable standalone against the live container
before vendoring it into the suite.

Real finding from testing the full 9-module suite installed together,
not this module in isolation: mc_education_fees's chart-of-accounts
setup (via `account`) posts journal items for the main company before
this module's demo data runs, and account.company.write() hard-blocks
a currency change once journal items exist - correctly so, you cannot
retroactively change a currency after transactions are posted in it.
Company currency belongs at true initial setup, before any financial
module has run, never as a later "apply a theme" change; dropped the
currency_id change on the rebranded main company accordingly (the
brand-new second company has no such history, so its currency is set
safely at creation). This is exactly why this session tests each
module against a real container, then the whole suite together before
calling anything done - this specific bug only exists when multiple
modules' demo data compose, and was invisible testing this module
alone.

Full-suite verification: all 9 mc_education_* modules plus
web_responsive installed together in one database, 81 tests across 8
of the 9 modules (mc_education_admission has none - a known gap, it
was verified by hand via curl/shell against the live container
instead, see its own commit), 0 failed, 0 errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:24:05 -04:00
metatroncubeswdev
8c8c79aa64 O8: mc_education_lms - thin glue linking a batch to a course channel
Serves Demo Scene 8. Per spec this is glue only, not a custom LMS:
mc.batch gets one new field (channel_id -> slide.channel), and
mc.enrollment's create()/write() calls the stock
slide.channel._action_add_members() when an enrollment becomes active
for a batch that has a channel - that's the entire feature. 40 lines
of model code, well under the spec's own "~200 lines or something has
gone wrong" ceiling.

Demo data matches Scene 8's script exactly - "Mathematics - Algebra
Basics": a video lesson, a PDF handout, a 5-question quiz (verified:
each question has exactly one correct and one incorrect answer, the
minimum website_slides itself requires).

This module took far longer to get right than its size suggests, and
the reason is worth recording. A ForeignKeyViolation on an unrelated
model (mc.batch referencing a channel Postgres said was never
inserted, despite that channel being created earlier in the same
file) sent the investigation looking for an install-time flush-
ordering bug for a long time - checking whether attachment=True
binary fields interact badly with a pending FK write in the same
flush batch, splitting the demo data across multiple files, even
routing the batch-channel link through a post_init_hook to sidestep
it. All of that was chasing a symptom. Bisecting the actual XML down
to a single record eventually surfaced the real, simple cause:
type="base64" on an XML <field> is only valid paired with a file=
attribute pointing to a real file in the addon - inline base64 text
raises a ValueError that Odoo's demo-data loader catches and
downgrades to "installed without demo data", and in an earlier
configuration (batch-link in the same file) that same swallowed
error surfaced instead as the confusing FK violation. Fixed by saving
the handout as a real file (static/demo/algebra_handout.pdf) and
referencing it properly; the post_init_hook and file-splitting were
reverted since the real fix needed neither. Verified byte-for-byte:
the attachment Odoo stores is exactly 604 bytes, detected as
application/pdf, matching the source file's md5sum.

Also caught before it reached git: line-ending conversion on this
binary PDF ("LF will be replaced by CRLF"), which would have silently
corrupted it on checkout for any contributor with Windows's
core.autocrlf on. Added *.pdf (and common image types) as binary to
.gitattributes and confirmed the staged blob's md5sum matches the
source file exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:01:06 -04:00
metatroncubeswdev
151bc32747 O7: mc_education_portal - parent/student portal, the live refused-access demo
Serves Demo Scene 5 in full, including the specific requirement to
demonstrate a refused access attempt live on the call. Every route
that takes a student id resolves it through _get_authorized_student(),
which does no manual comparison of its own - it relies on mc.student's
own record rules (a real mc.student.guardian link for guardians,
partner_id for students, never group membership alone, per CLAUDE.md
sec 3's "Guardian access is by relationship, not by role") to filter
a plain search(), and turns an empty result into an explicit
AccessError. The child switcher - "the single most convincing single
feature in the demo" per the spec - is just that same search with no
id filter, returning exactly the caller's own children.

Verified with real HTTP requests (HttpCase), not with_user()
shortcuts alone: authenticated as a real guardian, opened their own
child's dashboard and all four sub-pages (fees/attendance/timetable/
results) and got 200 on every one, then requested another family's
child by id and got 403 with the exact refusal message, on every one
of those same five routes. That is "editing the student identifier in
the URL and showing that it is refused" performed for real inside the
test, not asserted from a domain expression.

That same real-request testing earned its keep twice over, catching
two gaps a with_user()-only test would have missed entirely:
  - mc.batch had never been portal-reachable before this module and
    had no access row at all for guardian/student - a legitimate
    own-child dashboard load failed with a raw ACL error, not even a
    record-rule denial. Added mc.batch rules scoped the same way as
    everything else (via mc.batch.enrollment_ids, a new reverse field
    this module adds).
  - Rendering the timetable page touches mc.subject, mc.room, mc.exam,
    mc.academic.year/term and hr.employee through related-field reads,
    each of which triggers its own ACL check independent of whatever
    rule scopes mc.timetable.slot itself. Granted broad (unrestricted)
    guardian/student read on the non-sensitive reference models
    (subject/room/exam/year/term names - catalog data, not per-student
    data, same reasoning already applied to Teacher/Staff/Accountant
    in earlier modules); used sudo() instead for hr.employee
    specifically, since employee records carry real HR data no
    blanket portal grant should touch, with a comment noting the
    student was already authorized by that point in the request.

Fees needs no new rule at all: mc_education_fees deliberately bills
the primary guardian's own partner_id (a design decision made back in
O3, for exactly this reason), so stock account+portal's own
partner_id-scoped visibility already covers it, and group_guardian/
group_student already imply base.group_portal from O1.

mc.notice is a new model - shared/DOMAIN_MODEL.md lists it as an
entity Track O needs but no module spec in CLAUDE.md sec 5 ever gave
it a field table; added here since it is specifically an O7 view
requirement, scoped by the same batch-or-school-wide pattern as
everything else.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 13:32:38 -04:00
metatroncubeswdev
c77fb3d0e5 O6: mc_education_exam - grading scales as data, grid entry, report card PDF
Serves Demo Scene 6. mc.grading.scale + mc.grading.interval are pure
data (threshold/letter/point/description rows) - CBSE, ICSE, IB,
Cambridge, Ontario and a plain percentage scale all shipped as data
in data/mc_grading_scale_data.xml, noupdate="1" so a school's own
edits survive a module upgrade. mc.mark.grade resolves via
mc.program.grading_scale_id (new field on mc.program, since grading
board is a per-program concept already carrying "board" from O1) -
the moment this needed an `if board == "CBSE"` anywhere, per
shared/DOMAIN_MODEL.md sec 5, it would have been the wrong design;
verified instead that swapping a program's scale recomputes an
existing mark's grade with zero code involved
(test_changing_scale_recomputes_existing_marks).

Grid mark entry ("whole class on one screen, tab between fields,
keyboard only") is a plain editable list on mc.mark, filtered to one
exam - that native Odoo behavior already gives real Tab-key
navigation with no custom widget. mc.exam.action_enter_marks()
pre-creates a row per actively-enrolled student before opening it
(mirroring O4's bulk-attendance idempotency: creating it again never
duplicates or resets an already-entered mark).

The report card is a real rendered PDF, not just a template read by
inspection - rendered it for Aditya via odoo shell
(report._render_qweb_pdf), pulled the bytes out of the container, and
read the actual PDF: one A4 page, no clipped columns, correct grades
per subject matching the CBSE scale (92->A1, 85->A2, 78->B1, 67->B2,
58->C1), signature block, term correctly resolved. That last part
exposed a real logic bug before it shipped: the term-resolution
method originally preferred "today's date" over "the term with
marks", which would have shown an empty Term 1 when generating the
card *today* even though the demo's graded marks are all in Term 2 -
a report card is generated to review a term's results, typically
after that term's exams are done (often during the *next* term), so
marks-with-data now wins over the calendar, falling back to today's
date only for a student with nothing graded yet.

Attendance summary on the report degrades to None when
mc_education_attendance isn't installed (checked directly, not
assumed) - this module depends on mc_education_base only, matching
"each mc_education_* module installs independently" (CLAUDE.md sec
1.4), while still showing the real summary when both are installed
together for the actual demo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 13:16:49 -04:00
metatroncubeswdev
a36c89222b O5: mc_education_timetable - one dataset, three views, conflict-checked
Serves Demo Scene 7: mc.timetable.slot (batch, weekday, period,
subject, teacher, room, year, term) is the single dataset. "Three
read views" are three entry points into that same model rather than
three separate data structures - a stat button on mc.batch, on
mc.teacher, and on mc.student (resolved through the student's active
enrollment to their batch) each open the same list/search action with
a different domain. Auto-generation is out of scope per spec; this
module only configures slots by hand (demo data is a real Mon-Fri
week for Grade 8-A, since a timetable is a recurring weekly pattern
and one week fully represents it, unlike attendance/fees which
genuinely need a run of history).

Conflict prevention ("a teacher or a room cannot hold two slots in
the same weekday+period") follows the same two-layer pattern used for
mc.academic.year.is_current and mc.enrollment in O1: a partial unique
index per conflict type (teacher, room, and - not explicitly asked
for but an obvious extension of the same rule - batch, since a batch
can't be in two places at once either) is the actual guarantee, and a
pre-check in create()/write() raises a readable ValidationError
before the insert/update, not after, for the same reason established
building mc.enrollment: the DB index fires first and the friendly
message is unreachable otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 12:53:47 -04:00
metatroncubeswdev
e5aba291f1 O4: mc_education_attendance - mobile-first bulk marking, own batches only
Serves Demo Scene 4: mc.attendance (student x date x session, unique
constraint so resubmission can never duplicate) plus a bulk-marking
wizard - pick a batch/date/session, the roster loads pre-filled
Present (or whatever was already recorded, if reopening), tap to
change an exception, Submit. Submitting again for the same
batch/date/session updates the same rows rather than creating
duplicates - verified with a real test that marks a batch present,
then reopens and corrects one student, then asserts there are still
exactly two rows, not three.

The "teachers may only mark their own batches" rule is a record rule
(ir.rule scoped to group_teacher via batch_id.class_teacher_id.
user_id), not a UI check, per CLAUDE.md sec 3 and the O4 spec line
verbatim. Verified for real, not just declared: a teacher user who
is not the class teacher of a batch gets AccessError on create *and*
on reading an existing attendance row by id directly (the actual
"cannot open another teacher's batch by editing the URL" scenario),
while Administrator remains unrestricted since the rule's `groups`
field scopes it to teacher only.

Caught two Odoo 19 search-view schema changes while installing
against a live odoo:19.0 container - a plain read of the view XML
wouldn't have caught these, only trying to actually load it did:
neither the group-by `<group>` element nor the filter groups inside
a `<search>` view accept a `string` or `expand` attribute anymore
(confirmed against hr's own search views, which use bare `<group>`).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 12:49:04 -04:00
metatroncubeswdev
d6f891838b O3: mc_education_fees - structures, schedules, concessions, invoicing
Serves Demo Scene 3: a fee structure (category x amount lines) with a
term-wise installment schedule generates a correct account.move for a
given enrollment + term, a concession recalculates it correctly, and
the invoice is billed to the primary guardian's partner so stock
portal invoice visibility (partner_id-based) works with no new record
rule. Invoices are plain account.move (_inherit adds mc_student_id/
mc_enrollment_id only) - no invoice model was built, per CLAUDE.md
sec 1.3.

Fixed a real modeling mistake before it shipped: the schedule's
"percentages must total 100%" rule was originally a blocking
@api.constrains on every line write, which breaks the normal workflow
of adding one term at a time (every intermediate state before the
last line is, correctly, under 100%) - and would have broken this
module's own demo data loading, since each schedule line is a
separate XML record. Moved the check to where it actually matters:
the invoice-generation wizard now raises a clear UserError if the
resolved schedule doesn't total 100% at the point of use, while a
live constraint still blocks the one thing that's unambiguously wrong
at any point - allocating more than 100%.

Also found, by testing money arithmetic against a live odoo:19.0
container rather than trusting the arithmetic by inspection: every
generated invoice total came back at exactly 1.15x the expected
amount, because the standing "School Fee" product picked up the demo
company's default sales tax. Fixed by explicitly clearing taxes_id on
the product - school fees are correctly untaxed (education services
are GST-exempt in India), not just conveniently untaxed for the test.

Testing this module's access rules surfaced two real bugs in the
security model, not just test bugs, fixed here:
  - group_school_staff (from O1) never implied base.group_user, so
    any real user holding only this app's custom groups lacked
    ordinary internal-user access to core models like res.company -
    caught directly via a test user unable to even create an
    mc.fee.structure (whose company_id defaults through
    self.env.company).
  - mc.fee.concession reveals sensitive per-student financial data
    (e.g. a need-based hardship discount and its reason). Staff had
    read access, and since group_teacher implies group_school_staff,
    teachers inherited it too - exposing family financial
    circumstances to a role with no legitimate need for it. Removed
    the staff access row; only Administrator/Accountant keep it now.
    Fee *structure* (per-program pricing, not sensitive) correctly
    stays staff/teacher-readable.

CLAUDE.md gets one more Odoo 19 API correction:
res.users.groups_id -> group_ids.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 12:34:26 -04:00
metatroncubeswdev
508bf27417 O2: mc_education_admission - public form through to enrolled student
Serves Demo Scene 2 end to end: mc.applicant with the stage pipeline
(Applied -> Document Verification -> Interview -> Offered -> Accepted
-> Enrolled/Rejected) on mail.thread, a public admission page on the
website, and a convert wizard that turns an accepted applicant into a
real mc.student + mc.enrollment with zero re-typing.

The public form uses Odoo's stock /website/form/<model> mechanism,
not a custom controller (CLAUDE.md sec 1.3 - writing a custom version
of stock infrastructure is a bug). Verified the real mechanism against
core source first rather than assuming: website_hr_recruitment's own
data/config_data.xml is the template this follows (ir.model.
website_form_access + ir.model.fields.formbuilder_whitelist()).

This is the module's actual security boundary, and it's worth being
explicit about why it holds. The generic controller creates the record
as SUPERUSER - normal ir.model.access rows do not apply to it at all.
The only thing stopping a submitter from setting state, student_id,
application_no or company_id is that those fields are not in the
formbuilder_whitelist() call in data/mc_applicant_website_form_data.xml
(every field defaults to website_form_blacklisted=True and stays that
way unless explicitly opted in). Confirmed this isn't just theoretical:
posted state=enrolled and application_no=HACKED-0001 directly at
/website/form/mc.applicant on a live instance, and the resulting
record came back with the model's own default state=applied and a
server-generated APP20260004 - the injected values were silently
dropped, exactly as the whitelist should do. Also exercised a real
file upload (birth certificate) and the full convert-to-student path
(guardian dedup by email, application_no -> student.application_no,
enrollment, attachment reparenting) via odoo shell against the live
container, not just read by inspection.

mc.student gets a new application_no field (_inherit from this
module, not O1 - it only makes sense where admission is installed)
so "the application number persists on the student" is a stored fact,
not just a claim in the demo script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 11:55:48 -04:00
metatroncubeswdev
4a2e7b7016 fix: admin group membership on install, and db_name breaking the db list
- group_school_administrator now includes base.user_admin, matching
  core Odoo's own convention (see hr.group_hr_manager) - otherwise
  every fresh install requires a manual trip to Settings > Users just
  to see this module's own menus.
- odoo.conf: drop db_name. Found by hand while testing: when db_name
  is set and dbfilter is not, Odoo's list_dbs() returns db_name's
  value verbatim instead of querying postgres, so the database
  selector shows only that one database no matter how many actually
  exist. Silently breaks O9's "swap to the second demo school in
  under five minutes" requirement, which depends on switching between
  multiple real databases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 11:25:37 -04:00
metatroncubeswdev
709bd2eca7 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>
2026-09-11 09:58:36 -04:00
metatroncubeswdev
1e5ac14510 CLAUDE.md: correct two Odoo 19 API claims found building O1
Coding standards said "No APIs deprecated in 17 or 18" as if that
were sufficient - it isn't, because it's phrased as a non-regression
check against an assumed-correct baseline, and the baseline itself
(an 18-era mental model of _sql_constraints and res.groups) was
already wrong for 19. Verified against a real odoo:19.0 container
while building mc_education_base: _sql_constraints is replaced by
per-attribute models.Constraint(sql, message), and res.groups lost
category_id in favor of a new res.groups.privilege record referenced
via privilege_id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 08:53:52 -04:00
metatroncubeswdev
8b7bd91f74 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>
2026-09-11 08:53:34 -04:00
metatroncubeswdev
85f2b4b11a O0: Docker/Odoo 19 CE scaffolding
docker-compose.yml (postgres 16 + odoo:19.0), odoo.conf, env template,
and setup/backup/update/demo-data scripts. update.sh always backs up
and records the previous image before upgrading modules, per the
lesson paid for on the Frappe track. .gitattributes pins LF on shell
scripts so they don't break under the container's bash on checkout
from Windows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 06:45:58 -04:00