import logging from odoo import http from odoo.http import request from odoo.addons.encoach_api.controllers.base import ( jwt_required, _json_response, _error_response, _get_json_body, _paginate, ) _logger = logging.getLogger(__name__) def _ser_year(r): terms = [] if hasattr(r, 'academic_term_ids'): for t in r.academic_term_ids: terms.append(_ser_term(t)) return { 'id': r.id, 'name': r.name or '', 'start_date': str(r.start_date) if r.start_date else '', 'end_date': str(r.end_date) if r.end_date else '', 'term_structure': getattr(r, 'term_structure', 'others') or 'others', 'terms': terms, } def _ser_term(t): return { 'id': t.id, 'name': t.name or '', 'term_start_date': str(t.term_start_date) if hasattr(t, 'term_start_date') and t.term_start_date else str(t.start_date) if hasattr(t, 'start_date') and t.start_date else '', 'term_end_date': str(t.term_end_date) if hasattr(t, 'term_end_date') and t.term_end_date else str(t.end_date) if hasattr(t, 'end_date') and t.end_date else '', 'academic_year_id': t.academic_year_id.id if hasattr(t, 'academic_year_id') and t.academic_year_id else 0, 'academic_year_name': t.academic_year_id.name if hasattr(t, 'academic_year_id') and t.academic_year_id else '', 'parent_term_id': t.parent_id.id if hasattr(t, 'parent_id') and t.parent_id else None, 'parent_term_name': t.parent_id.name if hasattr(t, 'parent_id') and t.parent_id else None, } def _ser_dept(d): env = d.env course_count = 0 faculty_count = 0 try: if 'op.course' in env: Course = env['op.course'].sudo() if 'department_id' in Course._fields: course_count = Course.search_count([('department_id', '=', d.id)]) if 'op.faculty' in env: Faculty = env['op.faculty'].sudo() if 'department_id' in Faculty._fields: faculty_count = Faculty.search_count([('department_id', '=', d.id)]) elif 'main_department_id' in Faculty._fields: faculty_count = Faculty.search_count([('main_department_id', '=', d.id)]) except Exception: pass return { 'id': d.id, 'name': d.name or '', 'code': d.code or '' if hasattr(d, 'code') else '', 'parent_id': d.parent_id.id if hasattr(d, 'parent_id') and d.parent_id else None, 'parent_name': d.parent_id.name if hasattr(d, 'parent_id') and d.parent_id else None, 'course_count': course_count, 'faculty_count': faculty_count, } class AcademicController(http.Controller): # ── Academic Years ─────────────────────────────────────────────── @http.route('/api/academic-years', type='http', auth='public', methods=['GET'], csrf=False) @jwt_required def list_years(self, **kw): try: M = request.env['op.academic.year'].sudo() offset, limit, page = _paginate(kw) total = M.search_count([]) recs = M.search([], offset=offset, limit=limit, order='id desc') return _json_response({'items': [_ser_year(r) for r in recs], 'data': [_ser_year(r) for r in recs], 'total': total, 'page': page, 'size': limit}) except Exception as e: _logger.exception('list_years') return _error_response(str(e), 500) @http.route('/api/academic-years/', type='http', auth='public', methods=['GET'], csrf=False) @jwt_required def get_year(self, yid, **kw): try: rec = request.env['op.academic.year'].sudo().browse(yid) if not rec.exists(): return _error_response('Not found', 404) return _json_response(_ser_year(rec)) except Exception as e: return _error_response(str(e), 500) @http.route('/api/academic-years', type='http', auth='public', methods=['POST'], csrf=False) @jwt_required def create_year(self, **kw): try: body = _get_json_body() Year = request.env['op.academic.year'].sudo() vals = { 'name': body.get('name', ''), 'start_date': body.get('start_date'), 'end_date': body.get('end_date'), } if body.get('term_structure') and 'term_structure' in Year._fields: vals['term_structure'] = body['term_structure'] rec = Year.create(vals) return _json_response(_ser_year(rec)) except Exception as e: return _error_response(str(e), 500) @http.route('/api/academic-years/', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False) @jwt_required def update_year(self, yid, **kw): try: rec = request.env['op.academic.year'].sudo().browse(yid) if not rec.exists(): return _error_response('Not found', 404) body = _get_json_body() vals = {} for k in ('name', 'start_date', 'end_date'): if k in body: vals[k] = body[k] if 'term_structure' in body and 'term_structure' in rec._fields: vals['term_structure'] = body['term_structure'] if vals: rec.write(vals) return _json_response(_ser_year(rec)) except Exception as e: return _error_response(str(e), 500) @http.route('/api/academic-years/', type='http', auth='public', methods=['DELETE'], csrf=False) @jwt_required def delete_year(self, yid, **kw): try: rec = request.env['op.academic.year'].sudo().browse(yid) if rec.exists(): # Unlink dependent terms first to avoid RESTRICT FK violation. if hasattr(rec, 'academic_term_ids') and rec.academic_term_ids: rec.academic_term_ids.unlink() rec.unlink() return _json_response({'success': True}) except Exception as e: _logger.exception('delete_year') return _error_response(str(e), 500) @http.route('/api/academic-years//generate-terms', type='http', auth='public', methods=['POST'], csrf=False) @jwt_required def generate_terms(self, yid, **kw): """Create a set of op.academic.term records spanning the year. The number of terms is dictated by the year's `term_structure`: two_sem → 2 semesters two_sem_qua → 2 semesters + 4 quarters (nested under each sem) three_sem → 3 semesters four_Quarter → 4 quarters final_year → 1 single term others → 2 semesters (default fallback) """ try: from datetime import timedelta year = request.env['op.academic.year'].sudo().browse(yid) if not year.exists(): return _error_response('Not found', 404) start = year.start_date end = year.end_date if not start or not end: return _error_response('Year has no dates', 400) total_days = max(1, (end - start).days) structure = getattr(year, 'term_structure', 'two_sem') or 'two_sem' body = _get_json_body() or {} if body.get('replace'): for existing in year.academic_term_ids: existing.sudo().unlink() Term = request.env['op.academic.term'].sudo() def _split(n, label): chunk = total_days // n out = [] for i in range(n): s = start + timedelta(days=i * chunk) e = end if i == n - 1 else start + timedelta(days=(i + 1) * chunk - 1) out.append(Term.create({ 'name': f'{year.name} - {label} {i + 1}', 'term_start_date': str(s), 'term_end_date': str(e), 'academic_year_id': year.id, })) return out created = [] if structure == 'two_sem': created = _split(2, 'Semester') elif structure == 'three_sem': created = _split(3, 'Semester') elif structure == 'four_Quarter': created = _split(4, 'Quarter') elif structure == 'final_year': created = [Term.create({ 'name': f'{year.name} - Final Year', 'term_start_date': str(start), 'term_end_date': str(end), 'academic_year_id': year.id, })] elif structure == 'two_sem_qua': sems = _split(2, 'Semester') quarters = [] for parent_idx, parent in enumerate(sems): s = parent.term_start_date e = parent.term_end_date span = max(1, (e - s).days) half = span // 2 q1 = Term.create({ 'name': f'{year.name} - Q{parent_idx * 2 + 1}', 'term_start_date': str(s), 'term_end_date': str(s + timedelta(days=half)), 'academic_year_id': year.id, }) q2 = Term.create({ 'name': f'{year.name} - Q{parent_idx * 2 + 2}', 'term_start_date': str(s + timedelta(days=half + 1)), 'term_end_date': str(e), 'academic_year_id': year.id, }) if 'parent_id' in Term._fields: q1.write({'parent_id': parent.id}) q2.write({'parent_id': parent.id}) quarters += [q1, q2] created = sems + quarters else: created = _split(2, 'Semester') year.invalidate_recordset() return _json_response([_ser_term(t) for t in created]) except Exception as e: _logger.exception('generate_terms') return _error_response(str(e), 500) # ── Academic Terms ─────────────────────────────────────────────── @http.route('/api/academic-terms', type='http', auth='public', methods=['GET'], csrf=False) @jwt_required def list_terms(self, **kw): try: M = request.env['op.academic.term'].sudo() domain = [] if kw.get('year_id'): domain.append(('academic_year_id', '=', int(kw['year_id']))) offset, limit, page = _paginate(kw) total = M.search_count(domain) recs = M.search(domain, offset=offset, limit=limit, order='id desc') return _json_response({'items': [_ser_term(r) for r in recs], 'data': [_ser_term(r) for r in recs], 'total': total, 'page': page, 'size': limit}) except Exception as e: return _error_response(str(e), 500) @http.route('/api/academic-terms', type='http', auth='public', methods=['POST'], csrf=False) @jwt_required def create_term(self, **kw): try: body = _get_json_body() vals = { 'name': body.get('name', ''), 'term_start_date': body.get('term_start_date'), 'term_end_date': body.get('term_end_date'), } if body.get('academic_year_id'): vals['academic_year_id'] = int(body['academic_year_id']) rec = request.env['op.academic.term'].sudo().create(vals) return _json_response(_ser_term(rec)) except Exception as e: return _error_response(str(e), 500) @http.route('/api/academic-terms/', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False) @jwt_required def update_term(self, tid, **kw): try: rec = request.env['op.academic.term'].sudo().browse(tid) if not rec.exists(): return _error_response('Not found', 404) body = _get_json_body() vals = {} for k in ('name', 'term_start_date', 'term_end_date'): if k in body: vals[k] = body[k] if 'academic_year_id' in body: vals['academic_year_id'] = int(body['academic_year_id']) if vals: rec.write(vals) return _json_response(_ser_term(rec)) except Exception as e: return _error_response(str(e), 500) @http.route('/api/academic-terms/', type='http', auth='public', methods=['DELETE'], csrf=False) @jwt_required def delete_term(self, tid, **kw): try: rec = request.env['op.academic.term'].sudo().browse(tid) if rec.exists(): rec.unlink() return _json_response({'success': True}) except Exception as e: return _error_response(str(e), 500) # ── Departments ────────────────────────────────────────────────── @http.route('/api/departments', type='http', auth='public', methods=['GET'], csrf=False) @jwt_required def list_depts(self, **kw): try: M = request.env['op.department'].sudo() offset, limit, page = _paginate(kw) total = M.search_count([]) recs = M.search([], offset=offset, limit=limit, order='id desc') return _json_response({'items': [_ser_dept(r) for r in recs], 'data': [_ser_dept(r) for r in recs], 'total': total, 'page': page, 'size': limit}) except Exception as e: return _error_response(str(e), 500) @http.route('/api/departments', type='http', auth='public', methods=['POST'], csrf=False) @jwt_required def create_dept(self, **kw): try: body = _get_json_body() vals = {'name': body.get('name', '')} if body.get('code'): vals['code'] = body['code'] if body.get('parent_id'): vals['parent_id'] = int(body['parent_id']) rec = request.env['op.department'].sudo().create(vals) return _json_response(_ser_dept(rec)) except Exception as e: return _error_response(str(e), 500) @http.route('/api/departments/', type='http', auth='public', methods=['PATCH', 'PUT'], csrf=False) @jwt_required def update_dept(self, did, **kw): try: rec = request.env['op.department'].sudo().browse(did) if not rec.exists(): return _error_response('Not found', 404) body = _get_json_body() vals = {} for k in ('name', 'code'): if k in body: vals[k] = body[k] if 'parent_id' in body: vals['parent_id'] = int(body['parent_id']) if body['parent_id'] else False if vals: rec.write(vals) return _json_response(_ser_dept(rec)) except Exception as e: return _error_response(str(e), 500) @http.route('/api/departments/', type='http', auth='public', methods=['DELETE'], csrf=False) @jwt_required def delete_dept(self, did, **kw): try: rec = request.env['op.department'].sudo().browse(did) if rec.exists(): rec.unlink() return _json_response({'success': True}) except Exception as e: return _error_response(str(e), 500)