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>
This commit is contained in:
parent
4a2e7b7016
commit
508bf27417
3
addons/mc_education_admission/__init__.py
Normal file
3
addons/mc_education_admission/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from . import models
|
||||||
|
from . import wizards
|
||||||
|
from . import controllers
|
||||||
23
addons/mc_education_admission/__manifest__.py
Normal file
23
addons/mc_education_admission/__manifest__.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "School ERP - Admission",
|
||||||
|
"version": "19.0.1.0.0",
|
||||||
|
"category": "Education",
|
||||||
|
"summary": "Public admission form through to an enrolled student, zero re-typing.",
|
||||||
|
"author": "Metatroncube Software Solutions LLP",
|
||||||
|
"license": "Other proprietary",
|
||||||
|
"depends": ["mc_education_base", "website"],
|
||||||
|
"data": [
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"data/mc_applicant_sequence_data.xml",
|
||||||
|
"data/mc_applicant_website_form_data.xml",
|
||||||
|
"views/mc_applicant_views.xml",
|
||||||
|
"views/mc_applicant_convert_wizard_views.xml",
|
||||||
|
"views/website_admission_templates.xml",
|
||||||
|
"views/website_admission_menu.xml",
|
||||||
|
],
|
||||||
|
"demo": [
|
||||||
|
"demo/mc_applicant_demo.xml",
|
||||||
|
],
|
||||||
|
"installable": True,
|
||||||
|
"application": False,
|
||||||
|
}
|
||||||
4
addons/mc_education_admission/controllers/__init__.py
Normal file
4
addons/mc_education_admission/controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# No custom controller here on purpose: the public admission form uses
|
||||||
|
# Odoo's stock /website/form/<model> mechanism (see
|
||||||
|
# data/mc_applicant_website_form_data.xml), not a reimplementation of it.
|
||||||
|
# CLAUDE.md sec 1.3: writing a custom version of stock infrastructure is a bug.
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="seq_mc_applicant_application_no" model="ir.sequence">
|
||||||
|
<field name="name">Applicant Application Number</field>
|
||||||
|
<field name="code">mc.applicant.application_no</field>
|
||||||
|
<field name="prefix">APP%(year)s</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!--
|
||||||
|
Security-critical: this is the ONLY thing that decides which
|
||||||
|
mc.applicant fields a fully anonymous website visitor can set.
|
||||||
|
Odoo's generic /website/form/<model> controller creates the record
|
||||||
|
as SUPERUSER, so field-level access rights do not apply here at
|
||||||
|
all - the sole guard is website_form_blacklisted (default True on
|
||||||
|
every field of every model since Odoo 15) plus website_form_access
|
||||||
|
on the model itself. Never add application_no, state, student_id
|
||||||
|
or company_id to this list: those must only ever be set by
|
||||||
|
server-side code (see models/mc_applicant.py, wizards/).
|
||||||
|
Pattern copied verbatim from Odoo core's own
|
||||||
|
website_hr_recruitment/data/config_data.xml.
|
||||||
|
-->
|
||||||
|
<record id="model_mc_applicant" model="ir.model">
|
||||||
|
<field name="website_form_key">apply_admission</field>
|
||||||
|
<field name="website_form_access">True</field>
|
||||||
|
<field name="website_form_label">Apply for Admission</field>
|
||||||
|
</record>
|
||||||
|
<function model="ir.model.fields" name="formbuilder_whitelist">
|
||||||
|
<value>mc.applicant</value>
|
||||||
|
<value eval="[
|
||||||
|
'name',
|
||||||
|
'dob',
|
||||||
|
'program_id',
|
||||||
|
'guardian_name',
|
||||||
|
'guardian_relationship',
|
||||||
|
'guardian_phone',
|
||||||
|
'guardian_email',
|
||||||
|
]"/>
|
||||||
|
</function>
|
||||||
|
</odoo>
|
||||||
37
addons/mc_education_admission/demo/mc_applicant_demo.xml
Normal file
37
addons/mc_education_admission/demo/mc_applicant_demo.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Populates the pipeline kanban so it reads as a real pipeline, not
|
||||||
|
an empty board, without pre-empting the live Scene 2 walkthrough
|
||||||
|
(Applied -> ... -> Enroll), which starts from a fresh public-form
|
||||||
|
submission during the demo itself. -->
|
||||||
|
<record id="demo_applicant_kavya_reddy" model="mc.applicant">
|
||||||
|
<field name="name">Kavya Reddy</field>
|
||||||
|
<field name="dob">2018-03-14</field>
|
||||||
|
<field name="program_id" ref="mc_education_base.demo_program_grade5"/>
|
||||||
|
<field name="guardian_name">Suresh Reddy</field>
|
||||||
|
<field name="guardian_relationship">father</field>
|
||||||
|
<field name="guardian_phone">+91 98450 11223</field>
|
||||||
|
<field name="guardian_email">suresh.reddy@example.com</field>
|
||||||
|
<field name="state">document_verification</field>
|
||||||
|
</record>
|
||||||
|
<record id="demo_applicant_ishaan_malhotra" model="mc.applicant">
|
||||||
|
<field name="name">Ishaan Malhotra</field>
|
||||||
|
<field name="dob">2015-07-22</field>
|
||||||
|
<field name="program_id" ref="mc_education_base.demo_program_grade8"/>
|
||||||
|
<field name="guardian_name">Priya Malhotra</field>
|
||||||
|
<field name="guardian_relationship">mother</field>
|
||||||
|
<field name="guardian_phone">+91 99000 44556</field>
|
||||||
|
<field name="guardian_email">priya.malhotra@example.com</field>
|
||||||
|
<field name="state">interview</field>
|
||||||
|
</record>
|
||||||
|
<record id="demo_applicant_emma_tremblay" model="mc.applicant">
|
||||||
|
<field name="name">Emma Tremblay</field>
|
||||||
|
<field name="dob">2018-11-02</field>
|
||||||
|
<field name="program_id" ref="mc_education_base.demo_program_grade5"/>
|
||||||
|
<field name="guardian_name">Marc Tremblay</field>
|
||||||
|
<field name="guardian_relationship">father</field>
|
||||||
|
<field name="guardian_phone">+1 519-555-0148</field>
|
||||||
|
<field name="guardian_email">marc.tremblay@example.com</field>
|
||||||
|
<field name="state">offered</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
2
addons/mc_education_admission/models/__init__.py
Normal file
2
addons/mc_education_admission/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import mc_applicant
|
||||||
|
from . import mc_student
|
||||||
98
addons/mc_education_admission/models/mc_applicant.py
Normal file
98
addons/mc_education_admission/models/mc_applicant.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class McApplicant(models.Model):
|
||||||
|
_name = "mc.applicant"
|
||||||
|
_inherit = ["mail.thread"]
|
||||||
|
_description = "Applicant"
|
||||||
|
_order = "create_date desc"
|
||||||
|
_rec_name = "name"
|
||||||
|
|
||||||
|
name = fields.Char(string="Applicant Name", required=True, tracking=True)
|
||||||
|
dob = fields.Date(string="Date of Birth")
|
||||||
|
program_id = fields.Many2one(
|
||||||
|
"mc.program", string="Grade Applied For", required=True, tracking=True,
|
||||||
|
)
|
||||||
|
guardian_name = fields.Char(string="Guardian Name", required=True)
|
||||||
|
guardian_relationship = fields.Selection(
|
||||||
|
[
|
||||||
|
("father", "Father"),
|
||||||
|
("mother", "Mother"),
|
||||||
|
("legal_guardian", "Legal Guardian"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
string="Guardian Relationship", default="legal_guardian",
|
||||||
|
)
|
||||||
|
guardian_phone = fields.Char(string="Guardian Phone")
|
||||||
|
guardian_email = fields.Char(string="Guardian Email", required=True)
|
||||||
|
|
||||||
|
application_no = fields.Char(string="Application No.", copy=False, tracking=True)
|
||||||
|
state = fields.Selection(
|
||||||
|
[
|
||||||
|
("applied", "Applied"),
|
||||||
|
("document_verification", "Document Verification"),
|
||||||
|
("interview", "Interview"),
|
||||||
|
("offered", "Offered"),
|
||||||
|
("accepted", "Accepted"),
|
||||||
|
("enrolled", "Enrolled"),
|
||||||
|
("rejected", "Rejected"),
|
||||||
|
],
|
||||||
|
string="Stage", default="applied", required=True, tracking=True, group_expand="_read_group_state",
|
||||||
|
)
|
||||||
|
student_id = fields.Many2one(
|
||||||
|
"mc.student", string="Student", readonly=True, copy=False,
|
||||||
|
help="Set once this applicant has been converted. The application number persists onto the student record.",
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
"res.company", string="Company", required=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
)
|
||||||
|
|
||||||
|
_application_no_uniq = models.Constraint(
|
||||||
|
"unique(application_no)",
|
||||||
|
"An applicant with this application number already exists.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _read_group_state(self, states, domain):
|
||||||
|
# Keeps every kanban column visible even when empty, so the pipeline
|
||||||
|
# always reads as a pipeline rather than losing stages that have
|
||||||
|
# nothing in them right now.
|
||||||
|
return [key for key, _label in self._fields["state"].selection]
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if not vals.get("application_no"):
|
||||||
|
vals["application_no"] = self.env["ir.sequence"].next_by_code(
|
||||||
|
"mc.applicant.application_no"
|
||||||
|
)
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def action_set_document_verification(self):
|
||||||
|
self.write({"state": "document_verification"})
|
||||||
|
|
||||||
|
def action_set_interview(self):
|
||||||
|
self.write({"state": "interview"})
|
||||||
|
|
||||||
|
def action_set_offered(self):
|
||||||
|
self.write({"state": "offered"})
|
||||||
|
|
||||||
|
def action_set_accepted(self):
|
||||||
|
self.write({"state": "accepted"})
|
||||||
|
|
||||||
|
def action_reject(self):
|
||||||
|
self.write({"state": "rejected"})
|
||||||
|
|
||||||
|
def action_open_convert_wizard(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "mc.applicant.convert.wizard",
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
"context": {
|
||||||
|
"default_applicant_id": self.id,
|
||||||
|
"default_program_id": self.program_id.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
11
addons/mc_education_admission/models/mc_student.py
Normal file
11
addons/mc_education_admission/models/mc_student.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class McStudent(models.Model):
|
||||||
|
_inherit = "mc.student"
|
||||||
|
|
||||||
|
application_no = fields.Char(
|
||||||
|
string="Application No.", copy=False, readonly=True,
|
||||||
|
help="The application number from the admission pipeline, preserved on "
|
||||||
|
"the student record so nothing about the original application is lost.",
|
||||||
|
)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_mc_applicant_administrator,mc.applicant.administrator,model_mc_applicant,mc_education_base.group_school_administrator,1,1,1,1
|
||||||
|
access_mc_applicant_staff,mc.applicant.staff,model_mc_applicant,mc_education_base.group_school_staff,1,1,1,0
|
||||||
|
access_mc_applicant_convert_wizard_administrator,mc.applicant.convert.wizard.administrator,model_mc_applicant_convert_wizard,mc_education_base.group_school_administrator,1,1,1,1
|
||||||
|
access_mc_applicant_convert_wizard_staff,mc.applicant.convert.wizard.staff,model_mc_applicant_convert_wizard,mc_education_base.group_school_staff,1,1,1,1
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_mc_applicant_convert_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">mc.applicant.convert.wizard.form</field>
|
||||||
|
<field name="model">mc.applicant.convert.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Enroll Applicant">
|
||||||
|
<group>
|
||||||
|
<field name="applicant_id" invisible="1"/>
|
||||||
|
<field name="program_id"/>
|
||||||
|
<field name="year_id"/>
|
||||||
|
<field name="batch_id"/>
|
||||||
|
<field name="roll_no"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="action_convert" string="Enroll" type="object" class="btn-primary"/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
100
addons/mc_education_admission/views/mc_applicant_views.xml
Normal file
100
addons/mc_education_admission/views/mc_applicant_views.xml
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_mc_applicant_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">mc.applicant.kanban</field>
|
||||||
|
<field name="model">mc.applicant</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban default_group_by="state" records_draggable="1">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="program_id"/>
|
||||||
|
<field name="guardian_name"/>
|
||||||
|
<field name="application_no"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="oe_kanban_card">
|
||||||
|
<strong><field name="name"/></strong>
|
||||||
|
<div><field name="program_id"/></div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<field name="guardian_name"/>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<field name="application_no"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_mc_applicant_list" model="ir.ui.view">
|
||||||
|
<field name="name">mc.applicant.list</field>
|
||||||
|
<field name="model">mc.applicant</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Applicants">
|
||||||
|
<field name="application_no"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="program_id"/>
|
||||||
|
<field name="guardian_name"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_mc_applicant_form" model="ir.ui.view">
|
||||||
|
<field name="name">mc.applicant.form</field>
|
||||||
|
<field name="model">mc.applicant</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Applicant">
|
||||||
|
<header>
|
||||||
|
<button name="action_set_document_verification" string="Move to Document Verification"
|
||||||
|
type="object" class="oe_highlight" invisible="state != 'applied'"/>
|
||||||
|
<button name="action_set_interview" string="Move to Interview"
|
||||||
|
type="object" class="oe_highlight" invisible="state != 'document_verification'"/>
|
||||||
|
<button name="action_set_offered" string="Make Offer"
|
||||||
|
type="object" class="oe_highlight" invisible="state != 'interview'"/>
|
||||||
|
<button name="action_set_accepted" string="Mark Accepted"
|
||||||
|
type="object" class="oe_highlight" invisible="state != 'offered'"/>
|
||||||
|
<button name="action_open_convert_wizard" string="Enroll"
|
||||||
|
type="object" class="oe_highlight" invisible="state != 'accepted'"/>
|
||||||
|
<button name="action_reject" string="Reject"
|
||||||
|
type="object" invisible="state in ('enrolled', 'rejected')"/>
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="applied,document_verification,interview,offered,accepted,enrolled"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name"/>
|
||||||
|
<h1><field name="name"/></h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="application_no" readonly="1"/>
|
||||||
|
<field name="dob"/>
|
||||||
|
<field name="program_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="guardian_name"/>
|
||||||
|
<field name="guardian_relationship"/>
|
||||||
|
<field name="guardian_phone"/>
|
||||||
|
<field name="guardian_email"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<field name="student_id" invisible="not student_id" readonly="1"/>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_mc_applicant" model="ir.actions.act_window">
|
||||||
|
<field name="name">Applicants</field>
|
||||||
|
<field name="res_model">mc.applicant</field>
|
||||||
|
<field name="view_mode">kanban,list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_mc_applicant" name="Admissions"
|
||||||
|
parent="mc_education_base.menu_school_root"
|
||||||
|
action="action_mc_applicant" sequence="5"/>
|
||||||
|
</odoo>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="website_menu_admission_apply" model="website.menu">
|
||||||
|
<field name="name">Admissions</field>
|
||||||
|
<field name="url">/admissions/apply</field>
|
||||||
|
<field name="parent_id" ref="website.main_menu"/>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -0,0 +1,146 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!--
|
||||||
|
Hand-written to match the DOM structure/classes of Odoo core's own
|
||||||
|
s_website_form snippet (website/views/snippets/s_website_form.xml)
|
||||||
|
exactly, so the stock frontend widget (form.js, loaded by every
|
||||||
|
website page via website.layout) picks it up automatically - no
|
||||||
|
custom JS, no custom submit controller. Posting still goes through
|
||||||
|
Odoo's generic /website/form/<model> controller; see
|
||||||
|
data/mc_applicant_website_form_data.xml for the field whitelist
|
||||||
|
that is the actual security boundary on that path.
|
||||||
|
-->
|
||||||
|
<template id="admission_apply_page" name="Admission Application">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div id="wrap">
|
||||||
|
<section class="s_website_form pt32 pb32" data-snippet="s_website_form">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<h1 class="mb-4">Apply for Admission</h1>
|
||||||
|
<form action="/website/form/" method="post" enctype="multipart/form-data"
|
||||||
|
class="o_mark_required" data-mark="*"
|
||||||
|
data-model_name="mc.applicant"
|
||||||
|
data-success-mode="message">
|
||||||
|
<div class="s_website_form_rows row s_col_no_bgcolor">
|
||||||
|
|
||||||
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="char">
|
||||||
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
||||||
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_name">
|
||||||
|
<span class="s_website_form_label_content">Student Name</span>
|
||||||
|
<span class="s_website_form_mark"> *</span>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input class="form-control s_website_form_input" type="text"
|
||||||
|
name="name" id="admission_name" required="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-name="Field" class="s_website_form_field mb-3 col-12" data-type="date">
|
||||||
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
||||||
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_dob">
|
||||||
|
<span class="s_website_form_label_content">Date of Birth</span>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input class="form-control s_website_form_input" type="date"
|
||||||
|
name="dob" id="admission_dob"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="many2one">
|
||||||
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
||||||
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_program_id">
|
||||||
|
<span class="s_website_form_label_content">Grade Applied For</span>
|
||||||
|
<span class="s_website_form_mark"> *</span>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select class="form-select s_website_form_input"
|
||||||
|
name="program_id" id="admission_program_id" required="1">
|
||||||
|
<option value="">Select a grade...</option>
|
||||||
|
<!-- Program display labels are public information (a
|
||||||
|
school's grade list is meant to be seen by anyone
|
||||||
|
filling this form) - sudo() only reads name/id for
|
||||||
|
this dropdown, nothing is written here. -->
|
||||||
|
<t t-foreach="request.env['mc.program'].sudo().search([], order='sequence_no')" t-as="program">
|
||||||
|
<option t-att-value="program.id" t-esc="program.display_label"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="char">
|
||||||
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
||||||
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_guardian_name">
|
||||||
|
<span class="s_website_form_label_content">Parent / Guardian Name</span>
|
||||||
|
<span class="s_website_form_mark"> *</span>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input class="form-control s_website_form_input" type="text"
|
||||||
|
name="guardian_name" id="admission_guardian_name" required="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="tel">
|
||||||
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
||||||
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_guardian_phone">
|
||||||
|
<span class="s_website_form_label_content">Parent / Guardian Phone</span>
|
||||||
|
<span class="s_website_form_mark"> *</span>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input class="form-control s_website_form_input" type="tel"
|
||||||
|
name="guardian_phone" id="admission_guardian_phone" required="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_required" data-type="email">
|
||||||
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
||||||
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_guardian_email">
|
||||||
|
<span class="s_website_form_label_content">Parent / Guardian Email</span>
|
||||||
|
<span class="s_website_form_mark"> *</span>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input class="form-control s_website_form_input" type="email"
|
||||||
|
name="guardian_email" id="admission_guardian_email" required="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-name="Field" class="s_website_form_field mb-3 col-12" data-type="binary">
|
||||||
|
<div class="row s_col_no_resize s_col_no_bgcolor">
|
||||||
|
<label class="col-form-label col-sm-3 s_website_form_label" for="admission_birth_certificate">
|
||||||
|
<span class="s_website_form_label_content">Birth Certificate</span>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<!-- "birth_certificate" matches no real field on mc.applicant,
|
||||||
|
so Odoo's generic form controller stores it as a plain
|
||||||
|
ir.attachment linked to the new record - exactly the
|
||||||
|
"file uploads to ir.attachment" the spec asks for,
|
||||||
|
with no dedicated Binary field needed on the model. -->
|
||||||
|
<input class="form-control s_website_form_input" type="file"
|
||||||
|
name="birth_certificate" id="admission_birth_certificate"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-0 py-2 col-12 s_website_form_submit text-end s_website_form_no_submit_label" data-name="Submit Button">
|
||||||
|
<div class="s_website_form_label"/>
|
||||||
|
<span id="s_website_form_result"/>
|
||||||
|
<a href="#" role="button" class="btn btn-primary s_website_form_send">Submit Application</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<record id="website_page_admission_apply" model="website.page">
|
||||||
|
<field name="url">/admissions/apply</field>
|
||||||
|
<field name="view_id" ref="admission_apply_page"/>
|
||||||
|
<field name="is_published">True</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1
addons/mc_education_admission/wizards/__init__.py
Normal file
1
addons/mc_education_admission/wizards/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import mc_applicant_convert_wizard
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class McApplicantConvertWizard(models.TransientModel):
|
||||||
|
_name = "mc.applicant.convert.wizard"
|
||||||
|
_description = "Convert Applicant to Student"
|
||||||
|
|
||||||
|
applicant_id = fields.Many2one(
|
||||||
|
"mc.applicant", string="Applicant", required=True, readonly=True,
|
||||||
|
)
|
||||||
|
program_id = fields.Many2one("mc.program", string="Program", required=True)
|
||||||
|
year_id = fields.Many2one(
|
||||||
|
"mc.academic.year", string="Academic Year", required=True,
|
||||||
|
default=lambda self: self.env["mc.academic.year"].search(
|
||||||
|
[("is_current", "=", True)], limit=1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
batch_id = fields.Many2one(
|
||||||
|
"mc.batch", string="Batch", required=True,
|
||||||
|
domain="[('program_id', '=', program_id), ('year_id', '=', year_id)]",
|
||||||
|
)
|
||||||
|
roll_no = fields.Char(string="Roll No.")
|
||||||
|
|
||||||
|
def _find_or_create_guardian(self, applicant):
|
||||||
|
partner = self.env["res.partner"].search(
|
||||||
|
[("email", "=", applicant.guardian_email)], limit=1,
|
||||||
|
)
|
||||||
|
if partner:
|
||||||
|
guardian = self.env["mc.guardian"].search(
|
||||||
|
[("partner_id", "=", partner.id)], limit=1,
|
||||||
|
)
|
||||||
|
if guardian:
|
||||||
|
return guardian
|
||||||
|
else:
|
||||||
|
partner = self.env["res.partner"].create({
|
||||||
|
"name": applicant.guardian_name,
|
||||||
|
"email": applicant.guardian_email,
|
||||||
|
"phone": applicant.guardian_phone,
|
||||||
|
})
|
||||||
|
return self.env["mc.guardian"].create({
|
||||||
|
"partner_id": partner.id,
|
||||||
|
"name": applicant.guardian_name,
|
||||||
|
"phone": applicant.guardian_phone,
|
||||||
|
"email": applicant.guardian_email,
|
||||||
|
})
|
||||||
|
|
||||||
|
def action_convert(self):
|
||||||
|
self.ensure_one()
|
||||||
|
applicant = self.applicant_id
|
||||||
|
guardian = self._find_or_create_guardian(applicant)
|
||||||
|
|
||||||
|
student_partner = self.env["res.partner"].create({"name": applicant.name})
|
||||||
|
student = self.env["mc.student"].create({
|
||||||
|
"partner_id": student_partner.id,
|
||||||
|
"name": applicant.name,
|
||||||
|
"dob": applicant.dob,
|
||||||
|
"admission_date": fields.Date.context_today(self),
|
||||||
|
"status": "active",
|
||||||
|
"application_no": applicant.application_no,
|
||||||
|
})
|
||||||
|
self.env["mc.student.guardian"].create({
|
||||||
|
"student_id": student.id,
|
||||||
|
"guardian_id": guardian.id,
|
||||||
|
"relationship": applicant.guardian_relationship or "other",
|
||||||
|
"is_primary": True,
|
||||||
|
})
|
||||||
|
self.env["mc.enrollment"].create({
|
||||||
|
"student_id": student.id,
|
||||||
|
"program_id": self.program_id.id,
|
||||||
|
"batch_id": self.batch_id.id,
|
||||||
|
"year_id": self.year_id.id,
|
||||||
|
"state": "active",
|
||||||
|
"roll_no": self.roll_no,
|
||||||
|
"date_enrolled": fields.Date.context_today(self),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Every attachment collected during the application (birth
|
||||||
|
# certificate, etc.) moves with it - zero re-typing, zero re-upload.
|
||||||
|
attachments = self.env["ir.attachment"].search([
|
||||||
|
("res_model", "=", "mc.applicant"), ("res_id", "=", applicant.id),
|
||||||
|
])
|
||||||
|
attachments.write({"res_model": "mc.student", "res_id": student.id})
|
||||||
|
|
||||||
|
applicant.write({"student_id": student.id, "state": "enrolled"})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "mc.student",
|
||||||
|
"res_id": student.id,
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "current",
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user