Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions education/bulk_enroll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import frappe
from frappe import _

BATCH_SIZE = 200

@frappe.whitelist()
def bulk_approve_enroll(names):
import json
if isinstance(names, str):
names = json.loads(names)

all_applicants = frappe.get_all(
'Student Applicant',
filters=[['name', 'in', names]],
fields=['name', 'application_status', 'first_name', 'last_name',
'program', 'academic_year', 'academic_term', 'date_of_birth',
'gender', 'student_email_id', 'student_mobile_number',
'student_category']
)

applicant_map = {a.name: a for a in all_applicants}

already_enrolled = [n for n in names if applicant_map.get(n)
and applicant_map[n].application_status in ('Admitted', 'Approved')]

to_process = [n for n in names if applicant_map.get(n)
and applicant_map[n].application_status not in ('Admitted', 'Approved')]

if not to_process:
return {
'queued': False,
'message': _('All selected students are already enrolled.'),
'already_enrolled_count': len(already_enrolled),
'to_process_count': 0
}

success = []
failed = []

default_academic_year = frappe.db.get_single_value(
'Education Settings', 'current_academic_year'
) or frappe.db.get_value('Academic Year', {}, 'name',
order_by='year_start_date desc')

default_academic_term = frappe.db.get_single_value(
'Education Settings', 'current_academic_term'
) or frappe.db.get_value('Academic Term', {}, 'name',
order_by='term_start_date desc')

existing_students = frappe.get_all('Student',
filters=[['student_applicant', 'in', to_process]],
fields=['name', 'student_applicant'])

existing_student_map = {s.student_applicant: s.name
for s in existing_students}

term_cache = {}

def get_term_for_year(year):
if year not in term_cache:
term_cache[year] = frappe.db.get_value(
'Academic Term', {'academic_year': year}, 'name',
order_by='term_start_date asc') or default_academic_term
return term_cache[year]

for i in range(0, len(to_process), BATCH_SIZE):
batch = to_process[i:i + BATCH_SIZE]

for name in batch:
try:
doc_data = applicant_map[name]
yr = doc_data.academic_year or default_academic_year
term = doc_data.academic_term or get_term_for_year(yr)

# STEP 1: Approve
doc = frappe.get_doc('Student Applicant', name)
doc.application_status = 'Approved'
doc.academic_year = yr
doc.academic_term = term
doc.save(ignore_permissions=True, ignore_version=True)

# STEP 2: Create Student record
if name in existing_student_map:
student_name = existing_student_map[name]
else:
s = frappe.new_doc('Student')
s.first_name = doc_data.first_name
s.last_name = doc_data.last_name or ''
s.student_applicant = name
if doc_data.date_of_birth:
s.date_of_birth = doc_data.date_of_birth
if doc_data.gender:
s.gender = doc_data.gender
if doc_data.student_email_id:
s.student_email_id = doc_data.student_email_id
if doc_data.student_mobile_number:
s.student_mobile_number = doc_data.student_mobile_number
if doc_data.student_category:
s.student_category = doc_data.student_category
s.insert(ignore_permissions=True)
student_name = s.name
existing_student_map[name] = student_name

# STEP 3: Program Enrollment
if doc_data.program:
if not frappe.db.get_value('Program Enrollment',
{'student': student_name, 'program': doc_data.program},
'name'):
e = frappe.new_doc('Program Enrollment')
e.student = student_name
e.program = doc_data.program
e.academic_year = yr
e.academic_term = term
e.enrollment_date = frappe.utils.today()
if doc_data.student_category:
e.student_category = doc_data.student_category
e.insert(ignore_permissions=True)

# STEP 4: Reload then mark as Admitted
doc.reload()
doc.application_status = 'Admitted'
doc.save(ignore_permissions=True, ignore_version=True)

success.append(name)

except Exception as ex:
frappe.db.rollback()
frappe.log_error(title='Bulk Enroll Failed', message=str(ex))
failed.append({'name': name, 'error': str(ex)})

frappe.db.commit()

