repo changed- files added
This commit is contained in:
commit
13e5953bdf
514
EDUCATION_MODULES_SUMMARY.md
Normal file
514
EDUCATION_MODULES_SUMMARY.md
Normal file
@ -0,0 +1,514 @@
|
|||||||
|
# 📚 Complete Technical Documentation of Custom Education Odoo Addons
|
||||||
|
|
||||||
|
This document contains a comprehensive, field-level technical summary of all **10 custom education modules** built for the Odoo Education & Student Portal ecosystem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 🏛️ `education_base` (Base Academic Setup & Hierarchy)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
The `education_base` module establishes the core structural hierarchy of the educational institution. It defines campuses, departments, degree programs, academic levels, and academic calendar timelines (years and terms).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models & Detailed Field Definitions**
|
||||||
|
|
||||||
|
#### **1.1 `education.campus` (Campus Master)**
|
||||||
|
- **Functionality**: Represents physical or virtual university campuses.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: The official name of the campus.
|
||||||
|
- `code` *(Char, Required)*: Unique abbreviation code.
|
||||||
|
- `street` / `street2` / `city` / `zip` / `state_id` / `country_id` *(Location fields)*: Physical address details.
|
||||||
|
- `phone` / `email` *(Char)*: Contact details for campus administration.
|
||||||
|
- `company_id` *(Many2one)*: Associated Odoo `res.company`.
|
||||||
|
- `active` *(Boolean)*: Soft-delete toggle (default: `True`).
|
||||||
|
|
||||||
|
#### **1.2 `education.department` (Department Master)**
|
||||||
|
- **Functionality**: Academic departments within a campus (e.g., Computer Science, Mechanical).
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Department name.
|
||||||
|
- `code` *(Char, Required)*: Short code.
|
||||||
|
- `campus_id` *(Many2one, Required)*: Relates to `education.campus`.
|
||||||
|
- `head_teacher_id` *(Many2one)*: Relates to `res.partner` (Department Chair / Dean).
|
||||||
|
- `program_ids` *(One2many)*: List of programs offered under this department.
|
||||||
|
- `company_id` *(Many2one)*: Multi-company reference.
|
||||||
|
|
||||||
|
#### **1.3 `education.program` (Academic Program)**
|
||||||
|
- **Functionality**: Degree or diploma courses (e.g., MCA, B.Tech, MBA).
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Program name.
|
||||||
|
- `code` *(Char, Required)*: Program short code.
|
||||||
|
- `department_id` *(Many2one, Required)*: Relates to `education.department`.
|
||||||
|
- `level_id` *(Many2one)*: Academic level (`Undergraduate`, `Postgraduate`).
|
||||||
|
- `duration` *(Integer)*: Number of duration units (e.g., `2`).
|
||||||
|
- `duration_unit` *(Selection)*: `year`, `month`, or `semester`.
|
||||||
|
- `description` *(Text)*: Overview of course curriculum.
|
||||||
|
|
||||||
|
#### **1.4 `education.academic.year` (Academic Session)**
|
||||||
|
- **Functionality**: Defines full academic years and tracks current active session.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Name of the academic year (e.g., `"2026 - 2027"`).
|
||||||
|
- `code` *(Char, Required)*: Unique code.
|
||||||
|
- `start_date` *(Date, Required)*: Start date of session.
|
||||||
|
- `end_date` *(Date, Required)*: End date of session.
|
||||||
|
- `current_year` *(Boolean)*: Indicates if this is the active operational year.
|
||||||
|
- `state` *(Selection)*: `draft`, `active`, `closed`.
|
||||||
|
- `term_ids` *(One2many)*: Semesters/Terms within this year.
|
||||||
|
|
||||||
|
#### **1.5 `education.academic.term` (Academic Semester / Term)**
|
||||||
|
- **Functionality**: Sub-divisions of an academic year (e.g., Semester 1, Semester 2).
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Term name.
|
||||||
|
- `code` *(Char, Required)*: Term code (e.g., `"SEM1"`).
|
||||||
|
- `academic_year_id` *(Many2one, Required)*: Parent `education.academic.year`.
|
||||||
|
- `sequence` *(Integer)*: Term ordering (1, 2, 3...).
|
||||||
|
- `start_date` / `end_date` *(Date)*: Start and end dates of term.
|
||||||
|
- `state` *(Selection)*: `draft`, `active`, `completed`.
|
||||||
|
|
||||||
|
#### **1.6 `education.configuration` (Education Configuration)**
|
||||||
|
- **Functionality**: Global institution-level settings and defaults.
|
||||||
|
- **Fields**:
|
||||||
|
- `school_name` / `school_code` *(Char, Required)*: Institution name and code.
|
||||||
|
- `default_campus_id` / `default_academic_year_id` / `default_term_id` *(Many2one)*: Default hierarchical selections for the institution.
|
||||||
|
- `logo` *(Binary)*: Institution logo.
|
||||||
|
- `contact_email` / `contact_phone` *(Char)*: Global contact information.
|
||||||
|
|
||||||
|
#### **1.7 `education.academic.level` (Academic Level)**
|
||||||
|
- **Functionality**: Classification of education levels (e.g., Undergraduate, Postgraduate).
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Level name.
|
||||||
|
- `code` *(Char, Required)*: Level code.
|
||||||
|
- `sequence` *(Integer)*: Ordering priority.
|
||||||
|
- `description` *(Text)*: Additional details about the level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Record Examples (`student_db`)**
|
||||||
|
```yaml
|
||||||
|
Campus Record:
|
||||||
|
ID: 1 | Name: "Main Campus" | Code: "MC" | City: "Thanjavur"
|
||||||
|
|
||||||
|
Department Record:
|
||||||
|
ID: 1 | Name: "School of Computing" | Code: "SCS" | Campus: "Main Campus"
|
||||||
|
|
||||||
|
Program Record:
|
||||||
|
ID: 1 | Name: "Master of Computer Applications - Lateral Entry" | Code: "MCA-L" | Duration: 2 Years
|
||||||
|
|
||||||
|
Academic Year Record:
|
||||||
|
ID: 1 | Name: "2026 - 2027" | Code: "053" | Start: 2026-07-03 | End: 2027-07-03 | Current: True
|
||||||
|
|
||||||
|
Academic Term Record:
|
||||||
|
ID: 1 | Name: "Semester 1" | Code: "SEM1" | Sequence: 1 | Year: "2026 - 2027"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 👨🎓 `education_student` (Student Lifecycle & Profile Management)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Central student registry handling personal profiles, academic placement, guardian information, document attachments, portal user links, and automated financial/attendance computations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models & Detailed Field Definitions**
|
||||||
|
|
||||||
|
#### **2.1 `education.student` (Student Profile)**
|
||||||
|
- **Personal Details**:
|
||||||
|
- `first_name` / `middle_name` / `last_name` *(Char)*: Individual name components.
|
||||||
|
- `full_name` *(Char, Computed)*: Concatenated name (`"Aadhav S"`).
|
||||||
|
- `gender` *(Selection)*: `male`, `female`, `other`.
|
||||||
|
- `date_of_birth` *(Date)*: Date of birth.
|
||||||
|
- `age` *(Integer, Computed)*: Calculated age in years.
|
||||||
|
- `email` / `phone` / `mobile` *(Char)*: Contact info.
|
||||||
|
- `image_1920` *(Image)*: Student profile photo.
|
||||||
|
- **Academic Identification & Placement**:
|
||||||
|
- `admission_number` *(Char)*: Auto-generated admission ID (`"ADM/2026/001"`).
|
||||||
|
- `roll_number` *(Char)*: Roll number (`"ROLL/0003"`).
|
||||||
|
- `registration_number` *(Char)*: University registration number.
|
||||||
|
- `campus_id` / `department_id` / `program_id` / `academic_year_id` / `academic_term_id` *(Many2one)*: Relates student to institutional hierarchy.
|
||||||
|
- `status` *(Selection)*: `draft`, `applied`, `admitted`, `enrolled`, `graduated`, `withdrawn`.
|
||||||
|
- **Parents & Guardian Information**:
|
||||||
|
- `father_name` / `father_phone` / `father_occupation` *(Char)*: Father's details.
|
||||||
|
- `mother_name` / `mother_phone` / `mother_occupation` *(Char)*: Mother's details.
|
||||||
|
- `guardian_name` / `guardian_phone` / `guardian_email` / `guardian_address` *(Char/Text)*: Guardian details.
|
||||||
|
- **Document Storage**:
|
||||||
|
- `doc_passport`, `doc_aadhaar`, `doc_birth_cert`, `doc_transfer_cert`, `doc_community_cert`, `doc_mark_sheets` *(Binary & Char)*: Stores uploaded compliance documents and filenames.
|
||||||
|
- **Computed Financial Summary**:
|
||||||
|
- `total_fees` *(Monetary, Computed)*: Total assigned fees (₹34,000.00).
|
||||||
|
- `total_paid` *(Monetary, Computed)*: Sum of all posted payments (₹34,000.00).
|
||||||
|
- `outstanding_amount` *(Monetary, Computed)*: Total due balance (₹0.00).
|
||||||
|
- `payment_status` *(Selection, Computed)*: `no_fees`, `unpaid`, `partial`, `fully_paid`.
|
||||||
|
- `ledger_line_ids` *(One2many)*: Inverse relation to `education.student.ledger`.
|
||||||
|
- **Computed Attendance Summary**:
|
||||||
|
- `attendance_pct` *(Float, Computed)*: Overall attendance percentage (e.g., `92.5`).
|
||||||
|
- `present_count` / `absent_count` / `late_count` / `medical_leave_count` *(Integer, Computed)*: Session counts.
|
||||||
|
- **Portal User Linkage**:
|
||||||
|
- `portal_user_id` *(Many2one)*: Relates to Odoo `res.users` (Portal Account).
|
||||||
|
- `portal_status` *(Selection)*: `draft`, `active`, `deactivated`.
|
||||||
|
|
||||||
|
#### **2.2 `education.batch` (Academic Batch)**
|
||||||
|
- **Functionality**: Represents a specific student batch/cohort.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Batch name.
|
||||||
|
- `code` *(Char, Required)*: Unique batch code.
|
||||||
|
|
||||||
|
#### **2.3 `education.semester` (Academic Semester)**
|
||||||
|
- **Functionality**: Specific academic semesters decoupled from global terms.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Semester name.
|
||||||
|
- `code` *(Char, Required)*: Unique semester code.
|
||||||
|
|
||||||
|
#### **2.4 `education.section` (Academic Section)**
|
||||||
|
- **Functionality**: Classroom sections or groups for students.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Section name.
|
||||||
|
- `code` *(Char, Required)*: Unique section code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Record Examples (`student_db`)**
|
||||||
|
```yaml
|
||||||
|
Student Record #284:
|
||||||
|
Admission No: "ADM/2026/001" | Roll No: "ROLL/0003"
|
||||||
|
Name: "Aadhav S" | Email: "aadhav@gmail.com" | Phone: "9876543210"
|
||||||
|
Program: "Master of Computer Applications - Lateral Entry"
|
||||||
|
Total Fees: ₹34,000.00 | Total Paid: ₹34,000.00 | Outstanding: ₹0.00
|
||||||
|
Payment Status: fully_paid
|
||||||
|
Attendance %: 92.5% (Present: 37, Absent: 3)
|
||||||
|
Portal User: aadhav@gmail.com (Status: Active)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 📅 `education_attendance` (Attendance & Absence Tracking)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Provides session-level attendance recording for students, automated calculation of attendance percentages, and historical session logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models & Detailed Field Definitions**
|
||||||
|
|
||||||
|
#### **3.1 `education.attendance` (Student Attendance Record)**
|
||||||
|
- **Fields**:
|
||||||
|
- `student_id` *(Many2one, Required)*: Relates to `education.student`.
|
||||||
|
- `date` *(Date, Required)*: Attendance session date.
|
||||||
|
- `state` *(Selection, Required)*: `present`, `absent`, `late`, `medical_leave`.
|
||||||
|
- `remarks` *(Text)*: Optional notes or justification for leave.
|
||||||
|
- `academic_year_id` / `academic_term_id` *(Many2one)*: Timeline reference.
|
||||||
|
- `company_id` *(Many2one)*: Multi-company lock.
|
||||||
|
|
||||||
|
#### **3.2 `education.attendance.session` (Attendance Session)**
|
||||||
|
- **Functionality**: Organizes attendance taking by combining date, location, and hierarchy.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Session title.
|
||||||
|
- `campus_id` / `department_id` / `program_id` / `academic_year_id` *(Many2one, Required)*: Targeting fields.
|
||||||
|
|
||||||
|
#### **3.3 `education.attendance.line` (Attendance Line Detail)**
|
||||||
|
- **Functionality**: Individual student attendance state for a session.
|
||||||
|
- **Fields**:
|
||||||
|
- `session_id` *(Many2one, Required)*: Parent session.
|
||||||
|
- `student_id` *(Many2one, Required)*: Target student.
|
||||||
|
- `status` *(Selection)*: `present`, `absent`, `late`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Record Examples (`student_db`)**
|
||||||
|
```yaml
|
||||||
|
Attendance Entry #101:
|
||||||
|
Student: "Aadhav S"
|
||||||
|
Date: 2026-08-10
|
||||||
|
State: Present
|
||||||
|
Academic Term: Semester 1
|
||||||
|
|
||||||
|
Attendance Entry #102:
|
||||||
|
Student: "Hareish S"
|
||||||
|
Date: 2026-08-10
|
||||||
|
State: Absent
|
||||||
|
Remarks: "Sick leave - Doctor note submitted"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 💳 `education_finance` (Fees, Payments & Double-Entry Ledger)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Complete financial accounting engine for student fee assessment, payment posting, fee structures, discounts, scholarships, refunds, and real-time double-entry student financial ledgers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models & Detailed Field Definitions**
|
||||||
|
|
||||||
|
#### **4.1 `education.fee.category` (Fee Head)**
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Name of fee type (`"Tuition"`, `"Hostel"`, `"Library"`).
|
||||||
|
- `code` *(Char, Required)*: Code (`TUI`, `HOS`, `LIB`, `EXM`, `TRN`).
|
||||||
|
|
||||||
|
#### **4.2 `education.fee.structure` & `education.fee.structure.line` (Fee Template)**
|
||||||
|
- **Fields (`education.fee.structure`)**:
|
||||||
|
- `name` *(Char, Required)*: Name of structure (e.g., `"MCA Semester 1 2026 Regular"`).
|
||||||
|
- `program_id` / `academic_year_id` / `academic_term_id` *(Many2one)*: Applicable target.
|
||||||
|
- `line_ids` *(One2many)*: List of `education.fee.structure.line` items.
|
||||||
|
- **Fields (`education.fee.structure.line`)**:
|
||||||
|
- `category_id` *(Many2one, Required)*: Relates to `education.fee.category`.
|
||||||
|
- `amount` *(Monetary, Required)*: Line fee amount.
|
||||||
|
|
||||||
|
#### **4.3 `education.fee.assignment` (Student Fee Assessment)**
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char)*: Sequence (`"ASSIGN/2026/00001"`).
|
||||||
|
- `student_id` *(Many2one, Required)*: Target student.
|
||||||
|
- `fee_structure_id` *(Many2one)*: Fee structure applied.
|
||||||
|
- `total_amount` *(Monetary)*: Gross fee amount (₹34,000.00).
|
||||||
|
- `discount_amount` *(Monetary)*: Discount concession applied.
|
||||||
|
- `net_amount` *(Monetary)*: Net due after discounts.
|
||||||
|
- `amount_paid` *(Monetary, Computed)*: Total payments posted against this assignment.
|
||||||
|
- `state` *(Selection)*: `draft`, `assigned`, `paid`, `cancelled`.
|
||||||
|
|
||||||
|
#### **4.4 `education.student.payment` (Payment Receipt)**
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char)*: Sequence number (`"PAY/2026/00012"`).
|
||||||
|
- `student_id` *(Many2one, Required)*: Paying student.
|
||||||
|
- `fee_assignment_id` *(Many2one)*: Associated fee assignment.
|
||||||
|
- `amount` *(Monetary, Required)*: Amount paid (₹34,000.00).
|
||||||
|
- `payment_date` *(Date, Required)*: Date of transaction.
|
||||||
|
- `payment_method` *(Selection)*: `cash`, `bank`, `online`.
|
||||||
|
- `transaction_id` *(Char)*: Payment gateway transaction ref (`"pay_Px92kL10z"`).
|
||||||
|
- `state` *(Selection)*: `draft`, `posted`, `cancelled`.
|
||||||
|
|
||||||
|
#### **4.5 `education.student.ledger` (Double-Entry Financial Ledger)**
|
||||||
|
- **Fields**:
|
||||||
|
- `student_id` *(Many2one, Required)*: Target student.
|
||||||
|
- `date` *(Date, Required)*: Transaction date.
|
||||||
|
- `description` *(Char, Required)*: Details (e.g., `"Fee Assessment: MCA Sem 1"`).
|
||||||
|
- `reference` *(Char)*: Document source (`ASSIGN/0001` or `PAY/0012`).
|
||||||
|
- `debit` *(Monetary)*: Increase in student financial liability.
|
||||||
|
- `credit` *(Monetary)*: Decrease in student financial liability (Payment).
|
||||||
|
- `balance` *(Monetary, Computed)*: Cumulative balance due after this entry.
|
||||||
|
|
||||||
|
#### **4.6 `education.student.discount` & `education.student.scholarship` & `education.student.refund`**
|
||||||
|
- Models for concession processing, merit scholarship credits, and fee refund disbursements.
|
||||||
|
|
||||||
|
#### **4.7 `education.fee.type` (Fee Type)**
|
||||||
|
- **Functionality**: Granular categorization of fee categories.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Fee type name.
|
||||||
|
- `category_id` *(Many2one, Required)*: Parent category.
|
||||||
|
- `currency_id` *(Many2one, Required)*: Currency for fee.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Record Examples (`student_db`)**
|
||||||
|
```yaml
|
||||||
|
Fee Categories:
|
||||||
|
- Tuition (Code: TUI)
|
||||||
|
- Examination (Code: EXM)
|
||||||
|
- Library (Code: LIB)
|
||||||
|
|
||||||
|
Fee Structure: "MCA Semester 1 2026 Regular"
|
||||||
|
- Line 1: Tuition Fee = ₹30,000.00
|
||||||
|
- Line 2: Examination & Library Fee = ₹4,000.00
|
||||||
|
Total: ₹34,000.00
|
||||||
|
|
||||||
|
Fee Assignment: ASSIGN/2026/00001
|
||||||
|
Student: "Aadhav S" | Net Amount: ₹34,000.00 | Amount Paid: ₹34,000.00 | State: Paid
|
||||||
|
|
||||||
|
Payment Record: PAY/2026/00012
|
||||||
|
Student: "Aadhav S" | Amount: ₹34,000.00 | Method: Online Razorpay | State: Posted
|
||||||
|
|
||||||
|
Ledger Statement for Aadhav S:
|
||||||
|
1. 2026-07-05 | Fee Assessment: MCA Sem 1 | Debit: ₹34,000.00 | Credit: ₹0.00 | Balance: ₹34,000.00
|
||||||
|
2. 2026-07-10 | Payment Received: PAY/0012 | Debit: ₹0.00 | Credit: ₹34,000.00 | Balance: ₹0.00
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 📊 `education_dashboard` (Analytics & Management KPIs)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Aggregates metrics across the institution to present real-time key performance indicators (KPIs) for administration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models & Detailed Field Definitions**
|
||||||
|
|
||||||
|
#### **5.1 `education.dashboard` (Dashboard Helper Model)**
|
||||||
|
- **Fields**:
|
||||||
|
- `total_students` *(Integer, Computed)*: Total active enrolled students.
|
||||||
|
- `total_fees_collected` *(Monetary, Computed)*: Cumulative fee revenue.
|
||||||
|
- `total_outstanding_dues` *(Monetary, Computed)*: Aggregate unpaid student balance.
|
||||||
|
- `average_attendance` *(Float, Computed)*: Average student attendance across institution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Record Examples (`student_db`)**
|
||||||
|
```yaml
|
||||||
|
Dashboard KPI Snapshot:
|
||||||
|
Total Students: 3
|
||||||
|
Total Fees Collected: ₹102,000.00
|
||||||
|
Total Dues Pending: ₹0.00
|
||||||
|
Average Campus Attendance: 94.2%
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 🖥️ `education_portal` (Student Self-Service Portal & Login)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Delivers the student-facing online portal (`/my/education`), online Razorpay payment integration, announcement notices, profile updates, and the customized 50/50 split-screen login page (`/web/login`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models & Detailed Overrides**
|
||||||
|
|
||||||
|
#### **6.1 Custom 50/50 Split-Screen Login Page (`web.login` & `web.login_layout`)**
|
||||||
|
- **Left Panel (Branding & Features)**:
|
||||||
|
- Glassmorphism tint over campus background image.
|
||||||
|
- Dynamic company logo (`/web/image?model=res.company&id=1&field=logo`).
|
||||||
|
- Generalized Welcome Headers & Institutional Description text.
|
||||||
|
- 2x2 Feature Highlights Grid:
|
||||||
|
- 🔐 **Secure Authentication**
|
||||||
|
- ⚡ **Fast & Reliable Access**
|
||||||
|
- 📊 **Real-Time Information**
|
||||||
|
- ☁️ **Cloud-Based Platform**
|
||||||
|
- **Right Panel (Form & Authentication)**:
|
||||||
|
- Welcome Card with database selector.
|
||||||
|
- Input field with icon (`Enter your Email`).
|
||||||
|
- Password input with interactive eye icon toggle.
|
||||||
|
- Remember Me & Forgot Password links.
|
||||||
|
- Google & Microsoft SSO buttons.
|
||||||
|
- Form action URL preserving parameters (`t-attf-action="/web/login?#{keep_query('*')}"`) and hidden `db` field to ensure CSRF validation never expires.
|
||||||
|
|
||||||
|
#### **6.2 `education.announcement` (Notice Board)**
|
||||||
|
- **Fields**:
|
||||||
|
- `title` *(Char, Required)*: Notice title.
|
||||||
|
- `content` *(Html, Required)*: Formatted announcement content.
|
||||||
|
- `category` *(Selection)*: `academic`, `finance`, `event`, `general`.
|
||||||
|
- `target_audience` *(Selection)*: `all`, `students`, `staff`.
|
||||||
|
- `date_published` *(Datetime)*: Timestamp of publication.
|
||||||
|
|
||||||
|
#### **6.3 `education.payment.transaction` (Payment Transaction)**
|
||||||
|
- **Functionality**: Tracks specific payment transactions triggered from the portal.
|
||||||
|
- **Fields**:
|
||||||
|
- `name` *(Char, Required)*: Transaction Reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Record Examples (`student_db`)**
|
||||||
|
```yaml
|
||||||
|
Announcement Record #1:
|
||||||
|
Title: "Semester 1 Examinations Schedule"
|
||||||
|
Category: Academic
|
||||||
|
Target Audience: Students
|
||||||
|
Published Date: 2026-08-20
|
||||||
|
Content: "The end-semester examinations for Semester 1 will commence from December 5th, 2026."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 📄 `education_reports` (QWeb PDF & Excel Analytics)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Generates formal printable PDF documents and analytical export files for identity cards, payment receipts, attendance rosters, and ledgers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Reports & Wizards**
|
||||||
|
|
||||||
|
#### **7.1 Student Fee Receipt (`report_student_fee_receipt`)**
|
||||||
|
- Printable PDF generated when a payment is posted. Includes transaction reference, student roll number, breakdown of fee items, amount paid, and signature line.
|
||||||
|
|
||||||
|
#### **7.2 Student Identity Card (`report_student_id_card`)**
|
||||||
|
- Printable student badge with student photo, barcode/QR code, roll number, department, blood group, and emergency contact details.
|
||||||
|
|
||||||
|
#### **7.3 `education.report.wizard` (Custom Report Exporter)**
|
||||||
|
- Filter wizard allowing administrators to select date ranges, programs, and fee categories to generate custom PDF/Excel reports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 📝 `education_exam` (Education Examinations)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Complete College Examination Module featuring exam scheduling, grading systems, marks entry, and portal integration for students to view results and schedules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models**
|
||||||
|
|
||||||
|
- **`education.exam`**: Examination
|
||||||
|
- **`education.exam.line`**: Exam Schedule Line
|
||||||
|
- **`education.exam.type`**: Examination Type
|
||||||
|
- **`education.exam.result`**: Exam Result
|
||||||
|
- **`education.exam.result.line`**: Exam Result Line
|
||||||
|
- **`education.grading.system`**: Grading System
|
||||||
|
- **`education.grade`**: Education Grade
|
||||||
|
- **`education.subject`**: Education Subject
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 🎓 `education_lms` (Learning Management System)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Learning Management System Foundation for Education managing courses, sections, lessons, materials, assignments, and student submissions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models**
|
||||||
|
|
||||||
|
- **`education.lms.course`**: LMS Course
|
||||||
|
- **`education.lms.section`**: LMS Section
|
||||||
|
- **`education.lms.lesson`**: LMS Lesson
|
||||||
|
- **`education.lms.material`**: LMS Material
|
||||||
|
- **`education.lms.assignment`**: LMS Assignment
|
||||||
|
- **`education.lms.submission`**: LMS Submission
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. ⏰ `education_timetable` (Academic Timetable)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Native Timetable module integrated with the education base architecture. Provides robust server-side validation for room, faculty, and student population conflicts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models**
|
||||||
|
|
||||||
|
- **`education.room`**: Education Room/Venue
|
||||||
|
- **`education.timetable.slot`**: Timetable Slot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 📚 `education_library` (Library Management System)
|
||||||
|
|
||||||
|
### **Module Overview**
|
||||||
|
Provides comprehensive library management including book cataloging, physical copies, member management, issuing/returning of books, reservations, and automated fine calculations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Models**
|
||||||
|
|
||||||
|
- **`education.library`**: Library Branch
|
||||||
|
- **`education.library.category`**: Book Category
|
||||||
|
- **`education.library.book`**: Book Master
|
||||||
|
- **`education.library.book.copy`**: Physical Book Copy
|
||||||
|
- **`education.library.member`**: Library Member
|
||||||
|
- **`education.library.issue`**: Book Issue / Return Record
|
||||||
|
- **`education.library.reservation`**: Book Reservation
|
||||||
|
- **`education.library.fine`**: Library Fine
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Comprehensive Summary Matrix of Custom Addons
|
||||||
|
|
||||||
|
| Module | Core Purpose | Models Defined | Key Output / UI View |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **`education_base`** | Institutional Hierarchy | `education.campus`, `education.department`, `education.program`, `education.academic.year`, `education.academic.term`, `education.configuration`, `education.academic.level` | Academic Setup Form & Tree Views |
|
||||||
|
| **`education_student`** | Student Registry & Profiles | `education.student`, `education.batch`, `education.semester`, `education.section` | Student Master Form, Kanban & Portal User Action |
|
||||||
|
| **`education_attendance`** | Attendance Log & Metrics | `education.attendance`, `education.attendance.session`, `education.attendance.line` | Daily Attendance Grid & % Computation |
|
||||||
|
| **`education_finance`** | Financials, Fees & Ledgers | `education.fee.category`, `education.fee.type`, `education.fee.structure`, `education.fee.assignment`, `education.student.payment`, `education.student.ledger`, `education.student.discount`, `education.student.scholarship`, `education.student.refund` | Student Ledger Statement, Fee Structure Form, Payment Receipts |
|
||||||
|
| **`education_dashboard`** | Admin KPIs & Analytics | `education.dashboard` | Management KPI Cards & Metrics Summary |
|
||||||
|
| **`education_portal`** | Self-Service Web & Login | `education.announcement`, `education.payment.transaction` | Student Portal `/my/education`, Razorpay Payment & Split-Screen Login Page |
|
||||||
|
| **`education_reports`** | PDF & Excel Exports | `education.report.wizard` | PDF Student ID Cards, Fee Receipts & Ledger Exports |
|
||||||
|
| **`education_exam`** | Examination Management | `education.exam`, `education.exam.result`, `education.subject`, `education.grading.system` | Exam Schedule, Grading Views, Student Portal Results |
|
||||||
|
| **`education_lms`** | Learning Management System | `education.lms.course`, `education.lms.assignment`, `education.lms.submission` | Course Views, Submissions, Portal LMS Templates |
|
||||||
|
| **`education_timetable`** | Academic Timetable | `education.room`, `education.timetable.slot` | Room Management, Timetable Slots |
|
||||||
|
| **`education_library`** | Library Management | `education.library.book`, `education.library.member`, `education.library.issue` | Book Catalog, Issues & Returns, Library Fine Ledger |
|
||||||
69
ERP_SYSTEM_WORKFLOW.md
Normal file
69
ERP_SYSTEM_WORKFLOW.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# 🔄 Odoo Education ERP: Complete End-to-End Workflow
|
||||||
|
|
||||||
|
This document outlines the operational lifecycle and workflow of a student moving through the custom Odoo Education ERP system, demonstrating how all 11 modules interact seamlessly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Phase 1: Institutional Setup (Admin/Management)
|
||||||
|
**Module Used:** `education_base`, `education_finance`
|
||||||
|
|
||||||
|
Before any students can be admitted, the institutional hierarchy must be configured:
|
||||||
|
1. **Create Infrastructure:** Define `Campuses` ➡️ `Departments` ➡️ `Programs` (e.g., Main Campus ➡️ Computer Science ➡️ B.Tech).
|
||||||
|
2. **Set Academic Timeline:** Create an `Academic Year` (2026-2027) and divide it into `Academic Terms` (Semester 1, Semester 2).
|
||||||
|
3. **Configure Finances:** Define `Fee Categories` (Tuition, Library) and bundle them into `Fee Structures` tied to specific Programs and Terms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👨🎓 Phase 2: Admission & Enrollment (Admissions Office)
|
||||||
|
**Module Used:** `education_student`, `education_portal`
|
||||||
|
|
||||||
|
1. **Student Registration:** A new `education.student` profile is created (Draft state).
|
||||||
|
2. **Academic Placement:** The student is assigned to a `Campus`, `Program`, `Batch`, `Semester`, and `Section`.
|
||||||
|
3. **Document Verification:** Identity and academic documents (Aadhaar, Transfer Certificates) are uploaded.
|
||||||
|
4. **Activation:** The student's status transitions: `Draft` ➡️ `Admitted` ➡️ `Active`.
|
||||||
|
5. **Portal Provisioning:** An Odoo `res.users` account is automatically generated for the student to access the self-service web portal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 Phase 3: Financial Assessment & Payment (Finance Dept & Student)
|
||||||
|
**Module Used:** `education_finance`, `education_portal`
|
||||||
|
|
||||||
|
1. **Fee Assignment:** An `education.fee.assignment` is generated for the student based on their Program's Fee Structure. The student's `education.student.ledger` is debited.
|
||||||
|
2. **Student Portal View:** The student logs into the Portal (`/my/education`) and sees their pending fee balance.
|
||||||
|
3. **Payment Collection:**
|
||||||
|
- **Online:** The student pays via Razorpay directly on the Portal.
|
||||||
|
- **Offline:** The Finance Office manually posts an `education.student.payment` (Cash/Bank).
|
||||||
|
4. **Ledger Reconciliation:** The payment credits the student's ledger, bringing the outstanding balance to zero. A PDF Fee Receipt is generated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Phase 4: Daily Academics & Library (Faculty & Librarian)
|
||||||
|
**Modules Used:** `education_attendance`, `education_timetable`, `education_library`, `education_lms`
|
||||||
|
|
||||||
|
1. **Timetable Generation:** Classes are scheduled in `education.room` slots without overlapping faculty or student groups.
|
||||||
|
2. **Attendance Tracking:** Faculty creates an `Attendance Session` for a lecture and marks students as Present, Absent, or Late. The student's overall attendance % updates instantly.
|
||||||
|
3. **LMS & Assignments:** Faculty upload `Courses` and `Materials` to the LMS. They create `Assignments` which students submit through the Portal.
|
||||||
|
4. **Library Operations:**
|
||||||
|
- The student visits the library.
|
||||||
|
- The Librarian creates an `education.library.member` profile for them.
|
||||||
|
- The student is issued a physical `education.library.book.copy`.
|
||||||
|
- If returned late, an `education.library.fine` is generated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Phase 5: Examinations & Results (Examination Controller)
|
||||||
|
**Module Used:** `education_exam`
|
||||||
|
|
||||||
|
1. **Exam Scheduling:** An `education.exam` is scheduled with specific dates for each subject.
|
||||||
|
2. **Mark Entry:** Faculty enters the raw marks into the `education.exam.result.line` for each student.
|
||||||
|
3. **Grade Calculation:** The system maps the raw marks to the configured `education.grading.system` (e.g., 90-100 = A+).
|
||||||
|
4. **Publication:** The results are marked as `Published` and instantly become visible on the Student Portal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 Phase 6: Graduation & Alumni (Management)
|
||||||
|
**Module Used:** `education_student`
|
||||||
|
|
||||||
|
1. **Final Clearance:** The system checks the student's Ledger (must have ₹0 dues) and Library profile (must have 0 unreturned books).
|
||||||
|
2. **Graduation:** The student's status is transitioned to `Graduated`.
|
||||||
|
3. **Portal Archiving:** The student's portal access is restricted to viewing historical academic records and transcripts.
|
||||||
40
ODOO19_DEVELOPMENT_GUIDELINES.md
Normal file
40
ODOO19_DEVELOPMENT_GUIDELINES.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# 🚀 Odoo 19 Custom Module Development Guidelines
|
||||||
|
|
||||||
|
This document summarizes the coding standards, strict constraints, and Odoo 19 specific architectural changes discovered and enforced across the `student_ERP_Metatron` modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 🖥️ User Interface & Views
|
||||||
|
- **Tree Views Deprecated**: The `<tree>` XML tag and `tree` view mode have been completely replaced by `<list>`.
|
||||||
|
- **Action Window Definitions**: You must define `view_mode` as `list,form` instead of `tree,form`.
|
||||||
|
- **View Architectures**: Use `<list>` tags instead of `<tree>` when defining list view layouts.
|
||||||
|
|
||||||
|
## 2. 🗄️ Database & ORM Constraints
|
||||||
|
- **SQL Constraints Deprecated**: The `_sql_constraints` list attribute is no longer supported.
|
||||||
|
- **Odoo 19 Standard**: Define SQL constraints using `models.Constraint` at the class level.
|
||||||
|
- **Example**:
|
||||||
|
```python
|
||||||
|
_code_unique = models.Constraint('unique(code)', 'The Code must be unique!')
|
||||||
|
```
|
||||||
|
- **Strict Validation**: Models like `education.student` have strict database-level `NotNullViolation` constraints for academic fields (`batch_id`, `semester_id`, `section_id`). These must always be populated when creating records, especially during automated testing.
|
||||||
|
|
||||||
|
## 3. 🔐 Security & Access Rights
|
||||||
|
- **Group Fields**: When assigning groups to `res.users` or defining access, note that the underlying model for groups uses `res.groups.privilege`.
|
||||||
|
- **Field Name Changes**: When creating or writing to `res.users`, the relational field for groups has been renamed from `groups_id` to `group_ids`.
|
||||||
|
- **Portal Controllers**: Portal users lack access to standard internal models (like `ir.sequence`). Sequence generation triggered from portal controllers (e.g., creating a library issue) must be executed using `.sudo()`.
|
||||||
|
|
||||||
|
## 4. 🌐 Web Controllers & Translation
|
||||||
|
- **Translation Function `_()`**: The standard translation function `_()` attempts to retrieve the user's language by accessing `self.env`.
|
||||||
|
- **The Issue**: In HTTP controllers, `self` refers to the Controller instance, not an ORM Model. Calling `_("Error")` inside a controller (e.g., within a `raise AccessError(...)`) throws `AttributeError: 'NoneType' object has no attribute 'uid'`.
|
||||||
|
- **The Solution**: Avoid wrapping error strings in `_()` within controllers unless explicitly using `request.env._()`.
|
||||||
|
|
||||||
|
## 5. 🧪 Automated Testing
|
||||||
|
- **Buffered ORM Operations**: Odoo 19 heavily buffers database inserts and updates.
|
||||||
|
- **Testing Constraints**: When testing database-level exceptions (like `UniqueViolation`), the error will not trigger immediately upon calling `.create()`. You must call `self.env.flush_all()` within the `assertRaises` block to force the ORM to flush the transaction to PostgreSQL and trigger the exception.
|
||||||
|
- **Example**:
|
||||||
|
```python
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
self.env['my.model'].create({'unique_field': 'duplicate'})
|
||||||
|
self.env.flush_all() # Crucial for triggering the DB error!
|
||||||
|
```
|
||||||
|
- **Constraint Exceptions**: Odoo 19 often raises raw database exceptions (`Exception` / `psycopg2` errors) rather than cleanly wrapped `ValidationError` objects for database-level violations. Adjust assertions accordingly.
|
||||||
BIN
addons/.DS_Store
vendored
Normal file
BIN
addons/.DS_Store
vendored
Normal file
Binary file not shown.
1
addons/education_attendance/__init__.py
Normal file
1
addons/education_attendance/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
37
addons/education_attendance/__manifest__.py
Normal file
37
addons/education_attendance/__manifest__.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
'name': 'Education Attendance',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Education',
|
||||||
|
'summary': 'Manage Daily and Period-wise Student Attendance',
|
||||||
|
'description': """
|
||||||
|
Attendance Management System for Educational Institutions.
|
||||||
|
Supports:
|
||||||
|
- Daily & Period-wise Attendance
|
||||||
|
- Attendance Sessions
|
||||||
|
- Bulk marking status updates
|
||||||
|
- Multi-company and Multi-campus isolation
|
||||||
|
- Interactive dashboards, pivot tables, and PDF reporting register
|
||||||
|
""",
|
||||||
|
'author': 'Senior Solution Architect',
|
||||||
|
'depends': [
|
||||||
|
'education_base',
|
||||||
|
'education_student',
|
||||||
|
'mail',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/education_attendance_security.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/attendance_session_views.xml',
|
||||||
|
'views/attendance_views.xml',
|
||||||
|
'views/student_views_inherit.xml',
|
||||||
|
'views/attendance_menus.xml',
|
||||||
|
'reports/attendance_reports.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'data/education_attendance_demo.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
BIN
addons/education_attendance/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/education_attendance/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
addons/education_attendance/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/education_attendance/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
440
addons/education_attendance/data/education_attendance_demo.xml
Normal file
440
addons/education_attendance/data/education_attendance_demo.xml
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Demo Campus -->
|
||||||
|
<record id="demo_attendance_campus" model="education.campus">
|
||||||
|
<field name="name">Attendance Demo Campus</field>
|
||||||
|
<field name="code">ADC</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Department -->
|
||||||
|
<record id="demo_attendance_dept" model="education.department">
|
||||||
|
<field name="name">Attendance Demo Department</field>
|
||||||
|
<field name="code">ADD</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Level -->
|
||||||
|
<record id="demo_attendance_level" model="education.academic.level">
|
||||||
|
<field name="name">Attendance Demo Level</field>
|
||||||
|
<field name="code">ADL</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Program -->
|
||||||
|
<record id="demo_attendance_program" model="education.program">
|
||||||
|
<field name="name">Attendance Demo Program</field>
|
||||||
|
<field name="code">ADP</field>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Academic Year -->
|
||||||
|
<record id="demo_attendance_year" model="education.academic.year">
|
||||||
|
<field name="name">Attendance Demo 2026</field>
|
||||||
|
<field name="code">ADY26</field>
|
||||||
|
<field name="start_date">2026-01-01</field>
|
||||||
|
<field name="end_date">2026-12-31</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Academic Term -->
|
||||||
|
<record id="demo_attendance_term" model="education.academic.term">
|
||||||
|
<field name="name">Attendance Demo Term 1</field>
|
||||||
|
<field name="code">ADT1</field>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="start_date">2026-01-01</field>
|
||||||
|
<field name="end_date">2026-06-30</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Academic Batch -->
|
||||||
|
<record id="demo_attendance_batch" model="education.batch">
|
||||||
|
<field name="name">Attendance Batch 2026</field>
|
||||||
|
<field name="code">ADB26</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Academic Semester -->
|
||||||
|
<record id="demo_attendance_semester" model="education.semester">
|
||||||
|
<field name="name">Attendance Semester 1</field>
|
||||||
|
<field name="code">ADS1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Academic Section -->
|
||||||
|
<record id="demo_attendance_section" model="education.section">
|
||||||
|
<field name="name">Attendance Section A</field>
|
||||||
|
<field name="code">ADSA</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Create 20 Demo Students -->
|
||||||
|
<record id="demo_student_1" model="education.student">
|
||||||
|
<field name="first_name">Alex</field>
|
||||||
|
<field name="last_name">Smith</field>
|
||||||
|
<field name="student_id">STU001</field>
|
||||||
|
<field name="admission_number">ADM001</field>
|
||||||
|
<field name="roll_number">RL001</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_2" model="education.student">
|
||||||
|
<field name="first_name">Bella</field>
|
||||||
|
<field name="last_name">Jones</field>
|
||||||
|
<field name="student_id">STU002</field>
|
||||||
|
<field name="admission_number">ADM002</field>
|
||||||
|
<field name="roll_number">RL002</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_3" model="education.student">
|
||||||
|
<field name="first_name">Charlie</field>
|
||||||
|
<field name="last_name">Brown</field>
|
||||||
|
<field name="student_id">STU003</field>
|
||||||
|
<field name="admission_number">ADM003</field>
|
||||||
|
<field name="roll_number">RL003</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_4" model="education.student">
|
||||||
|
<field name="first_name">Diana</field>
|
||||||
|
<field name="last_name">Prince</field>
|
||||||
|
<field name="student_id">STU004</field>
|
||||||
|
<field name="admission_number">ADM004</field>
|
||||||
|
<field name="roll_number">RL004</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_5" model="education.student">
|
||||||
|
<field name="first_name">Evan</field>
|
||||||
|
<field name="last_name">Wright</field>
|
||||||
|
<field name="student_id">STU005</field>
|
||||||
|
<field name="admission_number">ADM005</field>
|
||||||
|
<field name="roll_number">RL005</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_6" model="education.student">
|
||||||
|
<field name="first_name">Fiona</field>
|
||||||
|
<field name="last_name">Gallagher</field>
|
||||||
|
<field name="student_id">STU006</field>
|
||||||
|
<field name="admission_number">ADM006</field>
|
||||||
|
<field name="roll_number">RL006</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_7" model="education.student">
|
||||||
|
<field name="first_name">George</field>
|
||||||
|
<field name="last_name">Clooney</field>
|
||||||
|
<field name="student_id">STU007</field>
|
||||||
|
<field name="admission_number">ADM007</field>
|
||||||
|
<field name="roll_number">RL007</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_8" model="education.student">
|
||||||
|
<field name="first_name">Hannah</field>
|
||||||
|
<field name="last_name">Baker</field>
|
||||||
|
<field name="student_id">STU008</field>
|
||||||
|
<field name="admission_number">ADM008</field>
|
||||||
|
<field name="roll_number">RL008</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_9" model="education.student">
|
||||||
|
<field name="first_name">Ian</field>
|
||||||
|
<field name="last_name">Somerhalder</field>
|
||||||
|
<field name="student_id">STU009</field>
|
||||||
|
<field name="admission_number">ADM009</field>
|
||||||
|
<field name="roll_number">RL009</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_10" model="education.student">
|
||||||
|
<field name="first_name">Julia</field>
|
||||||
|
<field name="last_name">Roberts</field>
|
||||||
|
<field name="student_id">STU010</field>
|
||||||
|
<field name="admission_number">ADM010</field>
|
||||||
|
<field name="roll_number">RL010</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_11" model="education.student">
|
||||||
|
<field name="first_name">Kevin</field>
|
||||||
|
<field name="last_name">Hart</field>
|
||||||
|
<field name="student_id">STU011</field>
|
||||||
|
<field name="admission_number">ADM011</field>
|
||||||
|
<field name="roll_number">RL011</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_12" model="education.student">
|
||||||
|
<field name="first_name">Lily</field>
|
||||||
|
<field name="last_name">Collins</field>
|
||||||
|
<field name="student_id">STU012</field>
|
||||||
|
<field name="admission_number">ADM012</field>
|
||||||
|
<field name="roll_number">RL012</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_13" model="education.student">
|
||||||
|
<field name="first_name">Mike</field>
|
||||||
|
<field name="last_name">Tyson</field>
|
||||||
|
<field name="student_id">STU013</field>
|
||||||
|
<field name="admission_number">ADM013</field>
|
||||||
|
<field name="roll_number">RL013</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_14" model="education.student">
|
||||||
|
<field name="first_name">Nina</field>
|
||||||
|
<field name="last_name">Dobrev</field>
|
||||||
|
<field name="student_id">STU014</field>
|
||||||
|
<field name="admission_number">ADM014</field>
|
||||||
|
<field name="roll_number">RL014</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_15" model="education.student">
|
||||||
|
<field name="first_name">Oscar</field>
|
||||||
|
<field name="last_name">Isaac</field>
|
||||||
|
<field name="student_id">STU015</field>
|
||||||
|
<field name="admission_number">ADM015</field>
|
||||||
|
<field name="roll_number">RL015</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_16" model="education.student">
|
||||||
|
<field name="first_name">Penelope</field>
|
||||||
|
<field name="last_name">Cruz</field>
|
||||||
|
<field name="student_id">STU016</field>
|
||||||
|
<field name="admission_number">ADM016</field>
|
||||||
|
<field name="roll_number">RL016</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_17" model="education.student">
|
||||||
|
<field name="first_name">Quincy</field>
|
||||||
|
<field name="last_name">Jones</field>
|
||||||
|
<field name="student_id">STU017</field>
|
||||||
|
<field name="admission_number">ADM017</field>
|
||||||
|
<field name="roll_number">RL017</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_18" model="education.student">
|
||||||
|
<field name="first_name">Rachel</field>
|
||||||
|
<field name="last_name">Green</field>
|
||||||
|
<field name="student_id">STU018</field>
|
||||||
|
<field name="admission_number">ADM018</field>
|
||||||
|
<field name="roll_number">RL018</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_19" model="education.student">
|
||||||
|
<field name="first_name">Sam</field>
|
||||||
|
<field name="last_name">Winchester</field>
|
||||||
|
<field name="student_id">STU019</field>
|
||||||
|
<field name="admission_number">ADM019</field>
|
||||||
|
<field name="roll_number">RL019</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_student_20" model="education.student">
|
||||||
|
<field name="first_name">Tom</field>
|
||||||
|
<field name="last_name">Hanks</field>
|
||||||
|
<field name="student_id">STU020</field>
|
||||||
|
<field name="admission_number">ADM020</field>
|
||||||
|
<field name="roll_number">RL020</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="batch_id" ref="demo_attendance_batch"/>
|
||||||
|
<field name="semester_id" ref="demo_attendance_semester"/>
|
||||||
|
<field name="section_id" ref="demo_attendance_section"/>
|
||||||
|
<field name="status">active</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Create 1 Demo Attendance Session -->
|
||||||
|
<record id="demo_attendance_session_1" model="education.attendance.session">
|
||||||
|
<field name="name">Demo Session MCA Period 1</field>
|
||||||
|
<field name="campus_id" ref="demo_attendance_campus"/>
|
||||||
|
<field name="department_id" ref="demo_attendance_dept"/>
|
||||||
|
<field name="program_id" ref="demo_attendance_program"/>
|
||||||
|
<field name="academic_level_id" ref="demo_attendance_level"/>
|
||||||
|
<field name="academic_year_id" ref="demo_attendance_year"/>
|
||||||
|
<field name="academic_term_id" ref="demo_attendance_term"/>
|
||||||
|
<field name="attendance_date" eval="time.strftime('%Y-%m-%d')"/>
|
||||||
|
<field name="period">period_1</field>
|
||||||
|
<field name="faculty_id" ref="base.user_admin"/>
|
||||||
|
<field name="state">draft</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
4
addons/education_attendance/models/__init__.py
Normal file
4
addons/education_attendance/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import attendance_session
|
||||||
|
from . import attendance_line
|
||||||
|
from . import attendance
|
||||||
|
from . import student_inherit
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
29
addons/education_attendance/models/attendance.py
Normal file
29
addons/education_attendance/models/attendance.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EducationAttendance(models.Model):
|
||||||
|
_name = 'education.attendance'
|
||||||
|
_description = 'Student Attendance History'
|
||||||
|
_order = 'date desc, id desc'
|
||||||
|
|
||||||
|
student_id = fields.Many2one('education.student', string='Student', required=True, index=True, ondelete='cascade')
|
||||||
|
date = fields.Date(string='Date', required=True, index=True, default=fields.Date.context_today)
|
||||||
|
session_id = fields.Many2one('education.attendance.session', string='Session', ondelete='set null', index=True)
|
||||||
|
status = fields.Selection([
|
||||||
|
('present', 'Present'),
|
||||||
|
('absent', 'Absent'),
|
||||||
|
('late', 'Late'),
|
||||||
|
('medical_leave', 'Medical Leave'),
|
||||||
|
('on_duty', 'On Duty')
|
||||||
|
], string='Status', required=True, index=True)
|
||||||
|
faculty_id = fields.Many2one('res.users', string='Faculty', required=True, index=True)
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
string='Company',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
28
addons/education_attendance/models/attendance_line.py
Normal file
28
addons/education_attendance/models/attendance_line.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EducationAttendanceLine(models.Model):
|
||||||
|
_name = 'education.attendance.line'
|
||||||
|
_description = 'Attendance Line Detail'
|
||||||
|
_order = 'id asc'
|
||||||
|
|
||||||
|
session_id = fields.Many2one('education.attendance.session', string='Session', required=True, ondelete='cascade', index=True)
|
||||||
|
student_id = fields.Many2one('education.student', string='Student', required=True, index=True)
|
||||||
|
status = fields.Selection([
|
||||||
|
('present', 'Present'),
|
||||||
|
('absent', 'Absent'),
|
||||||
|
('late', 'Late'),
|
||||||
|
('medical_leave', 'Medical Leave'),
|
||||||
|
('on_duty', 'On Duty')
|
||||||
|
], string='Status', default='present', required=True, index=True)
|
||||||
|
remarks = fields.Char(string='Remarks')
|
||||||
|
time_in = fields.Datetime(string='Time In')
|
||||||
|
time_out = fields.Datetime(string='Time Out')
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
related='session_id.company_id',
|
||||||
|
store=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
159
addons/education_attendance/models/attendance_session.py
Normal file
159
addons/education_attendance/models/attendance_session.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo import api, fields, models
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class EducationAttendanceSession(models.Model):
|
||||||
|
_name = 'education.attendance.session'
|
||||||
|
_description = 'Attendance Session'
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_order = 'attendance_date desc, period asc, id desc'
|
||||||
|
|
||||||
|
name = fields.Char(string='Session Title', required=True, tracking=True)
|
||||||
|
campus_id = fields.Many2one('education.campus', string='Campus', required=True, index=True)
|
||||||
|
department_id = fields.Many2one('education.department', string='Department', required=True, index=True)
|
||||||
|
program_id = fields.Many2one('education.program', string='Program', required=True, index=True)
|
||||||
|
academic_year_id = fields.Many2one('education.academic.year', string='Academic Year', required=True, index=True)
|
||||||
|
academic_term_id = fields.Many2one('education.academic.term', string='Academic Term', required=True, index=True)
|
||||||
|
academic_level_id = fields.Many2one('education.academic.level', string='Academic Level', required=True, index=True)
|
||||||
|
|
||||||
|
attendance_date = fields.Date(string='Attendance Date', required=True, default=fields.Date.context_today, index=True, tracking=True)
|
||||||
|
period = fields.Selection([
|
||||||
|
('period_1', 'Period 1'),
|
||||||
|
('period_2', 'Period 2'),
|
||||||
|
('period_3', 'Period 3'),
|
||||||
|
('period_4', 'Period 4'),
|
||||||
|
('period_5', 'Period 5'),
|
||||||
|
('period_6', 'Period 6'),
|
||||||
|
('period_7', 'Period 7'),
|
||||||
|
('period_8', 'Period 8')
|
||||||
|
], string='Period', required=True, default='period_1', tracking=True)
|
||||||
|
faculty_id = fields.Many2one('res.users', string='Faculty', required=True, index=True, default=lambda self: self.env.user, tracking=True)
|
||||||
|
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('open', 'Open'),
|
||||||
|
('completed', 'Completed'),
|
||||||
|
('cancelled', 'Cancelled')
|
||||||
|
], string='Status', default='draft', required=True, index=True, tracking=True)
|
||||||
|
|
||||||
|
line_ids = fields.One2many('education.attendance.line', 'session_id', string='Attendance Lines', copy=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
string='Company',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_uniq_session_period = models.Constraint('unique(attendance_date, period, program_id, academic_level_id, company_id)', 'An attendance session has already been created for this period, program, and academic level on this date.')
|
||||||
|
|
||||||
|
def action_open(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state != 'draft':
|
||||||
|
continue
|
||||||
|
# Auto-populate attendance lines with active students
|
||||||
|
students = self.env['education.student'].search([
|
||||||
|
('campus_id', '=', rec.campus_id.id),
|
||||||
|
('department_id', '=', rec.department_id.id),
|
||||||
|
('program_id', '=', rec.program_id.id),
|
||||||
|
('academic_year_id', '=', rec.academic_year_id.id),
|
||||||
|
('academic_term_id', '=', rec.academic_term_id.id),
|
||||||
|
('level_id', '=', rec.academic_level_id.id),
|
||||||
|
('status', '=', 'active'),
|
||||||
|
])
|
||||||
|
if not students:
|
||||||
|
raise ValidationError("No active students found matching the selected academic criteria.")
|
||||||
|
|
||||||
|
# Create lines
|
||||||
|
lines_vals = []
|
||||||
|
for student in students:
|
||||||
|
lines_vals.append({
|
||||||
|
'session_id': rec.id,
|
||||||
|
'student_id': student.id,
|
||||||
|
'status': 'present',
|
||||||
|
'company_id': rec.company_id.id,
|
||||||
|
})
|
||||||
|
self.env['education.attendance.line'].create(lines_vals)
|
||||||
|
rec.state = 'open'
|
||||||
|
|
||||||
|
def action_complete(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state != 'open':
|
||||||
|
continue
|
||||||
|
if not rec.line_ids:
|
||||||
|
raise ValidationError("Cannot complete session without any student lines.")
|
||||||
|
|
||||||
|
# Copy to permanent history table
|
||||||
|
history_vals = []
|
||||||
|
for line in rec.line_ids:
|
||||||
|
history_vals.append({
|
||||||
|
'student_id': line.student_id.id,
|
||||||
|
'date': rec.attendance_date,
|
||||||
|
'session_id': rec.id,
|
||||||
|
'status': line.status,
|
||||||
|
'faculty_id': rec.faculty_id.id,
|
||||||
|
'remarks': line.remarks,
|
||||||
|
'company_id': rec.company_id.id,
|
||||||
|
})
|
||||||
|
self.env['education.attendance'].create(history_vals)
|
||||||
|
rec.state = 'completed'
|
||||||
|
|
||||||
|
# Invalidate student attendance cache
|
||||||
|
students = rec.line_ids.mapped('student_id')
|
||||||
|
students.invalidate_recordset()
|
||||||
|
|
||||||
|
def action_cancel(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state == 'completed':
|
||||||
|
# Delete permanent history
|
||||||
|
history = self.env['education.attendance'].search([('session_id', '=', rec.id)])
|
||||||
|
history.unlink()
|
||||||
|
|
||||||
|
# Delete lines
|
||||||
|
rec.line_ids.unlink()
|
||||||
|
rec.state = 'cancelled'
|
||||||
|
|
||||||
|
def action_draft(self):
|
||||||
|
for rec in self:
|
||||||
|
# Delete permanent history if completed
|
||||||
|
if rec.state == 'completed':
|
||||||
|
history = self.env['education.attendance'].search([('session_id', '=', rec.id)])
|
||||||
|
history.unlink()
|
||||||
|
|
||||||
|
# Delete lines
|
||||||
|
rec.line_ids.unlink()
|
||||||
|
rec.state = 'draft'
|
||||||
|
|
||||||
|
# Bulk marking actions
|
||||||
|
def action_bulk_present(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'open':
|
||||||
|
raise ValidationError("Bulk marking is only allowed when the session is Open.")
|
||||||
|
self.line_ids.write({'status': 'present'})
|
||||||
|
|
||||||
|
def action_bulk_absent(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'open':
|
||||||
|
raise ValidationError("Bulk marking is only allowed when the session is Open.")
|
||||||
|
self.line_ids.write({'status': 'absent'})
|
||||||
|
|
||||||
|
def action_bulk_late(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'open':
|
||||||
|
raise ValidationError("Bulk marking is only allowed when the session is Open.")
|
||||||
|
self.line_ids.write({'status': 'late'})
|
||||||
|
|
||||||
|
def action_bulk_medical(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'open':
|
||||||
|
raise ValidationError("Bulk marking is only allowed when the session is Open.")
|
||||||
|
self.line_ids.write({'status': 'medical_leave'})
|
||||||
|
|
||||||
|
def action_bulk_duty(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'open':
|
||||||
|
raise ValidationError("Bulk marking is only allowed when the session is Open.")
|
||||||
|
self.line_ids.write({'status': 'on_duty'})
|
||||||
33
addons/education_attendance/models/student_inherit.py
Normal file
33
addons/education_attendance/models/student_inherit.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EducationStudent(models.Model):
|
||||||
|
_inherit = 'education.student'
|
||||||
|
|
||||||
|
attendance_line_ids = fields.One2many('education.attendance', 'student_id', string='Attendance History')
|
||||||
|
attendance_pct = fields.Float(string='Attendance %', compute='_compute_attendance_metrics', store=True, index=True)
|
||||||
|
present_count = fields.Integer(string='Present Count', compute='_compute_attendance_metrics', store=True)
|
||||||
|
absent_count = fields.Integer(string='Absent Count', compute='_compute_attendance_metrics', store=True)
|
||||||
|
late_count = fields.Integer(string='Late Count', compute='_compute_attendance_metrics', store=True)
|
||||||
|
medical_leave_count = fields.Integer(string='Medical Leave Count', compute='_compute_attendance_metrics', store=True)
|
||||||
|
|
||||||
|
@api.depends('attendance_line_ids.status', 'attendance_line_ids.active')
|
||||||
|
def _compute_attendance_metrics(self):
|
||||||
|
for rec in self:
|
||||||
|
records = rec.attendance_line_ids.filtered(lambda a: a.active)
|
||||||
|
total = len(records)
|
||||||
|
|
||||||
|
p_count = len(records.filtered(lambda r: r.status in ('present', 'on_duty')))
|
||||||
|
l_count = len(records.filtered(lambda r: r.status == 'late'))
|
||||||
|
m_count = len(records.filtered(lambda r: r.status == 'medical_leave'))
|
||||||
|
a_count = len(records.filtered(lambda r: r.status == 'absent'))
|
||||||
|
|
||||||
|
rec.present_count = p_count
|
||||||
|
rec.absent_count = a_count
|
||||||
|
rec.late_count = l_count
|
||||||
|
rec.medical_leave_count = m_count
|
||||||
|
|
||||||
|
# Treat present, late, medical_leave, and on_duty as attended (positive)
|
||||||
|
attended = p_count + l_count + m_count
|
||||||
|
rec.attendance_pct = (attended / total * 100.0) if total > 0 else 100.0
|
||||||
138
addons/education_attendance/reports/attendance_reports.xml
Normal file
138
addons/education_attendance/reports/attendance_reports.xml
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Daily Attendance Report Action -->
|
||||||
|
<record id="action_report_daily_attendance" model="ir.actions.report">
|
||||||
|
<field name="name">Daily Attendance Register</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="report_type">qweb-pdf</field>
|
||||||
|
<field name="report_name">education_attendance.report_daily_attendance_template</field>
|
||||||
|
<field name="report_file">education_attendance.report_daily_attendance_template</field>
|
||||||
|
<field name="binding_model_id" ref="model_education_attendance_session"/>
|
||||||
|
<field name="binding_type">report</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Student Attendance History Report Action -->
|
||||||
|
<record id="action_report_student_attendance" model="ir.actions.report">
|
||||||
|
<field name="name">Student Attendance Report</field>
|
||||||
|
<field name="model">education.student</field>
|
||||||
|
<field name="report_type">qweb-pdf</field>
|
||||||
|
<field name="report_name">education_attendance.report_student_attendance_template</field>
|
||||||
|
<field name="report_file">education_attendance.report_student_attendance_template</field>
|
||||||
|
<field name="binding_model_id" ref="model_education_student"/>
|
||||||
|
<field name="binding_type">report</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Daily Attendance Template -->
|
||||||
|
<template id="report_daily_attendance_template">
|
||||||
|
<t t-call="web.html_container">
|
||||||
|
<t t-foreach="docs" t-as="o">
|
||||||
|
<t t-call="web.external_layout">
|
||||||
|
<div class="page">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2>DAILY ATTENDANCE REGISTER</h2>
|
||||||
|
<h4 t-field="o.name"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-6">
|
||||||
|
<strong>Academic Details:</strong>
|
||||||
|
<div>Campus: <span t-field="o.campus_id.name"/></div>
|
||||||
|
<div>Dept: <span t-field="o.department_id.name"/></div>
|
||||||
|
<div>Program: <span t-field="o.program_id.name"/></div>
|
||||||
|
<div>Level: <span t-field="o.academic_level_id.name"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-right">
|
||||||
|
<strong>Session Details:</strong>
|
||||||
|
<div>Date: <span t-field="o.attendance_date"/></div>
|
||||||
|
<div>Period: <span t-field="o.period"/></div>
|
||||||
|
<div>Faculty: <span t-field="o.faculty_id.name"/></div>
|
||||||
|
<div>Status: <span t-field="o.state"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-striped table-bordered mt-3">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Roll No</th>
|
||||||
|
<th>Student Name</th>
|
||||||
|
<th>Attendance Status</th>
|
||||||
|
<th>Time In</th>
|
||||||
|
<th>Remarks</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-foreach="o.line_ids" t-as="line">
|
||||||
|
<tr>
|
||||||
|
<td><span t-field="line.student_id.roll_number"/></td>
|
||||||
|
<td><span t-field="line.student_id.full_name"/></td>
|
||||||
|
<td>
|
||||||
|
<span t-field="line.status"/>
|
||||||
|
</td>
|
||||||
|
<td><span t-field="line.time_in" t-options='{"widget": "datetime"}'/></td>
|
||||||
|
<td><span t-field="line.remarks"/></td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Student Attendance Template -->
|
||||||
|
<template id="report_student_attendance_template">
|
||||||
|
<t t-call="web.html_container">
|
||||||
|
<t t-foreach="docs" t-as="o">
|
||||||
|
<t t-call="web.external_layout">
|
||||||
|
<div class="page">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2>STUDENT ATTENDANCE REPORT</h2>
|
||||||
|
<h4 t-field="o.full_name"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-6">
|
||||||
|
<strong>Student Profile:</strong>
|
||||||
|
<div>Student ID: <span t-field="o.student_id"/></div>
|
||||||
|
<div>Roll No: <span t-field="o.roll_number"/></div>
|
||||||
|
<div>Program: <span t-field="o.program_id.name"/></div>
|
||||||
|
<div>Campus: <span t-field="o.campus_id.name"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-right">
|
||||||
|
<strong>Attendance Summary:</strong>
|
||||||
|
<div>Present Count: <span t-field="o.present_count"/></div>
|
||||||
|
<div>Absent Count: <span t-field="o.absent_count"/></div>
|
||||||
|
<div>Late Count: <span t-field="o.late_count"/></div>
|
||||||
|
<div>Overall %: <strong><span t-field="o.attendance_pct"/>%</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-striped table-bordered mt-3">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Period / Session</th>
|
||||||
|
<th>Faculty</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Remarks</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-foreach="o.attendance_line_ids" t-as="line">
|
||||||
|
<tr>
|
||||||
|
<td><span t-field="line.date"/></td>
|
||||||
|
<td><span t-field="line.session_id.name"/></td>
|
||||||
|
<td><span t-field="line.faculty_id.name"/></td>
|
||||||
|
<td><span t-field="line.status"/></td>
|
||||||
|
<td><span t-field="line.remarks"/></td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Category -->
|
||||||
|
<record id="module_category_education_attendance" model="ir.module.category">
|
||||||
|
<field name="name">Education Attendance</field>
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Groups -->
|
||||||
|
<record id="group_attendance_user" model="res.groups">
|
||||||
|
<field name="name">Attendance User</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="comment">Allows marking attendance lines and tracking their sessions.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_attendance_manager" model="res.groups">
|
||||||
|
<field name="name">Attendance Manager</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_attendance_user'))]"/>
|
||||||
|
<field name="comment">Full managerial oversight, configuration, and reports.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Multi-Company Isolation -->
|
||||||
|
<record id="rule_attendance_session_company" model="ir.rule">
|
||||||
|
<field name="name">Attendance Session Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_attendance_session"/>
|
||||||
|
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', '=', user.company_id.id)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_attendance_line_company" model="ir.rule">
|
||||||
|
<field name="name">Attendance Line Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_attendance_line"/>
|
||||||
|
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', '=', user.company_id.id)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_attendance_history_company" model="ir.rule">
|
||||||
|
<field name="name">Attendance History Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_attendance"/>
|
||||||
|
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', '=', user.company_id.id)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Faculty Only Sessions Rule (Attendance Users see their own, Managers see all) -->
|
||||||
|
<record id="rule_attendance_session_faculty" model="ir.rule">
|
||||||
|
<field name="name">Faculty Sessions Only</field>
|
||||||
|
<field name="model_id" ref="model_education_attendance_session"/>
|
||||||
|
<field name="domain_force">[('faculty_id', '=', user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_attendance_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_attendance_session_manager" model="ir.rule">
|
||||||
|
<field name="name">Manager All Sessions</field>
|
||||||
|
<field name="model_id" ref="model_education_attendance_session"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_attendance_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
7
addons/education_attendance/security/ir.model.access.csv
Normal file
7
addons/education_attendance/security/ir.model.access.csv
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_attendance_session_user,Attendance Session User,model_education_attendance_session,group_attendance_user,1,1,1,0
|
||||||
|
access_attendance_session_mgr,Attendance Session Mgr,model_education_attendance_session,group_attendance_manager,1,1,1,1
|
||||||
|
access_attendance_line_user,Attendance Line User,model_education_attendance_line,group_attendance_user,1,1,1,0
|
||||||
|
access_attendance_line_mgr,Attendance Line Mgr,model_education_attendance_line,group_attendance_manager,1,1,1,1
|
||||||
|
access_attendance_user,Attendance User,model_education_attendance,group_attendance_user,1,1,1,0
|
||||||
|
access_attendance_mgr,Attendance Mgr,model_education_attendance,group_attendance_manager,1,1,1,1
|
||||||
|
1
addons/education_attendance/tests/__init__.py
Normal file
1
addons/education_attendance/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import test_attendance
|
||||||
Binary file not shown.
Binary file not shown.
205
addons/education_attendance/tests/test_attendance.py
Normal file
205
addons/education_attendance/tests/test_attendance.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestStudentAttendance(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestStudentAttendance, self).setUp()
|
||||||
|
|
||||||
|
self.company = self.env.ref('base.main_company')
|
||||||
|
|
||||||
|
# Setup master records
|
||||||
|
self.campus = self.env['education.campus'].create({
|
||||||
|
'name': 'Attendance Test Campus',
|
||||||
|
'code': 'ATC',
|
||||||
|
})
|
||||||
|
self.department = self.env['education.department'].create({
|
||||||
|
'name': 'Attendance Test Dept',
|
||||||
|
'code': 'ATD',
|
||||||
|
'campus_id': self.campus.id,
|
||||||
|
})
|
||||||
|
self.level = self.env['education.academic.level'].create({
|
||||||
|
'name': 'Attendance Test Level',
|
||||||
|
'code': 'ATL',
|
||||||
|
})
|
||||||
|
self.program = self.env['education.program'].create({
|
||||||
|
'name': 'Attendance Test Program',
|
||||||
|
'code': 'ATP',
|
||||||
|
'department_id': self.department.id,
|
||||||
|
'level_id': self.level.id,
|
||||||
|
})
|
||||||
|
self.year = self.env['education.academic.year'].create({
|
||||||
|
'name': 'Attendance Test Year 2026',
|
||||||
|
'code': 'ATY26',
|
||||||
|
'start_date': '2026-01-01',
|
||||||
|
'end_date': '2026-12-31',
|
||||||
|
})
|
||||||
|
self.term = self.env['education.academic.term'].create({
|
||||||
|
'name': 'Attendance Test Term 1',
|
||||||
|
'code': 'ATT1',
|
||||||
|
'academic_year_id': self.year.id,
|
||||||
|
'start_date': '2026-01-01',
|
||||||
|
'end_date': '2026-06-30',
|
||||||
|
})
|
||||||
|
self.batch = self.env['education.batch'].create({
|
||||||
|
'name': 'Attendance Test Batch 2026',
|
||||||
|
'code': 'ATB26',
|
||||||
|
})
|
||||||
|
self.semester = self.env['education.semester'].create({
|
||||||
|
'name': 'Attendance Test Semester 1',
|
||||||
|
'code': 'ATS1',
|
||||||
|
})
|
||||||
|
self.section = self.env['education.section'].create({
|
||||||
|
'name': 'Attendance Test Section A',
|
||||||
|
'code': 'ATSA',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create 3 active students
|
||||||
|
self.student_1 = self.env['education.student'].create({
|
||||||
|
'first_name': 'TestStudent',
|
||||||
|
'last_name': 'One',
|
||||||
|
'student_id': 'ATSTU01',
|
||||||
|
'admission_number': 'ATADM01',
|
||||||
|
'roll_number': 'ATRL01',
|
||||||
|
'campus_id': self.campus.id,
|
||||||
|
'department_id': self.department.id,
|
||||||
|
'program_id': self.program.id,
|
||||||
|
'level_id': self.level.id,
|
||||||
|
'academic_year_id': self.year.id,
|
||||||
|
'academic_term_id': self.term.id,
|
||||||
|
'batch_id': self.batch.id,
|
||||||
|
'semester_id': self.semester.id,
|
||||||
|
'section_id': self.section.id,
|
||||||
|
'status': 'active',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.student_2 = self.env['education.student'].create({
|
||||||
|
'first_name': 'TestStudent',
|
||||||
|
'last_name': 'Two',
|
||||||
|
'student_id': 'ATSTU02',
|
||||||
|
'admission_number': 'ATADM02',
|
||||||
|
'roll_number': 'ATRL02',
|
||||||
|
'campus_id': self.campus.id,
|
||||||
|
'department_id': self.department.id,
|
||||||
|
'program_id': self.program.id,
|
||||||
|
'level_id': self.level.id,
|
||||||
|
'academic_year_id': self.year.id,
|
||||||
|
'academic_term_id': self.term.id,
|
||||||
|
'batch_id': self.batch.id,
|
||||||
|
'semester_id': self.semester.id,
|
||||||
|
'section_id': self.section.id,
|
||||||
|
'status': 'active',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.student_3 = self.env['education.student'].create({
|
||||||
|
'first_name': 'TestStudent',
|
||||||
|
'last_name': 'Three',
|
||||||
|
'student_id': 'ATSTU03',
|
||||||
|
'admission_number': 'ATADM03',
|
||||||
|
'roll_number': 'ATRL03',
|
||||||
|
'campus_id': self.campus.id,
|
||||||
|
'department_id': self.department.id,
|
||||||
|
'program_id': self.program.id,
|
||||||
|
'level_id': self.level.id,
|
||||||
|
'academic_year_id': self.year.id,
|
||||||
|
'academic_term_id': self.term.id,
|
||||||
|
'batch_id': self.batch.id,
|
||||||
|
'semester_id': self.semester.id,
|
||||||
|
'section_id': self.section.id,
|
||||||
|
'status': 'active',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Draft Session
|
||||||
|
self.session = self.env['education.attendance.session'].create({
|
||||||
|
'name': 'Test Lecture Period 1',
|
||||||
|
'campus_id': self.campus.id,
|
||||||
|
'department_id': self.department.id,
|
||||||
|
'program_id': self.program.id,
|
||||||
|
'academic_level_id': self.level.id,
|
||||||
|
'academic_year_id': self.year.id,
|
||||||
|
'academic_term_id': self.term.id,
|
||||||
|
'attendance_date': '2026-02-01',
|
||||||
|
'period': 'period_1',
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_attendance_session_workflow(self):
|
||||||
|
"""Test workflow of opening session, marking attendance, completing it, and reverse logic"""
|
||||||
|
# Session should start in draft with no lines
|
||||||
|
self.assertEqual(self.session.state, 'draft')
|
||||||
|
self.assertEqual(len(self.session.line_ids), 0)
|
||||||
|
|
||||||
|
# Open session (should auto-generate lines for our 3 active students)
|
||||||
|
self.session.action_open()
|
||||||
|
self.assertEqual(self.session.state, 'open')
|
||||||
|
self.assertEqual(len(self.session.line_ids), 3)
|
||||||
|
|
||||||
|
# Confirm default state is 'present'
|
||||||
|
for line in self.session.line_ids:
|
||||||
|
self.assertEqual(line.status, 'present')
|
||||||
|
|
||||||
|
# Test bulk marking: set all to 'absent'
|
||||||
|
self.session.action_bulk_absent()
|
||||||
|
for line in self.session.line_ids:
|
||||||
|
self.assertEqual(line.status, 'absent')
|
||||||
|
|
||||||
|
# Let's set Student 1 to Present, Student 2 to Late, Student 3 to Medical
|
||||||
|
line_1 = self.session.line_ids.filtered(lambda l: l.student_id == self.student_1)
|
||||||
|
line_2 = self.session.line_ids.filtered(lambda l: l.student_id == self.student_2)
|
||||||
|
line_3 = self.session.line_ids.filtered(lambda l: l.student_id == self.student_3)
|
||||||
|
|
||||||
|
line_1.write({'status': 'present'})
|
||||||
|
line_2.write({'status': 'late'})
|
||||||
|
line_3.write({'status': 'medical_leave'})
|
||||||
|
|
||||||
|
# Complete Session (should write to permanent history education.attendance)
|
||||||
|
self.session.action_complete()
|
||||||
|
self.assertEqual(self.session.state, 'completed')
|
||||||
|
|
||||||
|
# Verify history matches
|
||||||
|
history_1 = self.env['education.attendance'].search([('student_id', '=', self.student_1.id), ('session_id', '=', self.session.id)])
|
||||||
|
history_2 = self.env['education.attendance'].search([('student_id', '=', self.student_2.id), ('session_id', '=', self.session.id)])
|
||||||
|
history_3 = self.env['education.attendance'].search([('student_id', '=', self.student_3.id), ('session_id', '=', self.session.id)])
|
||||||
|
|
||||||
|
self.assertEqual(len(history_1), 1)
|
||||||
|
self.assertEqual(history_1.status, 'present')
|
||||||
|
self.assertEqual(history_2.status, 'late')
|
||||||
|
self.assertEqual(history_3.status, 'medical_leave')
|
||||||
|
|
||||||
|
# Invalidate student record cache to read computed metrics
|
||||||
|
self.student_1.invalidate_recordset()
|
||||||
|
self.student_2.invalidate_recordset()
|
||||||
|
self.student_3.invalidate_recordset()
|
||||||
|
|
||||||
|
# Present, Late, Medical should all count as attended (100% attendance for this single session)
|
||||||
|
self.assertEqual(self.student_1.attendance_pct, 100.0)
|
||||||
|
self.assertEqual(self.student_2.attendance_pct, 100.0)
|
||||||
|
self.assertEqual(self.student_3.attendance_pct, 100.0)
|
||||||
|
|
||||||
|
self.assertEqual(self.student_1.present_count, 1)
|
||||||
|
self.assertEqual(self.student_2.late_count, 1)
|
||||||
|
self.assertEqual(self.student_3.medical_leave_count, 1)
|
||||||
|
|
||||||
|
# Reset to draft (should unlink lines and history)
|
||||||
|
self.session.action_draft()
|
||||||
|
self.assertEqual(self.session.state, 'draft')
|
||||||
|
self.assertEqual(len(self.session.line_ids), 0)
|
||||||
|
self.assertEqual(len(self.env['education.attendance'].search([('session_id', '=', self.session.id)])), 0)
|
||||||
|
|
||||||
|
def test_duplicate_session_constraint(self):
|
||||||
|
"""Test SQL constraints: cannot create duplicate session for same period and date"""
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
self.env['education.attendance.session'].create({
|
||||||
|
'name': 'Duplicate Session',
|
||||||
|
'campus_id': self.campus.id,
|
||||||
|
'department_id': self.department.id,
|
||||||
|
'program_id': self.program.id,
|
||||||
|
'academic_level_id': self.level.id,
|
||||||
|
'academic_year_id': self.year.id,
|
||||||
|
'academic_term_id': self.term.id,
|
||||||
|
'attendance_date': '2026-02-01',
|
||||||
|
'period': 'period_1',
|
||||||
|
})
|
||||||
|
self.env.flush_all()
|
||||||
22
addons/education_attendance/views/attendance_menus.xml
Normal file
22
addons/education_attendance/views/attendance_menus.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Attendance Root Menu under Education Parent Menu -->
|
||||||
|
<menuitem id="menu_education_attendance_root"
|
||||||
|
name="Attendance"
|
||||||
|
parent="education_base.menu_education_base_root"
|
||||||
|
sequence="25"
|
||||||
|
groups="group_attendance_user"/>
|
||||||
|
|
||||||
|
<!-- Submenus -->
|
||||||
|
<menuitem id="menu_education_attendance_session"
|
||||||
|
name="Attendance Sessions"
|
||||||
|
parent="menu_education_attendance_root"
|
||||||
|
action="action_education_attendance_session"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_education_attendance_history_menu"
|
||||||
|
name="Attendance History"
|
||||||
|
parent="menu_education_attendance_root"
|
||||||
|
action="action_education_attendance_history"
|
||||||
|
sequence="20"/>
|
||||||
|
</odoo>
|
||||||
184
addons/education_attendance/views/attendance_session_views.xml
Normal file
184
addons/education_attendance/views/attendance_session_views.xml
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_attendance_session_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.session.tree</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Attendance Sessions">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="campus_id"/>
|
||||||
|
<field name="program_id"/>
|
||||||
|
<field name="academic_year_id"/>
|
||||||
|
<field name="attendance_date"/>
|
||||||
|
<field name="period"/>
|
||||||
|
<field name="faculty_id"/>
|
||||||
|
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-warning="state == 'open'" decoration-success="state == 'completed'" decoration-danger="state == 'cancelled'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_session_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.session.kanban</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban default_group_by="state" class="o_kanban_small_column">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="attendance_date"/>
|
||||||
|
<field name="period"/>
|
||||||
|
<field name="faculty_id"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="oe_kanban_global_click o_kanban_card">
|
||||||
|
<div class="oe_kanban_content">
|
||||||
|
<div class="o_kanban_record_title font-weight-bold">
|
||||||
|
<strong><field name="name"/></strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Date: <field name="attendance_date"/>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Period: <field name="period"/>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
Faculty: <field name="faculty_id"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_session_calendar" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.session.calendar</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<calendar string="Attendance Calendar" date_start="attendance_date" color="state" mode="month">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="period"/>
|
||||||
|
<field name="faculty_id"/>
|
||||||
|
</calendar>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_session_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.session.pivot</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Sessions Analysis">
|
||||||
|
<field name="program_id" type="row"/>
|
||||||
|
<field name="state" type="col"/>
|
||||||
|
<field name="attendance_date" interval="month" type="row"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_session_graph" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.session.graph</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Sessions Analysis" type="bar">
|
||||||
|
<field name="program_id"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_session_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.session.form</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Attendance Session">
|
||||||
|
<header>
|
||||||
|
<button name="action_open" string="Open Session" type="object" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_complete" string="Complete Session" type="object" class="oe_highlight" invisible="state != 'open'"/>
|
||||||
|
<button name="action_cancel" string="Cancel Session" type="object" invisible="state in ('cancelled')"/>
|
||||||
|
<button name="action_draft" string="Reset to Draft" type="object" invisible="state not in ('open', 'completed', 'cancelled')"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,open,completed"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name" class="oe_edit_only"/>
|
||||||
|
<h1><field name="name" placeholder="Session Title e.g. Year 1 MCA Lecture" readonly="state != 'draft'"/></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info mt-2" role="alert" invisible="state != 'open'">
|
||||||
|
<strong>Bulk Actions:</strong>
|
||||||
|
<button name="action_bulk_present" string="Present All" type="object" class="btn btn-outline-success btn-sm mx-1"/>
|
||||||
|
<button name="action_bulk_absent" string="Absent All" type="object" class="btn btn-outline-danger btn-sm mx-1"/>
|
||||||
|
<button name="action_bulk_late" string="Late All" type="object" class="btn btn-outline-warning btn-sm mx-1"/>
|
||||||
|
<button name="action_bulk_medical" string="Medical All" type="object" class="btn btn-outline-info btn-sm mx-1"/>
|
||||||
|
<button name="action_bulk_duty" string="On Duty All" type="object" class="btn btn-outline-secondary btn-sm mx-1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Academic Grouping">
|
||||||
|
<field name="campus_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="department_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="program_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="academic_level_id" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group string="Schedule & Faculty">
|
||||||
|
<field name="academic_year_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="academic_term_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="attendance_date" readonly="state != 'draft'"/>
|
||||||
|
<field name="period" readonly="state != 'draft'"/>
|
||||||
|
<field name="faculty_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Students List" name="students_list" invisible="state == 'draft'">
|
||||||
|
<field name="line_ids" readonly="state != 'open'">
|
||||||
|
<list editable="bottom" create="false" delete="false">
|
||||||
|
<field name="student_id" readonly="1"/>
|
||||||
|
<field name="status" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="time_in"/>
|
||||||
|
<field name="time_out"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_session_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.session.search</field>
|
||||||
|
<field name="model">education.attendance.session</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Attendance Sessions">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="campus_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="program_id"/>
|
||||||
|
<field name="faculty_id"/>
|
||||||
|
<field name="attendance_date"/>
|
||||||
|
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
|
||||||
|
<filter string="Open (Active)" name="open" domain="[('state', '=', 'open')]"/>
|
||||||
|
<filter string="Completed" name="completed" domain="[('state', '=', 'completed')]"/>
|
||||||
|
<filter string="Cancelled" name="cancelled" domain="[('state', '=', 'cancelled')]"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Campus" name="group_campus" context="{'group_by': 'campus_id'}"/>
|
||||||
|
<filter string="Department" name="group_dept" context="{'group_by': 'department_id'}"/>
|
||||||
|
<filter string="Program" name="group_program" context="{'group_by': 'program_id'}"/>
|
||||||
|
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
|
||||||
|
<filter string="Faculty" name="group_faculty" context="{'group_by': 'faculty_id'}"/>
|
||||||
|
<filter string="Date" name="group_date" context="{'group_by': 'attendance_date'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_attendance_session" model="ir.actions.act_window">
|
||||||
|
<field name="name">Attendance Sessions</field>
|
||||||
|
<field name="res_model">education.attendance.session</field>
|
||||||
|
<field name="view_mode">list,kanban,form,calendar,pivot,graph</field>
|
||||||
|
<field name="search_view_id" ref="view_education_attendance_session_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
83
addons/education_attendance/views/attendance_views.xml
Normal file
83
addons/education_attendance/views/attendance_views.xml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_attendance_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.tree</field>
|
||||||
|
<field name="model">education.attendance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Attendance History" create="false" edit="false" delete="false">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="student_id"/>
|
||||||
|
<field name="session_id"/>
|
||||||
|
<field name="status" widget="badge" decoration-success="status == 'present' or status == 'on_duty'" decoration-danger="status == 'absent'" decoration-warning="status == 'late'" decoration-info="status == 'medical_leave'"/>
|
||||||
|
<field name="faculty_id"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_calendar" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.calendar</field>
|
||||||
|
<field name="model">education.attendance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<calendar string="Attendance Calendar" date_start="date" color="status" mode="month">
|
||||||
|
<field name="student_id"/>
|
||||||
|
<field name="status"/>
|
||||||
|
</calendar>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.pivot</field>
|
||||||
|
<field name="model">education.attendance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Attendance Analysis">
|
||||||
|
<field name="student_id" type="row"/>
|
||||||
|
<field name="status" type="col"/>
|
||||||
|
<field name="date" interval="month" type="row"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_graph" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.graph</field>
|
||||||
|
<field name="model">education.attendance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Attendance Analysis" type="bar">
|
||||||
|
<field name="status"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_attendance_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.attendance.search</field>
|
||||||
|
<field name="model">education.attendance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Attendance History">
|
||||||
|
<field name="student_id"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<field name="faculty_id"/>
|
||||||
|
<filter string="Present" name="present" domain="[('status', '=', 'present')]"/>
|
||||||
|
<filter string="Absent" name="absent" domain="[('status', '=', 'absent')]"/>
|
||||||
|
<filter string="Late" name="late" domain="[('status', '=', 'late')]"/>
|
||||||
|
<filter string="Medical Leave" name="medical_leave" domain="[('status', '=', 'medical_leave')]"/>
|
||||||
|
<filter string="On Duty" name="on_duty" domain="[('status', '=', 'on_duty')]"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Student" name="group_student" context="{'group_by': 'student_id'}"/>
|
||||||
|
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
|
||||||
|
<filter string="Date" name="group_date" context="{'group_by': 'date'}"/>
|
||||||
|
</group>
|
||||||
|
<searchpanel>
|
||||||
|
<field name="status" icon="fa-check-square-o"/>
|
||||||
|
</searchpanel>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_attendance_history" model="ir.actions.act_window">
|
||||||
|
<field name="name">Attendance History</field>
|
||||||
|
<field name="res_model">education.attendance</field>
|
||||||
|
<field name="view_mode">list,calendar,pivot,graph</field>
|
||||||
|
<field name="search_view_id" ref="view_education_attendance_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
38
addons/education_attendance/views/student_views_inherit.xml
Normal file
38
addons/education_attendance/views/student_views_inherit.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_student_form_inherit_attendance" model="ir.ui.view">
|
||||||
|
<field name="name">education.student.form.inherit.attendance</field>
|
||||||
|
<field name="model">education.student</field>
|
||||||
|
<field name="inherit_id" ref="education_student.view_education_student_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Replace placeholder Attendance button with dynamic record action -->
|
||||||
|
<xpath expr="//button[@icon='fa-calendar-check-o']" position="replace">
|
||||||
|
<button class="oe_stat_button" icon="fa-calendar-check-o" type="action" name="%(action_education_attendance_history)d" context="{'search_default_student_id': id}">
|
||||||
|
<div class="o_stat_info">
|
||||||
|
<span class="o_stat_value">
|
||||||
|
<field name="attendance_pct" widget="float" precision="1"/>%
|
||||||
|
</span>
|
||||||
|
<span class="o_stat_text">Attendance</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Inject Attendance summary page in student tabs -->
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Attendance Details" name="attendance_details" groups="education_attendance.group_attendance_user">
|
||||||
|
<group>
|
||||||
|
<group string="Summarized Metrics">
|
||||||
|
<field name="attendance_pct" widget="float" precision="1"/>
|
||||||
|
<field name="present_count"/>
|
||||||
|
<field name="absent_count"/>
|
||||||
|
</group>
|
||||||
|
<group string="Detailed Delineations">
|
||||||
|
<field name="late_count"/>
|
||||||
|
<field name="medical_leave_count"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1
addons/education_base/__init__.py
Normal file
1
addons/education_base/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
40
addons/education_base/__manifest__.py
Normal file
40
addons/education_base/__manifest__.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
'name': 'Education Base',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'summary': 'Core foundation for education management',
|
||||||
|
'description': '''
|
||||||
|
Education Base module provides the foundational master data, academic configuration,
|
||||||
|
and shared setup objects used by all education-related modules in the ERP.
|
||||||
|
It includes academic years, academic terms, campuses, departments, academic levels,
|
||||||
|
programs, and global education configuration.
|
||||||
|
''',
|
||||||
|
'category': 'Education',
|
||||||
|
'author': 'Metatron',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'depends': ['base', 'mail', 'contacts'],
|
||||||
|
'data': [
|
||||||
|
'security/education_base_security.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'security/record_rules.xml',
|
||||||
|
'data/education_base_sequence.xml',
|
||||||
|
'data/education_base_menu.xml',
|
||||||
|
'views/academic_year_views.xml',
|
||||||
|
'views/academic_term_views.xml',
|
||||||
|
'views/campus_views.xml',
|
||||||
|
'views/department_views.xml',
|
||||||
|
'views/academic_level_views.xml',
|
||||||
|
'views/program_views.xml',
|
||||||
|
'views/education_config_views.xml',
|
||||||
|
'views/menuitems.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'demo/demo_academic_level.xml',
|
||||||
|
'demo/demo_academic_year.xml',
|
||||||
|
'demo/demo_campus.xml',
|
||||||
|
'demo/demo_department.xml',
|
||||||
|
'demo/demo_program.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
BIN
addons/education_base/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/education_base/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
addons/education_base/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/education_base/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
6
addons/education_base/data/education_base_menu.xml
Normal file
6
addons/education_base/data/education_base_menu.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<menuitem id="menu_education_base_root" name="Education" sequence="0" groups="base.group_user" web_icon="fa fa-graduation-cap"/>
|
||||||
|
<menuitem id="menu_education_base_configuration" name="Configuration" parent="menu_education_base_root" sequence="100" groups="base.group_user"/>
|
||||||
|
<menuitem id="menu_education_base_master_data" name="Master Data" parent="menu_education_base_root" sequence="2" groups="base.group_user"/>
|
||||||
|
</odoo>
|
||||||
42
addons/education_base/data/education_base_sequence.xml
Normal file
42
addons/education_base/data/education_base_sequence.xml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="seq_education_academic_year" model="ir.sequence">
|
||||||
|
<field name="name">Academic Year</field>
|
||||||
|
<field name="code">education.academic.year</field>
|
||||||
|
<field name="prefix">AY/</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="seq_education_academic_term" model="ir.sequence">
|
||||||
|
<field name="name">Academic Term</field>
|
||||||
|
<field name="code">education.academic.term</field>
|
||||||
|
<field name="prefix">TERM/</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="seq_education_campus" model="ir.sequence">
|
||||||
|
<field name="name">Campus</field>
|
||||||
|
<field name="code">education.campus</field>
|
||||||
|
<field name="prefix">CAMP/</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="seq_education_department" model="ir.sequence">
|
||||||
|
<field name="name">Department</field>
|
||||||
|
<field name="code">education.department</field>
|
||||||
|
<field name="prefix">DEP/</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="seq_education_program" model="ir.sequence">
|
||||||
|
<field name="name">Program</field>
|
||||||
|
<field name="code">education.program</field>
|
||||||
|
<field name="prefix">PROG/</field>
|
||||||
|
<field name="padding">5</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
10
addons/education_base/demo/demo_academic_level.xml
Normal file
10
addons/education_base/demo/demo_academic_level.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="demo_academic_level_undergraduate" model="education.academic.level">
|
||||||
|
<field name="name">Undergraduate</field>
|
||||||
|
<field name="code">UG</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="description">Undergraduate study level.</field>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
12
addons/education_base/demo/demo_academic_year.xml
Normal file
12
addons/education_base/demo/demo_academic_year.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="demo_academic_year_2025" model="education.academic.year">
|
||||||
|
<field name="name">2025-2026</field>
|
||||||
|
<field name="code">AY2025</field>
|
||||||
|
<field name="start_date">2025-06-01</field>
|
||||||
|
<field name="end_date">2026-05-31</field>
|
||||||
|
<field name="state">active</field>
|
||||||
|
<field name="current_year">True</field>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
10
addons/education_base/demo/demo_campus.xml
Normal file
10
addons/education_base/demo/demo_campus.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="demo_campus_main" model="education.campus">
|
||||||
|
<field name="name">Main Campus</field>
|
||||||
|
<field name="code">MAIN</field>
|
||||||
|
<field name="phone">+1234567890</field>
|
||||||
|
<field name="email">main@school.example</field>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
8
addons/education_base/demo/demo_department.xml
Normal file
8
addons/education_base/demo/demo_department.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="demo_department_engineering" model="education.department">
|
||||||
|
<field name="name">Engineering</field>
|
||||||
|
<field name="code">ENG</field>
|
||||||
|
<field name="campus_id" ref="demo_campus_main"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
11
addons/education_base/demo/demo_program.xml
Normal file
11
addons/education_base/demo/demo_program.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="demo_program_btech" model="education.program">
|
||||||
|
<field name="name">B.Tech</field>
|
||||||
|
<field name="code">BTECH</field>
|
||||||
|
<field name="level_id" ref="demo_academic_level_undergraduate"/>
|
||||||
|
<field name="department_id" ref="demo_department_engineering"/>
|
||||||
|
<field name="duration">4</field>
|
||||||
|
<field name="duration_unit">years</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
7
addons/education_base/models/__init__.py
Normal file
7
addons/education_base/models/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from . import academic_year
|
||||||
|
from . import academic_term
|
||||||
|
from . import campus
|
||||||
|
from . import department
|
||||||
|
from . import academic_level
|
||||||
|
from . import program
|
||||||
|
from . import education_config
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
addons/education_base/models/__pycache__/campus.cpython-310.pyc
Normal file
BIN
addons/education_base/models/__pycache__/campus.cpython-310.pyc
Normal file
Binary file not shown.
BIN
addons/education_base/models/__pycache__/campus.cpython-312.pyc
Normal file
BIN
addons/education_base/models/__pycache__/campus.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
addons/education_base/models/__pycache__/program.cpython-310.pyc
Normal file
BIN
addons/education_base/models/__pycache__/program.cpython-310.pyc
Normal file
Binary file not shown.
BIN
addons/education_base/models/__pycache__/program.cpython-312.pyc
Normal file
BIN
addons/education_base/models/__pycache__/program.cpython-312.pyc
Normal file
Binary file not shown.
35
addons/education_base/models/academic_level.py
Normal file
35
addons/education_base/models/academic_level.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class AcademicLevel(models.Model):
|
||||||
|
_name = 'education.academic.level'
|
||||||
|
_description = 'Academic Level'
|
||||||
|
_order = 'sequence, name'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, string='Level Name')
|
||||||
|
code = fields.Char(required=True, string='Level Code', index=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
description = fields.Text(string='Description')
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
string='Company',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_academic_level_code_company', 'unique(code, company_id)', 'Academic level code must be unique per company.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.constrains('name', 'company_id')
|
||||||
|
def _check_unique_name(self):
|
||||||
|
for rec in self:
|
||||||
|
if self.search_count([
|
||||||
|
('company_id', '=', rec.company_id.id),
|
||||||
|
('name', '=ilike', rec.name),
|
||||||
|
('id', '!=', rec.id),
|
||||||
|
]):
|
||||||
|
raise ValidationError('Academic level name must be unique within a company.')
|
||||||
65
addons/education_base/models/academic_term.py
Normal file
65
addons/education_base/models/academic_term.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class AcademicTerm(models.Model):
|
||||||
|
_name = 'education.academic.term'
|
||||||
|
_description = 'Academic Term'
|
||||||
|
_order = 'sequence, start_date'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, string='Term Name')
|
||||||
|
code = fields.Char(string='Code', required=True, index=True)
|
||||||
|
academic_year_id = fields.Many2one(
|
||||||
|
'education.academic.year',
|
||||||
|
string='Academic Year',
|
||||||
|
required=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
start_date = fields.Date(string='Start Date', required=True)
|
||||||
|
end_date = fields.Date(string='End Date', required=True)
|
||||||
|
sequence = fields.Integer(default=1, required=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('active', 'Active'),
|
||||||
|
('closed', 'Closed'),
|
||||||
|
], default='draft', required=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
related='academic_year_id.company_id',
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_term_code_year', 'unique(code, academic_year_id)', 'Term code must be unique within the academic year.'),
|
||||||
|
('uniq_term_sequence_year', 'unique(sequence, academic_year_id)', 'Term sequence must be unique within the academic year.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.constrains('start_date', 'end_date')
|
||||||
|
def _check_term_dates(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.start_date and rec.end_date and rec.start_date > rec.end_date:
|
||||||
|
raise ValidationError('End date must be greater than start date.')
|
||||||
|
|
||||||
|
@api.constrains('academic_year_id', 'start_date', 'end_date')
|
||||||
|
def _check_term_overlap(self):
|
||||||
|
for rec in self:
|
||||||
|
if not rec.academic_year_id:
|
||||||
|
continue
|
||||||
|
overlapping = self.search([
|
||||||
|
('id', '!=', rec.id),
|
||||||
|
('academic_year_id', '=', rec.academic_year_id.id),
|
||||||
|
('start_date', '<=', rec.end_date),
|
||||||
|
('end_date', '>=', rec.start_date),
|
||||||
|
])
|
||||||
|
if overlapping:
|
||||||
|
raise ValidationError('Academic terms within the same year cannot overlap.')
|
||||||
|
|
||||||
|
def action_activate(self):
|
||||||
|
self.write({'state': 'active'})
|
||||||
|
|
||||||
|
def action_close(self):
|
||||||
|
self.write({'state': 'closed'})
|
||||||
70
addons/education_base/models/academic_year.py
Normal file
70
addons/education_base/models/academic_year.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class AcademicYear(models.Model):
|
||||||
|
_name = 'education.academic.year'
|
||||||
|
_description = 'Academic Year'
|
||||||
|
_order = 'start_date desc, name desc'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, string='Academic Year')
|
||||||
|
code = fields.Char(string='Code', required=True, index=True)
|
||||||
|
start_date = fields.Date(string='Start Date', required=True)
|
||||||
|
end_date = fields.Date(string='End Date', required=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
string='Company',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
current_year = fields.Boolean(string='Current Year', default=False)
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('active', 'Active'),
|
||||||
|
('closed', 'Closed'),
|
||||||
|
], default='draft', required=True, tracking=True)
|
||||||
|
term_ids = fields.One2many('education.academic.term', 'academic_year_id', string='Terms')
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_year_code_company', 'unique(code, company_id)', 'Academic year code must be unique per company.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.constrains('start_date', 'end_date')
|
||||||
|
def _check_year_dates(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.start_date and rec.end_date and rec.start_date > rec.end_date:
|
||||||
|
raise ValidationError('End date must be later than start date.')
|
||||||
|
|
||||||
|
@api.constrains('current_year')
|
||||||
|
def _check_single_current_year(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.current_year:
|
||||||
|
existing = self.search([
|
||||||
|
('company_id', '=', rec.company_id.id),
|
||||||
|
('current_year', '=', True),
|
||||||
|
('id', '!=', rec.id),
|
||||||
|
])
|
||||||
|
if existing:
|
||||||
|
raise ValidationError('Only one academic year can be marked as current for a company.')
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('state') == 'active':
|
||||||
|
vals['current_year'] = True
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def action_activate(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.start_date and rec.end_date and rec.start_date > rec.end_date:
|
||||||
|
raise ValidationError('Academic year dates are invalid.')
|
||||||
|
rec.state = 'active'
|
||||||
|
rec.current_year = True
|
||||||
|
|
||||||
|
def action_close(self):
|
||||||
|
self.write({'state': 'closed'})
|
||||||
|
|
||||||
|
def action_set_draft(self):
|
||||||
|
self.write({'state': 'draft'})
|
||||||
53
addons/education_base/models/campus.py
Normal file
53
addons/education_base/models/campus.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo import api, fields, models
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class EducationCampus(models.Model):
|
||||||
|
_name = 'education.campus'
|
||||||
|
_description = 'Campus'
|
||||||
|
_order = 'name'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, string='Campus Name')
|
||||||
|
code = fields.Char(string='Campus Code', required=True, index=True)
|
||||||
|
logo = fields.Image(string='Campus Logo', max_width=1920, max_height=1920)
|
||||||
|
phone = fields.Char(string='Phone')
|
||||||
|
email = fields.Char(string='Email')
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
string='Company',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
address_id = fields.Many2one('res.partner', string='Address')
|
||||||
|
department_ids = fields.One2many('education.department', 'campus_id', string='Departments')
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_campus_code_company', 'unique(code, company_id)', 'Campus code must be unique per company.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.constrains('name', 'company_id')
|
||||||
|
def _check_name_unique(self):
|
||||||
|
for rec in self:
|
||||||
|
similar = self.search([
|
||||||
|
('company_id', '=', rec.company_id.id),
|
||||||
|
('name', '=ilike', rec.name),
|
||||||
|
('id', '!=', rec.id),
|
||||||
|
])
|
||||||
|
if similar:
|
||||||
|
raise ValidationError('Campus name must be unique within a company.')
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('code'):
|
||||||
|
vals['code'] = vals['code'].strip().upper()
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if 'code' in vals and vals.get('code'):
|
||||||
|
vals['code'] = vals['code'].strip().upper()
|
||||||
|
return super().write(vals)
|
||||||
50
addons/education_base/models/department.py
Normal file
50
addons/education_base/models/department.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class EducationDepartment(models.Model):
|
||||||
|
_name = 'education.department'
|
||||||
|
_description = 'Department'
|
||||||
|
_order = 'name'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, string='Department Name')
|
||||||
|
code = fields.Char(string='Department Code', required=True, index=True)
|
||||||
|
campus_id = fields.Many2one(
|
||||||
|
'education.campus',
|
||||||
|
string='Campus',
|
||||||
|
required=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
related='campus_id.company_id',
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
head_teacher_id = fields.Many2one('res.partner', string='Head Teacher')
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
program_ids = fields.One2many('education.program', 'department_id', string='Programs')
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_department_code_company', 'unique(code, company_id)', 'Department code must be unique per company.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.constrains('code')
|
||||||
|
def _check_code_format(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.code and not rec.code.strip():
|
||||||
|
raise ValidationError('Department code cannot be blank.')
|
||||||
|
|
||||||
|
@api.constrains('name', 'company_id')
|
||||||
|
def _check_name_unique(self):
|
||||||
|
for rec in self:
|
||||||
|
similar = self.search([
|
||||||
|
('company_id', '=', rec.company_id.id),
|
||||||
|
('name', '=ilike', rec.name),
|
||||||
|
('campus_id', '=', rec.campus_id.id),
|
||||||
|
('id', '!=', rec.id),
|
||||||
|
])
|
||||||
|
if similar:
|
||||||
|
raise ValidationError('Department name must be unique within a campus.')
|
||||||
59
addons/education_base/models/education_config.py
Normal file
59
addons/education_base/models/education_config.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class EducationConfiguration(models.Model):
|
||||||
|
_name = 'education.configuration'
|
||||||
|
_description = 'Education Configuration'
|
||||||
|
_rec_name = 'school_name'
|
||||||
|
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
string='Company',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
school_name = fields.Char(string='School Name', required=True)
|
||||||
|
school_code = fields.Char(string='School Code', required=True)
|
||||||
|
default_campus_id = fields.Many2one('education.campus', string='Default Campus')
|
||||||
|
default_academic_year_id = fields.Many2one('education.academic.year', string='Default Academic Year')
|
||||||
|
default_term_id = fields.Many2one('education.academic.term', string='Default Term')
|
||||||
|
default_currency_id = fields.Many2one('res.currency', string='Default Currency')
|
||||||
|
logo = fields.Binary(string='Logo')
|
||||||
|
contact_email = fields.Char(string='Contact Email')
|
||||||
|
contact_phone = fields.Char(string='Contact Phone')
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_company_config', 'unique(company_id)', 'Only one education configuration can exist per company.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.constrains('default_campus_id', 'company_id')
|
||||||
|
def _check_default_campus_in_company(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.default_campus_id and rec.default_campus_id.company_id != rec.company_id:
|
||||||
|
raise ValidationError('Default campus must belong to the same company.')
|
||||||
|
|
||||||
|
@api.constrains('default_academic_year_id', 'company_id')
|
||||||
|
def _check_default_academic_year_in_company(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.default_academic_year_id and rec.default_academic_year_id.company_id != rec.company_id:
|
||||||
|
raise ValidationError('Default academic year must belong to the same company.')
|
||||||
|
|
||||||
|
@api.constrains('default_term_id', 'company_id')
|
||||||
|
def _check_default_term_in_company(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.default_term_id and rec.default_term_id.company_id != rec.company_id:
|
||||||
|
raise ValidationError('Default term must belong to the same company.')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_default_values(self):
|
||||||
|
config = self.search([('company_id', '=', self.env.company.id)], limit=1)
|
||||||
|
if config:
|
||||||
|
return config
|
||||||
|
return self.create({
|
||||||
|
'company_id': self.env.company.id,
|
||||||
|
'school_name': self.env.company.name,
|
||||||
|
'school_code': self.env.company.name[:5].upper(),
|
||||||
|
})
|
||||||
67
addons/education_base/models/program.py
Normal file
67
addons/education_base/models/program.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class EducationProgram(models.Model):
|
||||||
|
_name = 'education.program'
|
||||||
|
_description = 'Program'
|
||||||
|
_order = 'name'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, string='Program Name')
|
||||||
|
code = fields.Char(required=True, string='Program Code', index=True)
|
||||||
|
level_id = fields.Many2one(
|
||||||
|
'education.academic.level',
|
||||||
|
string='Academic Level',
|
||||||
|
required=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
department_id = fields.Many2one(
|
||||||
|
'education.department',
|
||||||
|
string='Department',
|
||||||
|
required=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
related='department_id.company_id',
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
duration = fields.Integer(string='Duration', default=1)
|
||||||
|
duration_unit = fields.Selection([
|
||||||
|
('years', 'Years'),
|
||||||
|
('months', 'Months'),
|
||||||
|
('semesters', 'Semesters'),
|
||||||
|
], default='years', required=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
description = fields.Text(string='Description')
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('uniq_program_code_company', 'unique(code, company_id)', 'Program code must be unique per company.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.constrains('duration')
|
||||||
|
def _check_duration(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.duration <= 0:
|
||||||
|
raise ValidationError('Program duration must be greater than zero.')
|
||||||
|
|
||||||
|
@api.constrains('name', 'company_id')
|
||||||
|
def _check_name_unique(self):
|
||||||
|
for rec in self:
|
||||||
|
if self.search_count([
|
||||||
|
('company_id', '=', rec.company_id.id),
|
||||||
|
('name', '=ilike', rec.name),
|
||||||
|
('id', '!=', rec.id),
|
||||||
|
]):
|
||||||
|
raise ValidationError('Program name must be unique within a company.')
|
||||||
|
|
||||||
|
@api.onchange('department_id')
|
||||||
|
def _onchange_department_id(self):
|
||||||
|
if self.department_id:
|
||||||
|
self.company_id = self.department_id.company_id.id
|
||||||
|
if self.level_id and self.level_id.company_id != self.company_id:
|
||||||
|
self.level_id = False
|
||||||
24
addons/education_base/security/education_base_security.xml
Normal file
24
addons/education_base/security/education_base_security.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="module_category_education_base" model="ir.module.category">
|
||||||
|
<field name="name">Education Base</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_education_base_user" model="res.groups">
|
||||||
|
<field name="name">Education Base User</field>
|
||||||
|
<field name="comment">Basic access to education master data.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_education_base_manager" model="res.groups">
|
||||||
|
<field name="name">Education Base Manager</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_education_base_user'))]"/>
|
||||||
|
<field name="comment">Manage academic configuration and master setup.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_education_base_admin" model="res.groups">
|
||||||
|
<field name="name">Education Base Admin</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_education_base_manager'))]"/>
|
||||||
|
<field name="comment">Full administrative access to base education configuration.</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
28
addons/education_base/security/ir.model.access.csv
Normal file
28
addons/education_base/security/ir.model.access.csv
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_education_academic_year_user,education.academic.year user,model_education_academic_year,group_education_base_user,1,0,0,0
|
||||||
|
access_education_academic_year_manager,education.academic.year manager,model_education_academic_year,group_education_base_manager,1,1,1,1
|
||||||
|
access_education_academic_year_admin,education.academic.year admin,model_education_academic_year,group_education_base_admin,1,1,1,1
|
||||||
|
|
||||||
|
access_education_academic_term_user,education.academic.term user,model_education_academic_term,group_education_base_user,1,0,0,0
|
||||||
|
access_education_academic_term_manager,education.academic.term manager,model_education_academic_term,group_education_base_manager,1,1,1,1
|
||||||
|
access_education_academic_term_admin,education.academic.term admin,model_education_academic_term,group_education_base_admin,1,1,1,1
|
||||||
|
|
||||||
|
access_education_campus_user,education.campus user,model_education_campus,group_education_base_user,1,0,0,0
|
||||||
|
access_education_campus_manager,education.campus manager,model_education_campus,group_education_base_manager,1,1,1,1
|
||||||
|
access_education_campus_admin,education.campus admin,model_education_campus,group_education_base_admin,1,1,1,1
|
||||||
|
|
||||||
|
access_education_department_user,education.department user,model_education_department,group_education_base_user,1,0,0,0
|
||||||
|
access_education_department_manager,education.department manager,model_education_department,group_education_base_manager,1,1,1,1
|
||||||
|
access_education_department_admin,education.department admin,model_education_department,group_education_base_admin,1,1,1,1
|
||||||
|
|
||||||
|
access_education_academic_level_user,education.academic.level user,model_education_academic_level,group_education_base_user,1,0,0,0
|
||||||
|
access_education_academic_level_manager,education.academic.level manager,model_education_academic_level,group_education_base_manager,1,1,1,1
|
||||||
|
access_education_academic_level_admin,education.academic.level admin,model_education_academic_level,group_education_base_admin,1,1,1,1
|
||||||
|
|
||||||
|
access_education_program_user,education.program user,model_education_program,group_education_base_user,1,0,0,0
|
||||||
|
access_education_program_manager,education.program manager,model_education_program,group_education_base_manager,1,1,1,1
|
||||||
|
access_education_program_admin,education.program admin,model_education_program,group_education_base_admin,1,1,1,1
|
||||||
|
|
||||||
|
access_education_configuration_user,education.configuration user,model_education_configuration,group_education_base_user,1,0,0,0
|
||||||
|
access_education_configuration_manager,education.configuration manager,model_education_configuration,group_education_base_manager,1,1,1,1
|
||||||
|
access_education_configuration_admin,education.configuration admin,model_education_configuration,group_education_base_admin,1,1,1,1
|
||||||
|
51
addons/education_base/security/record_rules.xml
Normal file
51
addons/education_base/security/record_rules.xml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="0">
|
||||||
|
<record id="rule_education_academic_year_company" model="ir.rule">
|
||||||
|
<field name="name">Academic Year Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_academic_year"/>
|
||||||
|
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_education_base_user')),(4, ref('group_education_base_manager')),(4, ref('group_education_base_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_education_academic_term_company" model="ir.rule">
|
||||||
|
<field name="name">Academic Term Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_academic_term"/>
|
||||||
|
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_education_base_user')),(4, ref('group_education_base_manager')),(4, ref('group_education_base_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_education_campus_company" model="ir.rule">
|
||||||
|
<field name="name">Campus Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_campus"/>
|
||||||
|
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_education_base_user')),(4, ref('group_education_base_manager')),(4, ref('group_education_base_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_education_department_company" model="ir.rule">
|
||||||
|
<field name="name">Department Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_department"/>
|
||||||
|
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_education_base_user')),(4, ref('group_education_base_manager')),(4, ref('group_education_base_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_education_academic_level_company" model="ir.rule">
|
||||||
|
<field name="name">Academic Level Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_academic_level"/>
|
||||||
|
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_education_base_user')),(4, ref('group_education_base_manager')),(4, ref('group_education_base_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_education_program_company" model="ir.rule">
|
||||||
|
<field name="name">Program Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_program"/>
|
||||||
|
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_education_base_user')),(4, ref('group_education_base_manager')),(4, ref('group_education_base_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_education_configuration_company" model="ir.rule">
|
||||||
|
<field name="name">Configuration Company Rule</field>
|
||||||
|
<field name="model_id" ref="model_education_configuration"/>
|
||||||
|
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_education_base_user')),(4, ref('group_education_base_manager')),(4, ref('group_education_base_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
4
addons/education_base/tests/__init__.py
Normal file
4
addons/education_base/tests/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import test_academic_year
|
||||||
|
from . import test_campus
|
||||||
|
from . import test_program
|
||||||
|
from . import test_security
|
||||||
39
addons/education_base/tests/test_academic_year.py
Normal file
39
addons/education_base/tests/test_academic_year.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestAcademicYear(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.company
|
||||||
|
|
||||||
|
def test_academic_year_date_validation(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env['education.academic.year'].create({
|
||||||
|
'name': '2025-2026',
|
||||||
|
'code': 'AY2025',
|
||||||
|
'start_date': '2026-06-01',
|
||||||
|
'end_date': '2025-06-01',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_single_current_year_per_company(self):
|
||||||
|
self.env['education.academic.year'].create({
|
||||||
|
'name': '2024-2025',
|
||||||
|
'code': 'AY2024',
|
||||||
|
'start_date': '2024-06-01',
|
||||||
|
'end_date': '2025-06-01',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'current_year': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env['education.academic.year'].create({
|
||||||
|
'name': '2025-2026',
|
||||||
|
'code': 'AY2025',
|
||||||
|
'start_date': '2025-06-01',
|
||||||
|
'end_date': '2026-06-01',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'current_year': True,
|
||||||
|
})
|
||||||
23
addons/education_base/tests/test_campus.py
Normal file
23
addons/education_base/tests/test_campus.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestCampus(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.company
|
||||||
|
|
||||||
|
def test_campus_unique_code(self):
|
||||||
|
self.env['education.campus'].create({
|
||||||
|
'name': 'Main Campus',
|
||||||
|
'code': 'CAMP01',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env['education.campus'].create({
|
||||||
|
'name': 'Another Campus',
|
||||||
|
'code': 'CAMP01',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
})
|
||||||
35
addons/education_base/tests/test_program.py
Normal file
35
addons/education_base/tests/test_program.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestProgram(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.company
|
||||||
|
self.campus = self.env['education.campus'].create({
|
||||||
|
'name': 'North Campus',
|
||||||
|
'code': 'NCAMP',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
})
|
||||||
|
self.department = self.env['education.department'].create({
|
||||||
|
'name': 'Engineering',
|
||||||
|
'code': 'ENG',
|
||||||
|
'campus_id': self.campus.id,
|
||||||
|
})
|
||||||
|
self.level = self.env['education.academic.level'].create({
|
||||||
|
'name': 'Undergraduate',
|
||||||
|
'code': 'UG',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_program_duration(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env['education.program'].create({
|
||||||
|
'name': 'Engineering Degree',
|
||||||
|
'code': 'ENGD',
|
||||||
|
'level_id': self.level.id,
|
||||||
|
'department_id': self.department.id,
|
||||||
|
'duration': 0,
|
||||||
|
'duration_unit': 'years',
|
||||||
|
})
|
||||||
13
addons/education_base/tests/test_security.py
Normal file
13
addons/education_base/tests/test_security.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestSecurity(TransactionCase):
|
||||||
|
|
||||||
|
def test_base_user_access(self):
|
||||||
|
user = self.env['res.users'].create({
|
||||||
|
'name': 'Education User',
|
||||||
|
'login': 'edu_user_test_001',
|
||||||
|
'email': 'edu_user_test_001@example.com',
|
||||||
|
'group_ids': [(6, 0, [self.env.ref('education_base.group_education_base_user').id])],
|
||||||
|
})
|
||||||
|
self.assertTrue(user)
|
||||||
61
addons/education_base/views/academic_level_views.xml
Normal file
61
addons/education_base/views/academic_level_views.xml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_academic_level_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.level.tree</field>
|
||||||
|
<field name="model">education.academic.level</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Academic Levels">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_academic_level_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.level.form</field>
|
||||||
|
<field name="model">education.academic.level</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Academic Level">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="sequence"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="description"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_academic_level_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.level.search</field>
|
||||||
|
<field name="model">education.academic.level</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Academic Levels">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Company" name="company" context="{'group_by': 'company_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_academic_level" model="ir.actions.act_window">
|
||||||
|
<field name="name">Academic Levels</field>
|
||||||
|
<field name="res_model">education.academic.level</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_education_academic_level_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
72
addons/education_base/views/academic_term_views.xml
Normal file
72
addons/education_base/views/academic_term_views.xml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_academic_term_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.term.tree</field>
|
||||||
|
<field name="model">education.academic.term</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Academic Terms">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="academic_year_id"/>
|
||||||
|
<field name="sequence"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_academic_term_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.term.form</field>
|
||||||
|
<field name="model">education.academic.term</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Academic Term">
|
||||||
|
<header>
|
||||||
|
<button name="action_activate" type="object" string="Activate" class="btn-primary" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_close" type="object" string="Close" invisible="state != 'active'"/>
|
||||||
|
<field name="state" widget="statusbar"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="academic_year_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="sequence"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_academic_term_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.term.search</field>
|
||||||
|
<field name="model">education.academic.term</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Academic Terms">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="academic_year_id"/>
|
||||||
|
<filter string="Active" name="active" domain="[('state', '=', 'active')]"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Academic Year" name="academic_year" context="{'group_by': 'academic_year_id'}"/>
|
||||||
|
<filter string="Company" name="company" context="{'group_by': 'company_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_academic_term" model="ir.actions.act_window">
|
||||||
|
<field name="name">Academic Terms</field>
|
||||||
|
<field name="res_model">education.academic.term</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_education_academic_term_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
91
addons/education_base/views/academic_year_views.xml
Normal file
91
addons/education_base/views/academic_year_views.xml
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_academic_year_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.year.tree</field>
|
||||||
|
<field name="model">education.academic.year</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Academic Years"
|
||||||
|
decoration-success="state == 'active'"
|
||||||
|
decoration-danger="state == 'inactive'"
|
||||||
|
decoration-muted="state in ('closed', 'draft')">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="state" widget="badge"
|
||||||
|
decoration-success="state == 'active'"
|
||||||
|
decoration-danger="state == 'inactive'"
|
||||||
|
decoration-secondary="state in ('closed', 'draft')"/>
|
||||||
|
<field name="current_year"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_academic_year_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.year.form</field>
|
||||||
|
<field name="model">education.academic.year</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Academic Year">
|
||||||
|
<header>
|
||||||
|
<button name="action_activate" type="object" string="Activate" class="btn-primary" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_close" type="object" string="Close" invisible="state != 'active'"/>
|
||||||
|
<button name="action_set_draft" type="object" string="Set to Draft" invisible="state != 'closed'"/>
|
||||||
|
<field name="state" widget="statusbar"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="current_year"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Terms">
|
||||||
|
<field name="term_ids" context="{'default_academic_year_id': id}">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="sequence"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_academic_year_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.academic.year.search</field>
|
||||||
|
<field name="model">education.academic.year</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Academic Years">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<filter string="Current Year" name="current_year" domain="[('current_year', '=', True)]"/>
|
||||||
|
<filter string="Active" name="active" domain="[('state', '=', 'active')]"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Company" name="company" context="{'group_by': 'company_id'}"/>
|
||||||
|
<filter string="State" name="state" context="{'group_by': 'state'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_academic_year" model="ir.actions.act_window">
|
||||||
|
<field name="name">Academic Years</field>
|
||||||
|
<field name="res_model">education.academic.year</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_education_academic_year_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
73
addons/education_base/views/campus_views.xml
Normal file
73
addons/education_base/views/campus_views.xml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_campus_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.campus.tree</field>
|
||||||
|
<field name="model">education.campus</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Campuses">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="phone"/>
|
||||||
|
<field name="email"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_campus_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.campus.form</field>
|
||||||
|
<field name="model">education.campus</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Campus">
|
||||||
|
<sheet>
|
||||||
|
<field name="logo" widget="image" class="oe_avatar"/>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="phone"/>
|
||||||
|
<field name="email"/>
|
||||||
|
<field name="address_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Departments">
|
||||||
|
<field name="department_ids">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="head_teacher_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_campus_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.campus.search</field>
|
||||||
|
<field name="model">education.campus</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Campuses">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<filter string="Archived" name="archived" domain="[('active', '=', False)]"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Company" name="company" context="{'group_by': 'company_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_campus" model="ir.actions.act_window">
|
||||||
|
<field name="name">Campuses</field>
|
||||||
|
<field name="res_model">education.campus</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_education_campus_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
72
addons/education_base/views/department_views.xml
Normal file
72
addons/education_base/views/department_views.xml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_department_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.department.tree</field>
|
||||||
|
<field name="model">education.department</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Departments">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="campus_id"/>
|
||||||
|
<field name="head_teacher_id"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_department_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.department.form</field>
|
||||||
|
<field name="model">education.department</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Department">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="campus_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="head_teacher_id"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Programs">
|
||||||
|
<field name="program_ids">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="level_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_department_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.department.search</field>
|
||||||
|
<field name="model">education.department</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Departments">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="campus_id"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Campus" name="campus" context="{'group_by': 'campus_id'}"/>
|
||||||
|
<filter string="Company" name="company" context="{'group_by': 'company_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_department" model="ir.actions.act_window">
|
||||||
|
<field name="name">Departments</field>
|
||||||
|
<field name="res_model">education.department</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_education_department_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
70
addons/education_base/views/education_config_views.xml
Normal file
70
addons/education_base/views/education_config_views.xml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_configuration_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.configuration.tree</field>
|
||||||
|
<field name="model">education.configuration</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Education Configuration">
|
||||||
|
<field name="school_name"/>
|
||||||
|
<field name="school_code"/>
|
||||||
|
<field name="default_campus_id"/>
|
||||||
|
<field name="default_academic_year_id"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_configuration_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.configuration.form</field>
|
||||||
|
<field name="model">education.configuration</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Education Configuration">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="school_name"/>
|
||||||
|
<field name="school_code"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="default_campus_id"/>
|
||||||
|
<field name="default_academic_year_id"/>
|
||||||
|
<field name="default_term_id"/>
|
||||||
|
<field name="default_currency_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="contact_email"/>
|
||||||
|
<field name="contact_phone"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="logo" widget="image" class="oe_avatar"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_configuration_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.configuration.search</field>
|
||||||
|
<field name="model">education.configuration</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Education Configuration">
|
||||||
|
<field name="school_name"/>
|
||||||
|
<field name="school_code"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Company" name="company" context="{'group_by': 'company_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_configuration" model="ir.actions.act_window">
|
||||||
|
<field name="name">Education Configuration</field>
|
||||||
|
<field name="res_model">education.configuration</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_education_configuration_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
11
addons/education_base/views/menuitems.xml
Normal file
11
addons/education_base/views/menuitems.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<menuitem id="menu_education_base_academic_year" name="Academic Years" parent="menu_education_base_master_data" action="action_education_academic_year" sequence="10" groups="base.group_user"/>
|
||||||
|
<menuitem id="menu_education_base_academic_term" name="Academic Terms" parent="menu_education_base_master_data" action="action_education_academic_term" sequence="20" groups="base.group_user"/>
|
||||||
|
<menuitem id="menu_education_base_campus" name="Campuses" parent="menu_education_base_master_data" action="action_education_campus" sequence="30" groups="base.group_user"/>
|
||||||
|
<menuitem id="menu_education_base_department" name="Departments" parent="menu_education_base_master_data" action="action_education_department" sequence="40" groups="base.group_user"/>
|
||||||
|
<menuitem id="menu_education_base_academic_level" name="Academic Levels" parent="menu_education_base_master_data" action="action_education_academic_level" sequence="50" groups="base.group_user"/>
|
||||||
|
<menuitem id="menu_education_base_program" name="Programs" parent="menu_education_base_master_data" action="action_education_program" sequence="60" groups="base.group_user"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_education_base_config" name="School Settings" parent="menu_education_base_configuration" action="action_education_configuration" sequence="10" groups="base.group_user"/>
|
||||||
|
</odoo>
|
||||||
70
addons/education_base/views/program_views.xml
Normal file
70
addons/education_base/views/program_views.xml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_education_program_tree" model="ir.ui.view">
|
||||||
|
<field name="name">education.program.tree</field>
|
||||||
|
<field name="model">education.program</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Programs">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="level_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="duration"/>
|
||||||
|
<field name="duration_unit"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_program_form" model="ir.ui.view">
|
||||||
|
<field name="name">education.program.form</field>
|
||||||
|
<field name="model">education.program</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Program">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="level_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="duration"/>
|
||||||
|
<field name="duration_unit"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="description"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_education_program_search" model="ir.ui.view">
|
||||||
|
<field name="name">education.program.search</field>
|
||||||
|
<field name="model">education.program</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Programs">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="level_id"/>
|
||||||
|
<group>
|
||||||
|
<filter string="Department" name="department" context="{'group_by': 'department_id'}"/>
|
||||||
|
<filter string="Academic Level" name="level" context="{'group_by': 'level_id'}"/>
|
||||||
|
<filter string="Company" name="company" context="{'group_by': 'company_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_education_program" model="ir.actions.act_window">
|
||||||
|
<field name="name">Programs</field>
|
||||||
|
<field name="res_model">education.program</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_education_program_search"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
2
addons/education_dashboard/__init__.py
Normal file
2
addons/education_dashboard/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
36
addons/education_dashboard/__manifest__.py
Normal file
36
addons/education_dashboard/__manifest__.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
'name': 'Education Dashboard',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Education',
|
||||||
|
'summary': 'Interactive Management Dashboard for Education ERP',
|
||||||
|
'description': """
|
||||||
|
Executive management dashboard providing:
|
||||||
|
- Student KPIs: totals, active enrollment, admissions, graduation rates
|
||||||
|
- Finance KPIs: total fees, collections, outstanding dues
|
||||||
|
- Academic KPIs: program counts, departments, campuses
|
||||||
|
- Dynamic interactive Chart.js widgets (student distribution, fee collections)
|
||||||
|
- Dynamic filters (Campus, Department, Program, Academic Year, Semester)
|
||||||
|
- Dynamic drill-down actions directly linking to filtered records
|
||||||
|
""",
|
||||||
|
'author': 'Senior Solution Architect',
|
||||||
|
'depends': [
|
||||||
|
'education_base',
|
||||||
|
'education_student',
|
||||||
|
'education_finance',
|
||||||
|
'education_attendance',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/dashboard_views.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'/education_dashboard/static/src/js/education_dashboard.js',
|
||||||
|
'/education_dashboard/static/src/xml/education_dashboard_templates.xml',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
BIN
addons/education_dashboard/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/education_dashboard/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
addons/education_dashboard/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/education_dashboard/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
1
addons/education_dashboard/controllers/__init__.py
Normal file
1
addons/education_dashboard/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import main
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
161
addons/education_dashboard/controllers/main.py
Normal file
161
addons/education_dashboard/controllers/main.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo import http, fields
|
||||||
|
# pyrefly: ignore [missing-import]
|
||||||
|
from odoo.http import request
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class EducationDashboardController(http.Controller):
|
||||||
|
|
||||||
|
@http.route(['/web/education/dashboard/kpis'], type='json', auth="user")
|
||||||
|
def get_dashboard_kpis(self, campus_id=None, program_id=None, academic_year_id=None, semester_id=None, env=None, **kw):
|
||||||
|
env = env or request.env
|
||||||
|
|
||||||
|
# ── Filters ──────────────────────────────────────────────────────────
|
||||||
|
student_domain = []
|
||||||
|
if campus_id:
|
||||||
|
student_domain.append(('campus_id', '=', int(campus_id)))
|
||||||
|
if program_id:
|
||||||
|
student_domain.append(('program_id', '=', int(program_id)))
|
||||||
|
if academic_year_id:
|
||||||
|
student_domain.append(('academic_year_id', '=', int(academic_year_id)))
|
||||||
|
if semester_id:
|
||||||
|
student_domain.append(('semester_id', '=', int(semester_id)))
|
||||||
|
|
||||||
|
# ── Students ─────────────────────────────────────────────────────────
|
||||||
|
students = env['education.student'].search(student_domain)
|
||||||
|
total_students = len(students)
|
||||||
|
active_students = len(students.filtered(lambda s: s.status == 'active'))
|
||||||
|
admissions = len(students.filtered(lambda s: s.status == 'admitted'))
|
||||||
|
graduates = len(students.filtered(lambda s: s.status == 'graduated'))
|
||||||
|
|
||||||
|
# ── Finance ───────────────────────────────────────────────────────────
|
||||||
|
total_fees = sum(students.mapped('total_fees'))
|
||||||
|
collected_fees = sum(students.mapped('total_paid'))
|
||||||
|
outstanding_fees = sum(students.mapped('outstanding_amount'))
|
||||||
|
fee_percent = int(collected_fees / total_fees * 100) if total_fees > 0 else 0
|
||||||
|
|
||||||
|
# ── Academic counts ───────────────────────────────────────────────────
|
||||||
|
programs_count = env['education.program'].search_count([])
|
||||||
|
dept_count = env['education.department'].search_count([])
|
||||||
|
campus_count = env['education.campus'].search_count([])
|
||||||
|
years_count = env['education.academic.year'].search_count([])
|
||||||
|
|
||||||
|
# ── Charts: Program distribution ──────────────────────────────────────
|
||||||
|
program_chart = []
|
||||||
|
for gd in env['education.student'].read_group(student_domain, ['program_id'], ['program_id']):
|
||||||
|
if gd.get('program_id'):
|
||||||
|
program_chart.append({'name': gd['program_id'][1], 'count': gd['program_id_count']})
|
||||||
|
|
||||||
|
# ── Charts: Campus distribution ───────────────────────────────────────
|
||||||
|
campus_chart = []
|
||||||
|
for gd in env['education.student'].read_group(student_domain, ['campus_id'], ['campus_id']):
|
||||||
|
if gd.get('campus_id'):
|
||||||
|
campus_chart.append({'name': gd['campus_id'][1], 'count': gd['campus_id_count']})
|
||||||
|
|
||||||
|
# ── Monthly enrollment trend (current calendar year) ─────────────────
|
||||||
|
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||||
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||||
|
cur_year = datetime.date.today().year
|
||||||
|
enrollment_trend = []
|
||||||
|
cumulative = 0
|
||||||
|
for m in range(1, 13):
|
||||||
|
ny = cur_year if m < 12 else cur_year + 1
|
||||||
|
nm = m + 1 if m < 12 else 1
|
||||||
|
count = env['education.student'].search_count([
|
||||||
|
('create_date', '>=', f'{cur_year}-{m:02d}-01'),
|
||||||
|
('create_date', '<', f'{ny}-{nm:02d}-01'),
|
||||||
|
])
|
||||||
|
cumulative += count
|
||||||
|
enrollment_trend.append({
|
||||||
|
'month': month_names[m - 1],
|
||||||
|
'new': count,
|
||||||
|
'total': cumulative if cumulative else total_students,
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── Upcoming exams ────────────────────────────────────────────────────
|
||||||
|
upcoming_exams = []
|
||||||
|
try:
|
||||||
|
exams = env['education.exam'].search(
|
||||||
|
[('state', 'not in', ['cancelled', 'completed', 'done'])],
|
||||||
|
order='date asc', limit=4
|
||||||
|
)
|
||||||
|
for e in exams:
|
||||||
|
upcoming_exams.append({
|
||||||
|
'name': e.name,
|
||||||
|
'date': e.date.strftime('%b %d, %Y') if e.date else '',
|
||||||
|
'program': e.program_id.name if getattr(e, 'program_id', False) and e.program_id else '',
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── Recent activities ─────────────────────────────────────────────────
|
||||||
|
recent_activities = []
|
||||||
|
try:
|
||||||
|
msgs = env['mail.message'].search([
|
||||||
|
('model', 'in', ['education.student', 'education.exam', 'education.fee.payment']),
|
||||||
|
('message_type', 'in', ['email', 'comment']),
|
||||||
|
], order='date desc', limit=5)
|
||||||
|
for msg in msgs:
|
||||||
|
body = msg.subject or ''
|
||||||
|
if not body and msg.body:
|
||||||
|
import re
|
||||||
|
body = re.sub('<[^>]+>', '', msg.body)[:60]
|
||||||
|
recent_activities.append({
|
||||||
|
'text': body or 'Activity recorded',
|
||||||
|
'time': msg.date.strftime('%I:%M %p') if msg.date else '',
|
||||||
|
'model': msg.model or '',
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── User / date context ───────────────────────────────────────────────
|
||||||
|
user_name = env.user.name
|
||||||
|
today_str = datetime.date.today().strftime('%A, %b %d, %Y')
|
||||||
|
current_year_name = ''
|
||||||
|
try:
|
||||||
|
cy = env['education.academic.year'].search([('current_year', '=', True)], limit=1)
|
||||||
|
if cy:
|
||||||
|
current_year_name = cy.name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── Filter dropdown data ──────────────────────────────────────────────
|
||||||
|
campuses = [{'id': c.id, 'name': c.name} for c in env['education.campus'].search([])]
|
||||||
|
programs = [{'id': p.id, 'name': p.name} for p in env['education.program'].search([])]
|
||||||
|
years = [{'id': y.id, 'name': y.name} for y in env['education.academic.year'].search([])]
|
||||||
|
semesters = [{'id': s.id, 'name': s.name} for s in env['education.semester'].search([])]
|
||||||
|
|
||||||
|
return {
|
||||||
|
# student KPIs
|
||||||
|
'total_students': total_students,
|
||||||
|
'active_students': active_students,
|
||||||
|
'admissions': admissions,
|
||||||
|
'graduates': graduates,
|
||||||
|
# finance
|
||||||
|
'total_fees': total_fees,
|
||||||
|
'collected_fees': collected_fees,
|
||||||
|
'outstanding_fees': outstanding_fees,
|
||||||
|
'fee_percent': fee_percent,
|
||||||
|
# academic
|
||||||
|
'programs_count': programs_count,
|
||||||
|
'dept_count': dept_count,
|
||||||
|
'campus_count': campus_count,
|
||||||
|
'years_count': years_count,
|
||||||
|
# charts
|
||||||
|
'program_chart': program_chart,
|
||||||
|
'campus_chart': campus_chart,
|
||||||
|
'enrollment_trend': enrollment_trend,
|
||||||
|
# operational
|
||||||
|
'upcoming_exams': upcoming_exams,
|
||||||
|
'recent_activities': recent_activities,
|
||||||
|
# context
|
||||||
|
'user_name': user_name,
|
||||||
|
'today': today_str,
|
||||||
|
'current_year_name': current_year_name,
|
||||||
|
# filter options
|
||||||
|
'campuses': campuses,
|
||||||
|
'programs': programs,
|
||||||
|
'years': years,
|
||||||
|
'semesters': semesters,
|
||||||
|
}
|
||||||
1
addons/education_dashboard/models/__init__.py
Normal file
1
addons/education_dashboard/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import dashboard
|
||||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user