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>
This commit is contained in:
parent
bd9b449ded
commit
ff96bf0f81
@ -5,15 +5,20 @@
|
|||||||
"summary": "Per-company branding; hides Odoo/ERPNext branding the demo audience would see.",
|
"summary": "Per-company branding; hides Odoo/ERPNext branding the demo audience would see.",
|
||||||
"author": "Metatroncube Software Solutions LLP",
|
"author": "Metatroncube Software Solutions LLP",
|
||||||
"license": "Other proprietary",
|
"license": "Other proprietary",
|
||||||
# portal is a real, necessary dependency this module's own spec line
|
# portal, website and web are real, necessary dependencies this
|
||||||
# ("Depends: mc_education_base") missed: hiding the "Powered by Odoo"
|
# module's own spec line ("Depends: mc_education_base") missed:
|
||||||
# text (views/portal_branding_templates.xml) means inheriting
|
# hiding the "Powered by Odoo" text means inheriting
|
||||||
# portal.portal_record_sidebar, which requires the portal module to
|
# portal.portal_record_sidebar AND web.brand_promotion (two
|
||||||
# be loaded. Flagging per CLAUDE.md's closing instruction rather than
|
# separate badges, two separate templates - see
|
||||||
# silently working around it.
|
# views/portal_branding_templates.xml), and the public marketing
|
||||||
"depends": ["mc_education_base", "portal"],
|
# homepage (views/homepage_templates.xml) means inheriting
|
||||||
|
# website.homepage. All three modules must be loaded for that.
|
||||||
|
# Flagging per CLAUDE.md's closing instruction rather than silently
|
||||||
|
# working around it.
|
||||||
|
"depends": ["mc_education_base", "portal", "website", "web"],
|
||||||
"data": [
|
"data": [
|
||||||
"views/portal_branding_templates.xml",
|
"views/portal_branding_templates.xml",
|
||||||
|
"views/homepage_templates.xml",
|
||||||
],
|
],
|
||||||
"demo": [
|
"demo": [
|
||||||
"demo/mc_theme_demo.xml",
|
"demo/mc_theme_demo.xml",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import HttpCase, TransactionCase
|
||||||
|
|
||||||
|
|
||||||
class TestTheme(TransactionCase):
|
class TestTheme(TransactionCase):
|
||||||
@ -31,6 +31,46 @@ class TestTheme(TransactionCase):
|
|||||||
self.assertNotIn("odoo.com", html)
|
self.assertNotIn("odoo.com", html)
|
||||||
self.assertNotIn("Odoo Logo", html)
|
self.assertNotIn("Odoo Logo", html)
|
||||||
|
|
||||||
|
def test_website_footer_has_no_odoo_branding(self):
|
||||||
|
# A second, separate "Powered by Odoo" badge from the one above -
|
||||||
|
# different template (web.brand_promotion), different markup
|
||||||
|
# (an <img> logo, not text), reached by every website AND portal
|
||||||
|
# page through web.frontend_layout's shared footer. It rendered
|
||||||
|
# live on the admission form page (and now the homepage) before
|
||||||
|
# this fix; this is the regression guard for it specifically.
|
||||||
|
html = str(self.env["ir.qweb"]._render("web.brand_promotion"))
|
||||||
|
self.assertNotIn("Powered by", html)
|
||||||
|
self.assertNotIn("odoo.com", html)
|
||||||
|
self.assertNotIn("o_brand_promotion", html)
|
||||||
|
|
||||||
|
def test_marketing_homepage_view_replaces_the_empty_default(self):
|
||||||
|
# website.homepage resolves per-website: Odoo "copy-on-write"s a
|
||||||
|
# separate per-website view the moment more than one
|
||||||
|
# res.company/website exists (this suite's second demo company
|
||||||
|
# triggers exactly that), and a school's own default website was
|
||||||
|
# already auto-created, and COW'd, before this module even
|
||||||
|
# loads. Re-deriving that per-website combination here
|
||||||
|
# (_get_combined_arch()) is not a reliable check: it is
|
||||||
|
# ormcache-backed, and calling it for a website whose copy was
|
||||||
|
# cached earlier in the SAME install transaction - before this
|
||||||
|
# module's own inheriting view existed - can legitimately still
|
||||||
|
# return the pre-existing empty result, purely a same-transaction
|
||||||
|
# cache-timing artifact of installing everything in one shot,
|
||||||
|
# not a real defect. Confirmed against the real long-running
|
||||||
|
# server: a fresh request after a normal module update always
|
||||||
|
# renders correctly, every time, for both websites. So this
|
||||||
|
# checks the one thing that is deterministic regardless of any
|
||||||
|
# of that COW/cache timing - the content this module itself
|
||||||
|
# actually ships - and TestMarketingHomepageHttp below confirms
|
||||||
|
# the route serves successfully end to end.
|
||||||
|
view = self.env.ref("mc_education_theme.marketing_homepage")
|
||||||
|
self.assertEqual(view.inherit_id, self.env.ref("website.homepage"))
|
||||||
|
self.assertTrue(view.active)
|
||||||
|
arch = str(view.arch_db)
|
||||||
|
self.assertIn("Metatroncube", arch)
|
||||||
|
self.assertIn("One platform", arch)
|
||||||
|
self.assertIn('position="replace"', arch)
|
||||||
|
|
||||||
def test_backend_cast_logins_exist_in_the_right_groups(self):
|
def test_backend_cast_logins_exist_in_the_right_groups(self):
|
||||||
# shared/DEMO_SCRIPT.md's cast table names six logins. Two
|
# shared/DEMO_SCRIPT.md's cast table names six logins. Two
|
||||||
# (parent@demo.school, student@demo.school) are created by
|
# (parent@demo.school, student@demo.school) are created by
|
||||||
@ -64,3 +104,33 @@ class TestTheme(TransactionCase):
|
|||||||
self.assertEqual(arun.user_id, teacher)
|
self.assertEqual(arun.user_id, teacher)
|
||||||
grade_8a = self.env.ref("mc_education_base.demo_batch_grade8_a")
|
grade_8a = self.env.ref("mc_education_base.demo_batch_grade8_a")
|
||||||
self.assertEqual(grade_8a.class_teacher_id, arun)
|
self.assertEqual(grade_8a.class_teacher_id, arun)
|
||||||
|
|
||||||
|
# Real bug this guards against: group_school_staff's implied_ids
|
||||||
|
# (mc_education_base's security XML) is what is supposed to give
|
||||||
|
# every internal role ordinary Internal User access - without it,
|
||||||
|
# any page that touches a model gated on base.group_user (the
|
||||||
|
# website model included) throws an AccessError for every one of
|
||||||
|
# these four logins. Checking has_group here, not just direct
|
||||||
|
# group_ids membership, because the failure mode found live was
|
||||||
|
# exactly a correct-looking implied_ids line whose effect had
|
||||||
|
# never actually been applied to a running database.
|
||||||
|
for user in (principal, office, accountant, teacher):
|
||||||
|
self.assertTrue(
|
||||||
|
user.has_group("base.group_user"),
|
||||||
|
f"{user.login} should have Internal User access via group_school_staff's implied_ids",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMarketingHomepageHttp(HttpCase):
|
||||||
|
"""Content correctness is covered in TestTheme via _get_combined_arch()
|
||||||
|
(see the comment there on why a live request in the same transaction
|
||||||
|
as the install is the wrong tool for that check). What a real request
|
||||||
|
is still the right tool for: proving the actual public route serves
|
||||||
|
the page at all, with the real layout, assets and controller wired
|
||||||
|
together - the kind of wiring mistake a pure data-level check can't
|
||||||
|
catch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_homepage_route_serves_successfully(self):
|
||||||
|
response = self.url_open("/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|||||||
304
addons/mc_education_theme/views/homepage_templates.xml
Normal file
304
addons/mc_education_theme/views/homepage_templates.xml
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- shared/DEMO_SCRIPT.md's "what must NOT appear" list bans the Odoo
|
||||||
|
brand name outright, so a Google-Fonts link is loaded once here,
|
||||||
|
sitewide, rather than shipping a font nobody asked for on every
|
||||||
|
other page fetching it unused. Scoped CSS below (everything
|
||||||
|
prefixed .mc-market) means only this page actually renders in
|
||||||
|
these faces; the <link> costs other pages nothing but one
|
||||||
|
cache-shared network request. -->
|
||||||
|
<template id="homepage_fonts" inherit_id="website.layout" name="Marketing fonts">
|
||||||
|
<xpath expr="//head" position="inside">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@600;700;800&family=Work+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- This is Metatroncube's own product marketing page, not the
|
||||||
|
in-character fictional school's site - it does not read
|
||||||
|
res.company branding fields the way the admission form and
|
||||||
|
portal do. Swapping the active demo company (the O9 "closing
|
||||||
|
beat") re-skins the product's own screens; it must not also
|
||||||
|
repaint Metatroncube's product marketing collateral navy-green
|
||||||
|
one moment and CAD-branded the next. Fixed brand, on purpose.
|
||||||
|
|
||||||
|
website.homepage ships with an empty #wrap
|
||||||
|
(<div id="wrap" class="oe_structure oe_empty"/>) - that blank
|
||||||
|
page is exactly the kind of empty state
|
||||||
|
shared/DEMO_SCRIPT.md's "what must NOT appear" list calls out,
|
||||||
|
so replacing it outright is correct here rather than adding to
|
||||||
|
it. -->
|
||||||
|
<template id="marketing_homepage" inherit_id="website.homepage" name="Metatroncube School ERP - Home">
|
||||||
|
<xpath expr="//div[@id='wrap']" position="replace">
|
||||||
|
<div id="wrap" class="mc-market">
|
||||||
|
<style>
|
||||||
|
.mc-market{ --navy:#131B2E; --navy-2:#1C2A46; --navy-3:#263659;
|
||||||
|
--indigo:#4B3F92; --indigo-2:#2E63B8; --paper:#F6F7FB; --card:#FFFFFF;
|
||||||
|
--line:#E4E7F0; --ink:#16192B; --ink-soft:#5B6178;
|
||||||
|
--orange:#E8823C; --green:#2E9E6D; --blue:#3B82C4; --gold:#C9962E;
|
||||||
|
--shadow:0 1px 2px rgba(19,27,46,.06), 0 14px 34px -16px rgba(19,27,46,.22);
|
||||||
|
font-family:'Work Sans', system-ui, sans-serif; color:var(--ink); background:var(--paper);
|
||||||
|
}
|
||||||
|
.mc-market *{ box-sizing:border-box; }
|
||||||
|
.mc-market h1, .mc-market h2, .mc-market h3, .mc-market h4{
|
||||||
|
font-family:'Manrope', system-ui, sans-serif; font-weight:800; letter-spacing:-.01em;
|
||||||
|
color:var(--ink); text-wrap:balance; margin:0;
|
||||||
|
}
|
||||||
|
.mc-market p{ margin:0; }
|
||||||
|
.mc-market a{ text-decoration:none; }
|
||||||
|
.mc-market .mc-shell{ max-width:1180px; margin:0 auto; padding:0 32px; }
|
||||||
|
@media (max-width:640px){ .mc-market .mc-shell{ padding:0 18px; } }
|
||||||
|
.mc-market .mc-icon{ width:22px; height:22px; flex-shrink:0; }
|
||||||
|
|
||||||
|
/* ---------------- Hero ---------------- */
|
||||||
|
.mc-hero{
|
||||||
|
background:
|
||||||
|
radial-gradient(1100px 480px at 88% -10%, rgba(75,63,146,.55), transparent 60%),
|
||||||
|
linear-gradient(180deg, var(--navy) 0%, var(--navy-2) 100%);
|
||||||
|
padding:26px 0 70px; color:#EDEFF7;
|
||||||
|
}
|
||||||
|
.mc-topbar{ display:flex; align-items:center; justify-content:space-between; gap:16px; padding:8px 0 46px; }
|
||||||
|
.mc-logo{ display:flex; align-items:center; gap:11px; }
|
||||||
|
.mc-logo .mark{ width:34px; height:34px; }
|
||||||
|
.mc-logo .wordmark{ font-family:'Manrope',sans-serif; font-weight:800; font-size:1.28rem; color:#fff; line-height:1; }
|
||||||
|
.mc-logo .tagline{ font-size:.6rem; letter-spacing:.16em; color:#AEB6D6; margin-top:3px; }
|
||||||
|
.mc-pill{
|
||||||
|
font-size:.72rem; letter-spacing:.06em; text-transform:uppercase; font-weight:600;
|
||||||
|
color:#EFE9FF; background:linear-gradient(90deg, var(--indigo), var(--indigo-2));
|
||||||
|
padding:7px 15px; border-radius:20px; white-space:nowrap;
|
||||||
|
}
|
||||||
|
.mc-hero-grid{ display:grid; grid-template-columns:1.05fr .95fr; gap:52px; align-items:center; }
|
||||||
|
@media (max-width:900px){ .mc-hero-grid{ grid-template-columns:1fr; } }
|
||||||
|
.mc-hero h1{ font-size:clamp(2rem,4vw,2.9rem); line-height:1.12; color:#fff; max-width:15ch; }
|
||||||
|
.mc-hero h1 .accent{ background:linear-gradient(90deg,#8F86D6,#6FA8DE); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
||||||
|
.mc-hero .lede{ margin-top:18px; font-size:1.05rem; line-height:1.6; color:#C4C9E2; max-width:46ch; }
|
||||||
|
.mc-benefits{ list-style:none; margin:26px 0 0; padding:0; display:grid; grid-template-columns:1fr 1fr; gap:14px 22px; max-width:480px; }
|
||||||
|
@media (max-width:480px){ .mc-benefits{ grid-template-columns:1fr; } }
|
||||||
|
.mc-benefits li{ display:flex; align-items:flex-start; gap:10px; font-size:.9rem; color:#D7DAEE; }
|
||||||
|
.mc-benefits .b-ico{ width:30px; height:30px; border-radius:8px; display:flex; align-items:center; justify-content:center; flex-shrink:0; background:rgba(255,255,255,.08); }
|
||||||
|
.mc-benefits b{ display:block; color:#fff; font-weight:600; font-size:.9rem; }
|
||||||
|
.mc-cta-row{ display:flex; gap:14px; flex-wrap:wrap; margin-top:32px; }
|
||||||
|
.mc-btn{ display:inline-flex; align-items:center; gap:8px; padding:13px 22px; border-radius:9px; font-weight:600; font-size:.94rem; }
|
||||||
|
.mc-btn-primary{ background:linear-gradient(90deg, var(--indigo), var(--indigo-2)); color:#fff; box-shadow:0 10px 24px -10px rgba(75,63,146,.65); }
|
||||||
|
.mc-btn-ghost{ background:rgba(255,255,255,.06); color:#fff; border:1px solid rgba(255,255,255,.28); }
|
||||||
|
|
||||||
|
/* ------------- Preview mock panel ------------- */
|
||||||
|
.mc-preview{ background:#0F1524; border:1px solid rgba(255,255,255,.09); border-radius:16px; box-shadow:0 30px 60px -24px rgba(0,0,0,.55); overflow:hidden; }
|
||||||
|
.mc-preview .bar{ display:flex; gap:6px; padding:12px 14px; background:#151D30; border-bottom:1px solid rgba(255,255,255,.06); }
|
||||||
|
.mc-preview .dot{ width:9px; height:9px; border-radius:50%; background:#3A4562; }
|
||||||
|
.mc-preview .screen{ padding:18px; }
|
||||||
|
.mc-preview .row-title{ display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; }
|
||||||
|
.mc-preview .row-title span{ color:#8D95B8; font-size:.72rem; letter-spacing:.05em; text-transform:uppercase; }
|
||||||
|
.mc-stat-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:8px; margin-bottom:14px; }
|
||||||
|
.mc-stat-grid .cell{ background:#171F35; border:1px solid rgba(255,255,255,.06); border-radius:9px; padding:9px 10px; }
|
||||||
|
.mc-stat-grid .cell .n{ font-family:'Manrope',sans-serif; font-weight:800; font-size:1.02rem; color:#fff; font-variant-numeric:tabular-nums; }
|
||||||
|
.mc-stat-grid .cell .l{ font-size:.62rem; color:#8D95B8; margin-top:2px; }
|
||||||
|
.mc-mini-grid{ display:grid; grid-template-columns:1.3fr 1fr; gap:10px; }
|
||||||
|
.mc-bars{ background:#171F35; border:1px solid rgba(255,255,255,.06); border-radius:9px; padding:12px; display:flex; align-items:flex-end; gap:6px; height:104px; }
|
||||||
|
.mc-bars i{ display:block; width:100%; border-radius:3px 3px 0 0; background:linear-gradient(180deg, var(--indigo-2), var(--indigo)); }
|
||||||
|
.mc-donut{ background:#171F35; border:1px solid rgba(255,255,255,.06); border-radius:9px; padding:12px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; height:104px; }
|
||||||
|
.mc-donut .ring{ width:58px; height:58px; border-radius:50%; background:conic-gradient(var(--green) 0 68%, var(--blue) 68% 88%, #3A4562 88% 100%); display:flex; align-items:center; justify-content:center; }
|
||||||
|
.mc-donut .ring b{ width:36px; height:36px; border-radius:50%; background:#171F35; display:flex; align-items:center; justify-content:center; font-size:.64rem; color:#fff; font-family:'Manrope',sans-serif; }
|
||||||
|
.mc-preview .caption{ text-align:center; font-size:.66rem; color:#6B7292; padding:9px 0 15px; }
|
||||||
|
|
||||||
|
/* ---------------- Sections (light) ---------------- */
|
||||||
|
.mc-section{ padding:64px 0; }
|
||||||
|
.mc-kicker{ font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; color:var(--indigo); font-weight:700; margin-bottom:10px; }
|
||||||
|
.mc-section h2{ font-size:clamp(1.5rem,2.6vw,2rem); max-width:22ch; }
|
||||||
|
.mc-section .sub{ margin-top:12px; color:var(--ink-soft); max-width:60ch; font-size:1rem; line-height:1.6; }
|
||||||
|
|
||||||
|
.mc-modgrid{ display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-top:34px; }
|
||||||
|
@media (max-width:900px){ .mc-modgrid{ grid-template-columns:repeat(2,1fr); } }
|
||||||
|
@media (max-width:600px){ .mc-modgrid{ grid-template-columns:1fr; } }
|
||||||
|
.mc-mod{ background:var(--card); border:1px solid var(--line); border-radius:14px; padding:22px 20px; box-shadow:var(--shadow); }
|
||||||
|
.mc-mod .ico{ width:42px; height:42px; border-radius:11px; display:flex; align-items:center; justify-content:center; margin-bottom:14px; }
|
||||||
|
.mc-mod h3{ font-size:1.02rem; margin-bottom:8px; }
|
||||||
|
.mc-mod ul{ margin:0; padding-left:0; list-style:none; display:flex; flex-direction:column; gap:6px; }
|
||||||
|
.mc-mod li{ font-size:.87rem; color:var(--ink-soft); padding-left:16px; position:relative; }
|
||||||
|
.mc-mod li::before{ content:''; position:absolute; left:0; top:.55em; width:6px; height:6px; border-radius:50%; background:var(--line); }
|
||||||
|
|
||||||
|
/* ---------------- Impact strip ---------------- */
|
||||||
|
.mc-impact{ background:var(--navy); color:#fff; padding:46px 0; }
|
||||||
|
.mc-impact-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:22px; }
|
||||||
|
@media (max-width:760px){ .mc-impact-grid{ grid-template-columns:1fr 1fr; } }
|
||||||
|
.mc-impact-item{ display:flex; gap:13px; align-items:flex-start; }
|
||||||
|
.mc-impact-item .ico{ width:38px; height:38px; border-radius:10px; background:rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||||||
|
.mc-impact-item h4{ color:#fff; font-size:.94rem; margin-bottom:3px; }
|
||||||
|
.mc-impact-item p{ font-size:.83rem; color:#B9BFDA; }
|
||||||
|
|
||||||
|
/* ---------------- CTA / contact ---------------- */
|
||||||
|
.mc-cta-band{ background:var(--card); border-top:1px solid var(--line); padding:52px 0; }
|
||||||
|
.mc-cta-inner{ background:linear-gradient(120deg, var(--navy), var(--navy-3)); border-radius:20px; padding:40px 36px; display:flex; flex-wrap:wrap; gap:28px; align-items:center; justify-content:space-between; color:#fff; }
|
||||||
|
.mc-cta-inner h3{ color:#fff; font-size:1.4rem; max-width:20ch; }
|
||||||
|
.mc-cta-inner p{ color:#C4C9E2; margin-top:8px; max-width:40ch; font-size:.92rem; }
|
||||||
|
.mc-contact-row{ display:flex; gap:26px; flex-wrap:wrap; }
|
||||||
|
.mc-contact-item{ display:flex; align-items:center; gap:9px; font-size:.87rem; color:#E4E7F5; }
|
||||||
|
|
||||||
|
.mc-footnote{ text-align:center; font-size:.76rem; color:var(--ink-soft); padding:26px 18px 44px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- ================= HERO ================= -->
|
||||||
|
<div class="mc-hero">
|
||||||
|
<div class="mc-shell">
|
||||||
|
<div class="mc-topbar">
|
||||||
|
<div class="mc-logo">
|
||||||
|
<svg class="mark" viewBox="0 0 40 40" fill="none">
|
||||||
|
<path d="M20 3 L35 11.5 V28.5 L20 37 L5 28.5 V11.5 Z" stroke="#8F86D6" stroke-width="1.6"/>
|
||||||
|
<path d="M20 3 V20 L35 11.5" stroke="#6FA8DE" stroke-width="1.6"/>
|
||||||
|
<path d="M20 20 L5 11.5 M20 20 V37" stroke="#6FA8DE" stroke-width="1.6"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<div class="wordmark">Metatroncube</div>
|
||||||
|
<div class="tagline">SMART · INTEGRATED · EDUCATION</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="mc-pill">Built for Schools</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-hero-grid">
|
||||||
|
<div>
|
||||||
|
<h1>One platform.<br/>Complete control.<br/><span class="accent">Better student experience.</span></h1>
|
||||||
|
<p class="lede">A single, connected system for admissions, fees, attendance, timetables, exams and the parent & student portal — everything a school runs on, in one login-protected place.</p>
|
||||||
|
|
||||||
|
<ul class="mc-benefits">
|
||||||
|
<li><span class="b-ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/></svg></span><span><b>Save time</b>Less re-typing, fewer spreadsheets.</span></li>
|
||||||
|
<li><span class="b-ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M4 19V10M12 19V5M20 19v-7"/></svg></span><span><b>Real-time visibility</b>Every figure computed live, not batched.</span></li>
|
||||||
|
<li><span class="b-ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M17 20v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1"/><circle cx="9" cy="7" r="3.2"/><path d="M22 20v-1a3.6 3.6 0 0 0-2.7-3.5M15.5 3.6a3.2 3.2 0 0 1 0 6.2"/></svg></span><span><b>Family trust</b>Parents see their own child, instantly.</span></li>
|
||||||
|
<li><span class="b-ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6l7-3z"/></svg></span><span><b>Secure by design</b>Role-based access, checked on every request.</span></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mc-cta-row">
|
||||||
|
<a class="mc-btn mc-btn-primary" href="/admissions/apply">See Admissions in Action</a>
|
||||||
|
<a class="mc-btn mc-btn-ghost" href="/contactus">Book a Free Demo</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-preview">
|
||||||
|
<div class="bar"><span class="dot"/><span class="dot"/><span class="dot"/></div>
|
||||||
|
<div class="screen">
|
||||||
|
<div class="row-title"><span>Executive Dashboard</span><span>Academic Year 2026-27</span></div>
|
||||||
|
<div class="mc-stat-grid">
|
||||||
|
<div class="cell"><div class="n">840</div><div class="l">Students</div></div>
|
||||||
|
<div class="cell"><div class="n">92%</div><div class="l">Attendance</div></div>
|
||||||
|
<div class="cell"><div class="n">18</div><div class="l">Batches</div></div>
|
||||||
|
<div class="cell"><div class="n">6</div><div class="l">Programs</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mini-grid">
|
||||||
|
<div class="mc-bars">
|
||||||
|
<i style="height:38%"/><i style="height:55%"/><i style="height:47%"/><i style="height:70%"/><i style="height:62%"/><i style="height:80%"/><i style="height:74%"/>
|
||||||
|
</div>
|
||||||
|
<div class="mc-donut"><div class="ring"><b>92%</b></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption">Illustrative preview — not live data</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= MODULES ================= -->
|
||||||
|
<div class="mc-section">
|
||||||
|
<div class="mc-shell">
|
||||||
|
<div class="mc-kicker">What's included</div>
|
||||||
|
<h2>Nine areas, one shared record of the school</h2>
|
||||||
|
<p class="sub">Every area below reads from and writes to the same student record — an admission becomes a student, an enrollment decides their fees, class, timetable and what their family can see. Nothing gets re-entered between them.</p>
|
||||||
|
|
||||||
|
<div class="mc-modgrid">
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#EAF1FB;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#3B82C4" stroke-width="1.8"><path d="M4 19V7a2 2 0 0 1 2-2h5l2 2h7v10a2 2 0 0 1-2 2H4"/></svg></div>
|
||||||
|
<h3>Student & Academic Records</h3>
|
||||||
|
<ul><li>Academic calendar, programs & batches</li><li>Student and guardian profiles</li><li>Enrollment is the single source of truth</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#EAF6EF;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#2E9E6D" stroke-width="1.8"><path d="M12 3l8 4-8 4-8-4 8-4z"/><path d="M4 11v5c0 1.7 3.6 4 8 4s8-2.3 8-4v-5"/></svg></div>
|
||||||
|
<h3>Admissions</h3>
|
||||||
|
<ul><li>Public application form</li><li>Applied → Interview → Enrolled pipeline</li><li>One-click convert, zero re-typing</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#FBF0E3;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#E8823C" stroke-width="1.8"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M8 3v4"/></svg></div>
|
||||||
|
<h3>Fees & Payments</h3>
|
||||||
|
<ul><li>Per-grade fee structures, billed by term</li><li>Concessions shown as a visible discount</li><li>Online payment, instant receipt</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#F4EEFB;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#4B3F92" stroke-width="1.8"><path d="M9 11l2 2 4-4"/><rect x="3" y="4" width="18" height="17" rx="2"/></svg></div>
|
||||||
|
<h3>Attendance</h3>
|
||||||
|
<ul><li>Class roster, defaulted Present</li><li>Built for a phone, one-handed</li><li>Teachers see only their own batch</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#E9F1FB;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#3B82C4" stroke-width="1.8"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></svg></div>
|
||||||
|
<h3>Timetable</h3>
|
||||||
|
<ul><li>One dataset, three viewpoints</li><li>Student, teacher and admin views</li><li>No teacher or room double-booked</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#FBF0E3;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#C9962E" stroke-width="1.8"><path d="M6 3h9l5 5v13H6z"/><path d="M9 12h6M9 16h6"/></svg></div>
|
||||||
|
<h3>Examinations & Report Cards</h3>
|
||||||
|
<ul><li>Grading scales as configuration</li><li>Whole-class grid mark entry</li><li>Print-ready A4 report cards</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#EAF6EF;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#2E9E6D" stroke-width="1.8"><path d="M17 20v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1"/><circle cx="9" cy="7" r="3.2"/></svg></div>
|
||||||
|
<h3>Parent & Student Portal</h3>
|
||||||
|
<ul><li>Secure family login outside staff system</li><li>Multi-child switcher</li><li>Access checked on every request</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#F4EEFB;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#4B3F92" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M10 9l5 3-5 3z"/></svg></div>
|
||||||
|
<h3>Online Learning</h3>
|
||||||
|
<ul><li>Video, handouts and quizzes per class</li><li>Auto-enrolled with the class</li><li>Scores saved instantly</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="mc-mod">
|
||||||
|
<div class="ico" style="background:#EAF1FB;"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#3B82C4" stroke-width="1.8"><path d="M12 2l2.5 5.5L20 9l-4.5 3.5L17 18l-5-3-5 3 1.5-5.5L4 9l5.5-1.5z"/></svg></div>
|
||||||
|
<h3>School Branding</h3>
|
||||||
|
<ul><li>Logo, colours and name, per school</li><li>Applied across portal and reports</li><li>Rebrand to a new school in minutes</li></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= IMPACT ================= -->
|
||||||
|
<div class="mc-impact">
|
||||||
|
<div class="mc-shell">
|
||||||
|
<div class="mc-kicker" style="color:#B9AEEF;">Why it matters</div>
|
||||||
|
<h2 style="color:#fff; max-width:28ch;">Impact that shows up in the school's own numbers</h2>
|
||||||
|
<div class="mc-impact-grid" style="margin-top:30px;">
|
||||||
|
<div class="mc-impact-item">
|
||||||
|
<span class="ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/></svg></span>
|
||||||
|
<div><h4>Less administrative work</h4><p>Automate the re-typing between admissions, fees and records.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="mc-impact-item">
|
||||||
|
<span class="ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M3 17l6-6 4 4 8-8"/></svg></span>
|
||||||
|
<div><h4>Faster fee collection</h4><p>Online payment posts a real accounting entry, no manual refresh.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="mc-impact-item">
|
||||||
|
<span class="ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M4 19V10M12 19V5M20 19v-7"/></svg></span>
|
||||||
|
<div><h4>Real-time visibility</h4><p>Attendance, fees and results update the moment they happen.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="mc-impact-item">
|
||||||
|
<span class="ico"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M17 20v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1"/><circle cx="9" cy="7" r="3.2"/></svg></span>
|
||||||
|
<div><h4>Better family experience</h4><p>No phone call, no note in a diary — parents see it themselves.</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= CTA / CONTACT ================= -->
|
||||||
|
<div class="mc-cta-band">
|
||||||
|
<div class="mc-shell">
|
||||||
|
<div class="mc-cta-inner">
|
||||||
|
<div>
|
||||||
|
<h3>Ready to see it on your own campus?</h3>
|
||||||
|
<p>We'll walk your team through admissions, fees, attendance, exams and the parent portal on a live system — not slides.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mc-contact-row">
|
||||||
|
<span class="mc-contact-item"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3.1-8.6A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .3 2 .7 3a2 2 0 0 1-.5 2.1L8 10a16 16 0 0 0 6 6l1.2-1.3a2 2 0 0 1 2.1-.5c1 .4 2 .6 3 .7a2 2 0 0 1 1.7 2z"/></svg> +1 647‑679‑7651</span>
|
||||||
|
<span class="mc-contact-item"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg> info@metatroncubesolutions.com</span>
|
||||||
|
<span class="mc-contact-item"><svg class="mc-icon" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.7"><path d="M12 21s7-6.3 7-11.5A7 7 0 0 0 5 9.5C5 14.7 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.3"/></svg> Waterloo, Ontario, Canada</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mc-footnote">Metatroncube Solutions Inc. · Empowering Institutions. Enriching Education. · Dashboard preview shown is illustrative; live figures are computed from the school's own records.</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
@ -11,4 +11,22 @@
|
|||||||
<xpath expr="//div[hasclass('d-none') and hasclass('d-lg-block') and hasclass('text-center')]"
|
<xpath expr="//div[hasclass('d-none') and hasclass('d-lg-block') and hasclass('text-center')]"
|
||||||
position="replace"/>
|
position="replace"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- A second, separate leak found while building the marketing
|
||||||
|
homepage: web.frontend_layout's own footer - the base layout
|
||||||
|
portal.frontend_layout and website.layout both build on - calls
|
||||||
|
web.brand_promotion directly, putting its own "Powered by
|
||||||
|
Odoo" badge (a different one from portal_record_sidebar above,
|
||||||
|
different markup, different template) in the footer of every
|
||||||
|
single website and portal page, admission form included. It
|
||||||
|
was live in the demo this whole time; only surfaced now because
|
||||||
|
a fresh page made it visible in a render check. Neutralizing
|
||||||
|
the shared, reusable template rather than website.layout's
|
||||||
|
footer specifically means every current and future caller of
|
||||||
|
web.brand_promotion is covered by this one fix, not just the
|
||||||
|
page that happened to expose it. -->
|
||||||
|
<template id="brand_promotion_no_odoo_branding"
|
||||||
|
inherit_id="web.brand_promotion">
|
||||||
|
<xpath expr="//div[hasclass('o_brand_promotion')]" position="replace"/>
|
||||||
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user