return {
'queued': True, 'success': success, 'failed': failed,
'already_enrolled_count': len(already_enrolled),
'to_process_count': len(to_process),
'total_selected': len(names),
'success_count': len(success),
'failed_count': len(failed)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,80 @@
frappe.listview_settings['Student Applicant'] = {
add_fields: ['application_status', 'paid'],
has_indicator_for_draft: 1,
get_indicator: function (doc) {
if (doc.paid) {
return [__('Paid'), 'green', 'paid,=,Yes']
} else if (doc.application_status == 'Applied') {
return [__('Applied'), 'orange', 'application_status,=,Applied']
} else if (doc.application_status == 'Approved') {
return [__('Approved'), 'green', 'application_status,=,Approved']
} else if (doc.application_status == 'Rejected') {
return [__('Rejected'), 'red', 'application_status,=,Rejected']
} else if (doc.application_status == 'Admitted') {
return [__('Admitted'), 'blue', 'application_status,=,Admitted']
add_fields: ['application_status'],
get_indicator: function(doc) {
if (doc.application_status === 'Admitted')
return [__('Admitted'), 'green', 'application_status,=,Admitted'];
if (doc.application_status === 'Approved')
return [__('Approved'), 'blue', 'application_status,=,Approved'];
if (doc.application_status === 'Rejected')
return [__('Rejected'), 'red', 'application_status,=,Rejected'];
return [__('Applied'), 'orange', 'application_status,=,Applied'];
},
onload: function(listview) {
listview.page.add_action_item(__('Approve & Enroll'), function() {
let selected = listview.get_checked_items();
if (!selected.length) {
frappe.msgprint({ title: 'No Students Selected',
message: 'Please select students first.',
indicator: 'orange' }); return;
}
let already = selected.filter(r =>
['Admitted','Approved'].includes(r.application_status));
let toEnroll = selected.filter(r =>
!['Admitted','Approved'].includes(r.application_status));
if (!toEnroll.length) {
frappe.msgprint({ title: 'Nothing to Process',
message: 'All selected are already enrolled.',
indicator: 'orange' }); return;
}
let msg = '<table style="width:100%;font-size:14px;border-collapse:collapse">';
msg += '<tr style="background:#f5f5f5"><td style="padding:10px">Total selected</td><td><b>' + selected.length + '</b></td></tr>';
msg += '<tr style="background:#fff8e1"><td style="padding:10px">Already enrolled (skip)</td><td><b style="color:orange">' + already.length + '</b></td></tr>';
msg += '<tr style="background:#e8f5e9"><td style="padding:10px">Will be enrolled</td><td><b style="color:green">' + toEnroll.length + '</b></td></tr>';
msg += '</table>';
let confirmDialog = new frappe.ui.Dialog({
title: 'Confirm Bulk Enrollment',
fields: [{ fieldtype: 'HTML', options: msg }],
primary_action_label: 'Yes, Enroll',
primary_action: function() {
confirmDialog.hide();
frappe.show_progress('Enrolling Students...', 0, 100, 'Starting...');
frappe.call({
method: 'education.bulk_enroll.bulk_approve_enroll',
args: { names: JSON.stringify(selected.map(d => d.name)) },
timeout: 300000,
callback: function(r) {
frappe.show_progress('Enrolling Students...', 100, 100, 'Done!');
setTimeout(() => frappe.hide_progress(), 800);
let res = r.message;
let out = '<table style="width:100%;font-size:14px">';
if (res.success_count > 0) out += '<tr style="background:#e8f5e9"><td style="padding:10px">Enrolled</td><td><b style="color:green">' + res.success_count + ' students</b></td></tr>';
if (res.already_enrolled_count > 0) out += '<tr style="background:#fff8e1"><td style="padding:10px">Skipped</td><td><b style="color:orange">' + res.already_enrolled_count + '</b></td></tr>';
if (res.failed_count > 0) out += '<tr style="background:#ffebee"><td style="padding:10px">Failed</td><td><b style="color:red">' + res.failed_count + '</b></td></tr>';
out += '</table>';
if (res.failed && res.failed.length) {
out += '<hr><p><b>Failed students:</b></p><ul>';
res.failed.forEach(f => out += '<li><b>' + f.name + '</b>: ' + f.error + '</li>');
out += '</ul>';
}
listview.refresh();
let d = new frappe.ui.Dialog({
title: res.failed_count > 0 ? 'Enrollment Complete with Errors' : 'Enrollment Complete!',
fields: [{ fieldtype: 'HTML', options: out }],
primary_action_label: 'Done',
primary_action: () => d.hide() });
d.show();
},
error: function(xhr) {
frappe.hide_progress();
frappe.msgprint({ title: 'Error',
message: 'Something went wrong. Check error logs.',
indicator: 'red' });
}
});
},
secondary_action_label: 'Cancel'
});
confirmDialog.show();
});
}
},
}
};
2 changes: 2 additions & 0 deletions education/public/js/education.bundle.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
import './assessment_result_tool.html'

import './student_applicant_list.js'