The bridge pattern
The single most important architectural idea in EduPrime: apps never import each other directly. Instead, a tiny bridge module joins two apps, and the base app exposes no-op stubs that the bridge fills in. This is what lets you install any subset of the suite and still get full integration.
The problem it solves
If ep_transport imported ep_fees directly, you could never install Transport without Fees, and a fee-model change would break Transport. EduPrime avoids this entirely.
How a bridge works (real example)
Take billing a bus assignment. In the base ep_transport module, the assignment model has no-op stub methods:
# ep_transport (base) — stub, does nothing on its own
def _create_fee_line(self):
return # real logic only exists if the fees bridge is installedInstall ep_fees_transport_bridge (which depends on both ep_transport and ep_fees, and is auto_install: True) and it overrides the stub with the real implementation and contributes the linking field:
# ep_fees_transport_bridge — real implementation
class EpTransportAssignment(models.Model):
_inherit = "ep.transport.assignment"
fee_assignment_line_id = fields.Many2one("ep.fee.assignment.line")
def _create_fee_line(self): # overrides the stub
if self.staff_commuter: return # staff ride free
fa = self.env["ep.fee.assignment"].search([...])
line = self.env["ep.fee.assignment.line"].create({
"category_id": self.env.ref("ep_fees.fee_cat_transport").id,
"amount": self.effective_fee, "transport_assignment_id": self.id})
self.fee_assignment_line_id = line.id
fa._compute_totals()When it fires
The assignment's own lifecycle buttons call the hook: action_activate → _create_fee_line(), and action_pause / action_cancel → _remove_fee_line(). With no bridge installed those calls hit the no-op stubs and nothing happens — Transport runs standalone.
Every bridge in the suite
| Bridge module | Joins | Source → target object | Trigger |
|---|---|---|---|
ep_fees_transport_bridge | Transport ↔ Fees | ep.transport.assignment → ep.fee.assignment.line | activate/pause/cancel |
ep_fees_hostel_bridge | Hostel ↔ Fees | ep.hostel.allocation → ep.fee.assignment.line | allocation |
ep_fees_meals_bridge | Meals ↔ Fees | ep.meals.subscription → ep.fee.assignment.line | subscribe |
ep_fees_library_bridge | Library ↔ Fees | overdue fine → ep.fee.assignment.line | fine raised |
ep_academic_library_bridge | Library ↔ Academic | ep.library.course.reserve → class reading lists | reserve created |
ep_assessment_gradebook_bridge | Exams ↔ Gradebook | ep.assessment.attempt → ep.gradebook.entry | attempt graded |
auto_install: True, so the moment both apps it joins are installed, the bridge installs itself. You never tick a box; integration just appears.
