diff --git a/.gitattributes b/.gitattributes
index 21f6efb..91d5d9d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,7 @@
*.sh text eol=lf
*.conf text eol=lf
+*.pdf binary
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
diff --git a/addons/mc_education_lms/__init__.py b/addons/mc_education_lms/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/addons/mc_education_lms/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons/mc_education_lms/__manifest__.py b/addons/mc_education_lms/__manifest__.py
new file mode 100644
index 0000000..d414a81
--- /dev/null
+++ b/addons/mc_education_lms/__manifest__.py
@@ -0,0 +1,17 @@
+{
+ "name": "School ERP - LMS",
+ "version": "19.0.1.0.0",
+ "category": "Education",
+ "summary": "Thin glue linking a batch to a website_slides course channel.",
+ "author": "Metatroncube Software Solutions LLP",
+ "license": "Other proprietary",
+ "depends": ["mc_education_base", "website_slides"],
+ "data": [
+ "views/mc_batch_views.xml",
+ ],
+ "demo": [
+ "demo/mc_lms_demo.xml",
+ ],
+ "installable": True,
+ "application": False,
+}
diff --git a/addons/mc_education_lms/demo/mc_lms_demo.xml b/addons/mc_education_lms/demo/mc_lms_demo.xml
new file mode 100644
index 0000000..d3e3e0f
--- /dev/null
+++ b/addons/mc_education_lms/demo/mc_lms_demo.xml
@@ -0,0 +1,129 @@
+
+
+
+
+ Mathematics - Algebra Basics
+ invite
+
+
+
+
+
+
+
+ Algebra Basics - Video Lesson
+
+ video
+ https://www.youtube.com/watch?v=dQw4w9WgXcQ
+ True
+ 1
+
+
+
+ Algebra Basics - Handout
+
+ document
+
+
+ True
+ 2
+
+
+
+ Algebra Basics - Quiz
+
+ quiz
+ True
+ 3
+
+
+
+
+ 1
+ What is the value of x in x + 5 = 12?
+
+
+
+ 7
+ True
+
+
+
+ 17
+
+
+
+
+ 2
+ Simplify: 3x + 2x
+
+
+
+ 5x
+ True
+
+
+
+ 6x
+
+
+
+
+ 3
+ What is the coefficient of x in 4x + 3?
+
+
+
+ 4
+ True
+
+
+
+ 3
+
+
+
+
+ 4
+ Solve for y: 2y = 10
+
+
+
+ 5
+ True
+
+
+
+ 20
+
+
+
+
+ 5
+ Which of these is a linear equation?
+
+
+
+ y = 2x + 1
+ True
+
+
+
+ y = x^2
+
+
diff --git a/addons/mc_education_lms/models/__init__.py b/addons/mc_education_lms/models/__init__.py
new file mode 100644
index 0000000..b8133f5
--- /dev/null
+++ b/addons/mc_education_lms/models/__init__.py
@@ -0,0 +1,2 @@
+from . import mc_batch
+from . import mc_enrollment
diff --git a/addons/mc_education_lms/models/mc_batch.py b/addons/mc_education_lms/models/mc_batch.py
new file mode 100644
index 0000000..84870db
--- /dev/null
+++ b/addons/mc_education_lms/models/mc_batch.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+
+
+class McBatch(models.Model):
+ _inherit = "mc.batch"
+
+ channel_id = fields.Many2one(
+ "slide.channel", string="Course Channel", ondelete="set null",
+ help="Enrolling a student in this batch enrolls them in this course channel.",
+ )
diff --git a/addons/mc_education_lms/models/mc_enrollment.py b/addons/mc_education_lms/models/mc_enrollment.py
new file mode 100644
index 0000000..d26168f
--- /dev/null
+++ b/addons/mc_education_lms/models/mc_enrollment.py
@@ -0,0 +1,27 @@
+from odoo import api, models
+
+
+class McEnrollment(models.Model):
+ _inherit = "mc.enrollment"
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ enrollments = super().create(vals_list)
+ enrollments.filtered(lambda e: e.state == "active")._sync_lms_channel_membership()
+ return enrollments
+
+ def write(self, vals):
+ res = super().write(vals)
+ if "state" in vals or "batch_id" in vals:
+ self.filtered(lambda e: e.state == "active")._sync_lms_channel_membership()
+ return res
+
+ def _sync_lms_channel_membership(self):
+ # Enrolling in the batch is what enrolls the student in its
+ # course channel - the whole point of this module (O8 spec:
+ # "link a channel to a mc.batch so enrolled students see their
+ # courses"). Stock website_slides API, nothing custom.
+ for enrollment in self:
+ channel = enrollment.batch_id.channel_id
+ if channel:
+ channel._action_add_members(enrollment.student_id.partner_id)
diff --git a/addons/mc_education_lms/static/demo/algebra_handout.pdf b/addons/mc_education_lms/static/demo/algebra_handout.pdf
new file mode 100644
index 0000000..d0df64d
Binary files /dev/null and b/addons/mc_education_lms/static/demo/algebra_handout.pdf differ
diff --git a/addons/mc_education_lms/tests/__init__.py b/addons/mc_education_lms/tests/__init__.py
new file mode 100644
index 0000000..d0b38d2
--- /dev/null
+++ b/addons/mc_education_lms/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_lms
diff --git a/addons/mc_education_lms/tests/test_lms.py b/addons/mc_education_lms/tests/test_lms.py
new file mode 100644
index 0000000..bedbe4e
--- /dev/null
+++ b/addons/mc_education_lms/tests/test_lms.py
@@ -0,0 +1,67 @@
+from odoo.tests.common import TransactionCase
+
+
+class TestLms(TransactionCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.year = cls.env["mc.academic.year"].create({
+ "name": "TEST-LMS-2026-27",
+ "date_start": "2026-06-01", "date_end": "2027-04-30",
+ })
+ cls.program = cls.env["mc.program"].create({
+ "name": "TEST LMS Program", "code": "TEST-LMS-P1", "sequence_no": 1,
+ "display_label": "Test Grade",
+ })
+ cls.channel = cls.env["slide.channel"].create({"name": "TEST LMS Channel"})
+ cls.batch = cls.env["mc.batch"].create({
+ "name": "TEST LMS Batch", "program_id": cls.program.id, "year_id": cls.year.id,
+ "channel_id": cls.channel.id,
+ })
+ partner = cls.env["res.partner"].create({"name": "LMS Test Student"})
+ cls.student = cls.env["mc.student"].create({
+ "partner_id": partner.id, "name": "LMS Test Student",
+ })
+
+ def test_active_enrollment_joins_the_batchs_channel(self):
+ self.env["mc.enrollment"].create({
+ "student_id": self.student.id, "program_id": self.program.id,
+ "batch_id": self.batch.id, "year_id": self.year.id, "state": "active",
+ })
+ member = self.env["slide.channel.partner"].search([
+ ("channel_id", "=", self.channel.id), ("partner_id", "=", self.student.partner_id.id),
+ ])
+ self.assertTrue(member)
+ self.assertEqual(member.member_status, "joined")
+
+ def test_draft_enrollment_does_not_join_the_channel(self):
+ self.env["mc.enrollment"].create({
+ "student_id": self.student.id, "program_id": self.program.id,
+ "batch_id": self.batch.id, "year_id": self.year.id, "state": "draft",
+ })
+ member = self.env["slide.channel.partner"].search([
+ ("channel_id", "=", self.channel.id), ("partner_id", "=", self.student.partner_id.id),
+ ])
+ self.assertFalse(member)
+
+ def test_activating_a_draft_enrollment_joins_the_channel(self):
+ enrollment = self.env["mc.enrollment"].create({
+ "student_id": self.student.id, "program_id": self.program.id,
+ "batch_id": self.batch.id, "year_id": self.year.id, "state": "draft",
+ })
+ enrollment.write({"state": "active"})
+ member = self.env["slide.channel.partner"].search([
+ ("channel_id", "=", self.channel.id), ("partner_id", "=", self.student.partner_id.id),
+ ])
+ self.assertTrue(member)
+
+ def test_batch_without_channel_does_not_error(self):
+ batch_no_channel = self.env["mc.batch"].create({
+ "name": "TEST LMS Batch No Channel", "program_id": self.program.id, "year_id": self.year.id,
+ })
+ # Should not raise.
+ self.env["mc.enrollment"].create({
+ "student_id": self.student.id, "program_id": self.program.id,
+ "batch_id": batch_no_channel.id, "year_id": self.year.id, "state": "active",
+ })
diff --git a/addons/mc_education_lms/views/mc_batch_views.xml b/addons/mc_education_lms/views/mc_batch_views.xml
new file mode 100644
index 0000000..9a6c646
--- /dev/null
+++ b/addons/mc_education_lms/views/mc_batch_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ mc.batch.form.inherit.mc.education.lms
+ mc.batch
+
+
+
+
+
+
+
+