full erp files
This commit is contained in:
commit
dccaef02f4
438
EDUCATION_MODULES_SUMMARY.md
Normal file
438
EDUCATION_MODULES_SUMMARY.md
Normal file
@ -0,0 +1,438 @@
|
||||
# 📚 Complete Technical Documentation of Custom Education Odoo Addons
|
||||
|
||||
This document contains a comprehensive, field-level technical summary of all **7 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.
|
||||
|
||||
---
|
||||
|
||||
## 📋 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 |
|
||||
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': '17.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.
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.
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,
|
||||
)
|
||||
161
addons/education_attendance/models/attendance_session.py
Normal file
161
addons/education_attendance/models/attendance_session.py
Normal file
@ -0,0 +1,161 @@
|
||||
# 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,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('uniq_session_period', '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,57 @@
|
||||
<?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="category_id" ref="module_category_education_attendance"/>
|
||||
<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="category_id" ref="module_category_education_attendance"/>
|
||||
<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['education.attendance.session'].flush_model()
|
||||
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>
|
||||
188
addons/education_attendance/views/attendance_session_views.xml
Normal file
188
addons/education_attendance/views/attendance_session_views.xml
Normal file
@ -0,0 +1,188 @@
|
||||
<?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">
|
||||
<tree 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'"/>
|
||||
</tree>
|
||||
</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="kanban-box">
|
||||
<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'">
|
||||
<tree 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"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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">
|
||||
<tree 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"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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': active_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': '17.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.
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="10" groups="base.group_user" web_icon="fa fa-graduation-cap"/>
|
||||
<menuitem id="menu_education_base_configuration" name="Configuration" parent="menu_education_base_root" sequence="1" 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.
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.
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.
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'})
|
||||
69
addons/education_base/models/academic_year.py
Normal file
69
addons/education_base/models/academic_year.py
Normal file
@ -0,0 +1,69 @@
|
||||
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
|
||||
def create(self, vals):
|
||||
if vals.get('state') == 'active':
|
||||
vals['current_year'] = True
|
||||
return super().create(vals)
|
||||
|
||||
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'})
|
||||
52
addons/education_base/models/campus.py
Normal file
52
addons/education_base/models/campus.py
Normal file
@ -0,0 +1,52 @@
|
||||
# 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
|
||||
def create(self, vals):
|
||||
if vals.get('code'):
|
||||
vals['code'] = vals['code'].strip().upper()
|
||||
return super().create(vals)
|
||||
|
||||
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
|
||||
27
addons/education_base/security/education_base_security.xml
Normal file
27
addons/education_base/security/education_base_security.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?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="category_id" ref="module_category_education_base"/>
|
||||
<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="category_id" ref="module_category_education_base"/>
|
||||
<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="category_id" ref="module_category_education_base"/>
|
||||
<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
|
||||
BIN
addons/education_base/tests/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/education_base/tests/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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',
|
||||
'groups_id': [(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">
|
||||
<tree 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"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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">
|
||||
<tree 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"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,form</field>
|
||||
<field name="search_view_id" ref="view_education_academic_term_search"/>
|
||||
</record>
|
||||
</odoo>
|
||||
85
addons/education_base/views/academic_year_views.xml
Normal file
85
addons/education_base/views/academic_year_views.xml
Normal file
@ -0,0 +1,85 @@
|
||||
<?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">
|
||||
<tree string="Academic Years">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="state"/>
|
||||
<field name="current_year"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</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': active_id}">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="sequence"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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">
|
||||
<tree string="Campuses">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="phone"/>
|
||||
<field name="email"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</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">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="head_teacher_id"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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">
|
||||
<tree 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"/>
|
||||
</tree>
|
||||
</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">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="level_id"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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">
|
||||
<tree 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"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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">
|
||||
<tree 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"/>
|
||||
</tree>
|
||||
</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 expand="1" string="Group By">
|
||||
<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">tree,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': '17.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.
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.
104
addons/education_dashboard/controllers/main.py
Normal file
104
addons/education_dashboard/controllers/main.py
Normal file
@ -0,0 +1,104 @@
|
||||
# pyrefly: ignore [missing-import]
|
||||
from odoo import http
|
||||
# pyrefly: ignore [missing-import]
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
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
|
||||
# Base Student Domain
|
||||
student_domain = []
|
||||
payment_domain = [('status', '=', 'posted')]
|
||||
|
||||
# Apply Filters
|
||||
if campus_id:
|
||||
student_domain.append(('campus_id', '=', int(campus_id)))
|
||||
payment_domain.append(('student_id.campus_id', '=', int(campus_id)))
|
||||
if program_id:
|
||||
student_domain.append(('program_id', '=', int(program_id)))
|
||||
payment_domain.append(('student_id.program_id', '=', int(program_id)))
|
||||
if academic_year_id:
|
||||
student_domain.append(('academic_year_id', '=', int(academic_year_id)))
|
||||
payment_domain.append(('student_id.academic_year_id', '=', int(academic_year_id)))
|
||||
if semester_id:
|
||||
student_domain.append(('semester_id', '=', int(semester_id)))
|
||||
payment_domain.append(('student_id.semester_id', '=', int(semester_id)))
|
||||
|
||||
# Fetch Student Records
|
||||
students = env['education.student'].search(student_domain)
|
||||
|
||||
# Executive KPIs
|
||||
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'))
|
||||
withdrawn = len(students.filtered(lambda s: s.status == 'withdrawn'))
|
||||
|
||||
# Finance KPIs
|
||||
total_fees = sum(students.mapped('total_fees'))
|
||||
collected_fees = sum(students.mapped('total_paid'))
|
||||
outstanding_fees = sum(students.mapped('outstanding_amount'))
|
||||
|
||||
# Academic KPIs
|
||||
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 (using read_group for speed)
|
||||
program_data = env['education.student'].read_group(
|
||||
student_domain, ['program_id'], ['program_id']
|
||||
)
|
||||
program_chart = []
|
||||
for gd in program_data:
|
||||
if gd.get('program_id'):
|
||||
program_chart.append({
|
||||
'name': gd['program_id'][1],
|
||||
'count': gd['program_id_count'],
|
||||
})
|
||||
|
||||
# Charts: Campus Distribution
|
||||
campus_data = env['education.student'].read_group(
|
||||
student_domain, ['campus_id'], ['campus_id']
|
||||
)
|
||||
campus_chart = []
|
||||
for gd in campus_data:
|
||||
if gd.get('campus_id'):
|
||||
campus_chart.append({
|
||||
'name': gd['campus_id'][1],
|
||||
'count': gd['campus_id_count'],
|
||||
})
|
||||
|
||||
# Master Data lists for Filters
|
||||
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 {
|
||||
'total_students': total_students,
|
||||
'active_students': active_students,
|
||||
'admissions': admissions,
|
||||
'graduates': graduates,
|
||||
'withdrawn': withdrawn,
|
||||
|
||||
'total_fees': total_fees,
|
||||
'collected_fees': collected_fees,
|
||||
'outstanding_fees': outstanding_fees,
|
||||
|
||||
'programs_count': programs_count,
|
||||
'dept_count': dept_count,
|
||||
'campus_count': campus_count,
|
||||
'years_count': years_count,
|
||||
|
||||
'program_chart': program_chart,
|
||||
'campus_chart': campus_chart,
|
||||
|
||||
'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.
7
addons/education_dashboard/models/dashboard.py
Normal file
7
addons/education_dashboard/models/dashboard.py
Normal file
@ -0,0 +1,7 @@
|
||||
# pyrefly: ignore [missing-import]
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class EducationDashboard(models.TransientModel):
|
||||
_name = 'education.dashboard'
|
||||
_description = 'Education Dashboard Helper'
|
||||
2
addons/education_dashboard/security/ir.model.access.csv
Normal file
2
addons/education_dashboard/security/ir.model.access.csv
Normal file
@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_education_dashboard,Education Dashboard,model_education_dashboard,base.group_user,1,1,1,1
|
||||
|
137
addons/education_dashboard/static/src/js/education_dashboard.js
Normal file
137
addons/education_dashboard/static/src/js/education_dashboard.js
Normal file
@ -0,0 +1,137 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component, onWillStart, onMounted, useState, useRef } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { loadJS } from "@web/core/assets";
|
||||
|
||||
class EducationDashboard extends Component {
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
this.actionService = useService("action");
|
||||
this.state = useState({
|
||||
kpis: {},
|
||||
filters: {
|
||||
campus_id: "",
|
||||
program_id: "",
|
||||
academic_year_id: "",
|
||||
semester_id: ""
|
||||
}
|
||||
});
|
||||
|
||||
this.programChartRef = useRef("programChart");
|
||||
this.campusChartRef = useRef("campusChart");
|
||||
|
||||
onWillStart(async () => {
|
||||
await loadJS("/web/static/lib/Chart/Chart.js");
|
||||
await this.loadData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
this.renderCharts();
|
||||
});
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
// Map empty string filters to null
|
||||
const params = {};
|
||||
for (const [k, v] of Object.entries(this.state.filters)) {
|
||||
params[k] = v ? parseInt(v) : null;
|
||||
}
|
||||
const data = await this.rpc("/web/education/dashboard/kpis", params);
|
||||
this.state.kpis = data;
|
||||
}
|
||||
|
||||
async onFilterChange(filterKey, ev) {
|
||||
this.state.filters[filterKey] = ev.target.value;
|
||||
await this.loadData();
|
||||
this.renderCharts();
|
||||
}
|
||||
|
||||
renderCharts() {
|
||||
const pCtx = this.programChartRef.el;
|
||||
if (pCtx) {
|
||||
if (this.pChart) this.pChart.destroy();
|
||||
const labels = this.state.kpis.program_chart.map(x => x.name);
|
||||
const data = this.state.kpis.program_chart.map(x => x.count);
|
||||
this.pChart = new Chart(pCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Students Count',
|
||||
data: data,
|
||||
backgroundColor: '#4F81BD'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { stepSize: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const cCtx = this.campusChartRef.el;
|
||||
if (cCtx) {
|
||||
if (this.cChart) this.cChart.destroy();
|
||||
const labels = this.state.kpis.campus_chart.map(x => x.name);
|
||||
const data = this.state.kpis.campus_chart.map(x => x.count);
|
||||
this.cChart = new Chart(cCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
backgroundColor: ['#4F81BD', '#C0504D', '#9BBB59', '#8064A2', '#4BACC6']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openStudents(status) {
|
||||
const domain = [];
|
||||
if (status) {
|
||||
domain.push(['status', '=', status]);
|
||||
}
|
||||
if (this.state.filters.campus_id) {
|
||||
domain.push(['campus_id', '=', parseInt(this.state.filters.campus_id)]);
|
||||
}
|
||||
if (this.state.filters.program_id) {
|
||||
domain.push(['program_id', '=', parseInt(this.state.filters.program_id)]);
|
||||
}
|
||||
|
||||
this.actionService.doAction({
|
||||
name: "Students List",
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "education.student",
|
||||
views: [[false, "list"], [false, "form"]],
|
||||
domain: domain,
|
||||
target: "current"
|
||||
});
|
||||
}
|
||||
|
||||
openPrograms() {
|
||||
this.actionService.doAction({
|
||||
name: "Active Programs",
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "education.program",
|
||||
views: [[false, "list"], [false, "form"]],
|
||||
target: "current"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EducationDashboard.template = "education_dashboard.DashboardMain";
|
||||
registry.category("actions").add("education_dashboard_main_tag", EducationDashboard);
|
||||
export { EducationDashboard };
|
||||
@ -0,0 +1,172 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="education_dashboard.DashboardMain">
|
||||
<div class="container-fluid py-4 bg-light overflow-auto" style="height: 100vh;">
|
||||
<!-- Filter Bar -->
|
||||
<div class="row bg-white p-3 shadow-sm rounded mb-4 mx-1">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label font-weight-bold small text-uppercase text-muted">Campus</label>
|
||||
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('campus_id', ev)">
|
||||
<option value="">All Campuses</option>
|
||||
<t t-foreach="state.kpis.campuses || []" t-as="c" t-key="c.id">
|
||||
<option t-att-value="c.id"><t t-esc="c.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label font-weight-bold small text-uppercase text-muted">Program</label>
|
||||
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('program_id', ev)">
|
||||
<option value="">All Programs</option>
|
||||
<t t-foreach="state.kpis.programs || []" t-as="p" t-key="p.id">
|
||||
<option t-att-value="p.id"><t t-esc="p.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label font-weight-bold small text-uppercase text-muted">Academic Year</label>
|
||||
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('academic_year_id', ev)">
|
||||
<option value="">All Years</option>
|
||||
<t t-foreach="state.kpis.years || []" t-as="y" t-key="y.id">
|
||||
<option t-att-value="y.id"><t t-esc="y.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label font-weight-bold small text-uppercase text-muted">Semester</label>
|
||||
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('semester_id', ev)">
|
||||
<option value="">All Semesters</option>
|
||||
<t t-foreach="state.kpis.semesters || []" t-as="s" t-key="s.id">
|
||||
<option t-att-value="s.id"><t t-esc="s.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Executive KPIs -->
|
||||
<div class="row mb-4 mx-1">
|
||||
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('')" style="cursor: pointer;">
|
||||
<div class="card shadow-sm border-0 bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-uppercase small mb-1">Total Students</h6>
|
||||
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.total_students || 0"/></h2>
|
||||
</div>
|
||||
<i class="fa fa-users fa-3x opacity-50"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('active')" style="cursor: pointer;">
|
||||
<div class="card shadow-sm border-0 bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-uppercase small mb-1">Active Students</h6>
|
||||
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.active_students || 0"/></h2>
|
||||
</div>
|
||||
<i class="fa fa-user-check fa-3x opacity-50"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('admitted')" style="cursor: pointer;">
|
||||
<div class="card shadow-sm border-0 bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-uppercase small mb-1">Admissions</h6>
|
||||
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.admissions || 0"/></h2>
|
||||
</div>
|
||||
<i class="fa fa-user-plus fa-3x opacity-50"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('graduated')" style="cursor: pointer;">
|
||||
<div class="card shadow-sm border-0 bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="text-uppercase small mb-1">Graduates</h6>
|
||||
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.graduates || 0"/></h2>
|
||||
</div>
|
||||
<i class="fa fa-graduation-cap fa-3x opacity-50"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finance KPIs -->
|
||||
<div class="row mb-4 mx-1">
|
||||
<div class="col-md-4 mb-2">
|
||||
<div class="card shadow-sm border-0 bg-white p-3">
|
||||
<div class="text-muted small text-uppercase">Total Fees Assigned</div>
|
||||
<h2 class="font-weight-bold text-dark mb-0"><t t-esc="state.kpis.total_fees || 0"/></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-2">
|
||||
<div class="card shadow-sm border-0 bg-white p-3">
|
||||
<div class="text-success small text-uppercase">Collected Fees</div>
|
||||
<h2 class="font-weight-bold text-success mb-0"><t t-esc="state.kpis.collected_fees || 0"/></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-2">
|
||||
<div class="card shadow-sm border-0 bg-white p-3">
|
||||
<div class="text-danger small text-uppercase">Outstanding Fees</div>
|
||||
<h2 class="font-weight-bold text-danger mb-0"><t t-esc="state.kpis.outstanding_fees || 0"/></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="row mb-4 mx-1">
|
||||
<div class="col-md-8 mb-2">
|
||||
<div class="card shadow-sm border-0 p-3 bg-white" style="height: 350px;">
|
||||
<h5 class="card-title font-weight-bold">Program-wise Distribution</h5>
|
||||
<div class="position-relative flex-grow-1" style="height: 280px;">
|
||||
<canvas t-ref="programChart"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-2">
|
||||
<div class="card shadow-sm border-0 p-3 bg-white" style="height: 350px;">
|
||||
<h5 class="card-title font-weight-bold">Campus Distribution</h5>
|
||||
<div class="position-relative flex-grow-1" style="height: 280px;">
|
||||
<canvas t-ref="campusChart"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Academic KPIs -->
|
||||
<div class="row mb-4 mx-1 text-center">
|
||||
<div class="col-md-3 mb-2" t-on-click="openPrograms" style="cursor: pointer;">
|
||||
<div class="card shadow-sm border-0 bg-white p-3">
|
||||
<div class="text-muted small text-uppercase">Programs</div>
|
||||
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.programs_count || 0"/></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card shadow-sm border-0 bg-white p-3">
|
||||
<div class="text-muted small text-uppercase">Departments</div>
|
||||
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.dept_count || 0"/></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card shadow-sm border-0 bg-white p-3">
|
||||
<div class="text-muted small text-uppercase">Campuses</div>
|
||||
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.campus_count || 0"/></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card shadow-sm border-0 bg-white p-3">
|
||||
<div class="text-muted small text-uppercase">Academic Years</div>
|
||||
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.years_count || 0"/></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
1
addons/education_dashboard/tests/__init__.py
Normal file
1
addons/education_dashboard/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import test_dashboard
|
||||
Binary file not shown.
Binary file not shown.
63
addons/education_dashboard/tests/test_dashboard.py
Normal file
63
addons/education_dashboard/tests/test_dashboard.py
Normal file
@ -0,0 +1,63 @@
|
||||
# pyrefly: ignore [missing-import]
|
||||
from odoo.tests.common import TransactionCase
|
||||
# pyrefly: ignore [missing-import]
|
||||
from odoo.addons.education_dashboard.controllers.main import EducationDashboardController
|
||||
|
||||
|
||||
class TestEducationDashboard(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEducationDashboard, self).setUp()
|
||||
|
||||
self.controller = EducationDashboardController()
|
||||
|
||||
# Setup master records
|
||||
self.campus = self.env['education.campus'].create({'name': 'Dash Campus', 'code': 'DC'})
|
||||
self.department = self.env['education.department'].create({'name': 'Dash Dept', 'code': 'DD', 'campus_id': self.campus.id})
|
||||
self.level = self.env['education.academic.level'].create({'name': 'Dash Level', 'code': 'DL'})
|
||||
self.program = self.env['education.program'].create({'name': 'Dash Prog', 'code': 'DP', 'department_id': self.department.id, 'level_id': self.level.id})
|
||||
self.year = self.env['education.academic.year'].create({'name': 'Dash Year', 'code': 'DY', 'start_date': '2026-01-01', 'end_date': '2026-12-31'})
|
||||
self.term = self.env['education.academic.term'].create({'name': 'Dash Term', 'code': 'DT', 'academic_year_id': self.year.id, 'start_date': '2026-01-01', 'end_date': '2026-06-30'})
|
||||
self.batch = self.env['education.batch'].create({'name': 'Dash Batch', 'code': 'DB'})
|
||||
self.semester = self.env['education.semester'].create({'name': 'Dash Sem', 'code': 'DS'})
|
||||
self.section = self.env['education.section'].create({'name': 'Dash Sec', 'code': 'DSEC'})
|
||||
|
||||
# Student
|
||||
self.student = self.env['education.student'].create({
|
||||
'first_name': 'Dashboard',
|
||||
'last_name': 'TestStudent',
|
||||
'student_id': 'DASHSTU1',
|
||||
'admission_number': 'DASHADM1',
|
||||
'roll_number': 'DASHRL1',
|
||||
'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',
|
||||
})
|
||||
|
||||
def test_dashboard_controller_kpis(self):
|
||||
"""Test that dashboard JSON controller returns valid and accurate stats"""
|
||||
|
||||
# Test default/no filter KPIs
|
||||
kpi_data = self.controller.get_dashboard_kpis(env=self.env)
|
||||
|
||||
self.assertGreaterEqual(kpi_data['total_students'], 1)
|
||||
self.assertGreaterEqual(kpi_data['active_students'], 1)
|
||||
self.assertEqual(kpi_data['admissions'], 0)
|
||||
self.assertGreaterEqual(kpi_data['years_count'], 1)
|
||||
self.assertGreaterEqual(kpi_data['programs_count'], 1)
|
||||
|
||||
# Test filtering by specific campus
|
||||
kpi_filtered = self.controller.get_dashboard_kpis(campus_id=self.campus.id, env=self.env)
|
||||
self.assertEqual(kpi_filtered['total_students'], 1)
|
||||
|
||||
# Test filtering by dummy campus (should return 0)
|
||||
kpi_empty = self.controller.get_dashboard_kpis(campus_id=9999, env=self.env)
|
||||
self.xlsx_file = None # or anything
|
||||
self.assertEqual(kpi_empty['total_students'], 0)
|
||||
16
addons/education_dashboard/views/dashboard_views.xml
Normal file
16
addons/education_dashboard/views/dashboard_views.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Client Action for OWL Dashboard -->
|
||||
<record id="action_education_dashboard_main" model="ir.actions.client">
|
||||
<field name="name">Management Dashboard</field>
|
||||
<field name="tag">education_dashboard_main_tag</field>
|
||||
</record>
|
||||
|
||||
<!-- Top-level Menu Item -->
|
||||
<menuitem id="menu_education_dashboard_root"
|
||||
name="Dashboard"
|
||||
parent="education_base.menu_education_base_root"
|
||||
action="action_education_dashboard_main"
|
||||
sequence="1"
|
||||
groups="education_student.group_education_student_manager"/>
|
||||
</odoo>
|
||||
2
addons/education_exam/__init__.py
Normal file
2
addons/education_exam/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
33
addons/education_exam/__manifest__.py
Normal file
33
addons/education_exam/__manifest__.py
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
'name': 'Education Examinations',
|
||||
'version': '1.0',
|
||||
'category': 'Education',
|
||||
'summary': 'Complete College Examination Module',
|
||||
'description': """
|
||||
Odoo Education Examination Module.
|
||||
Features:
|
||||
- Exam scheduling
|
||||
- Grading systems
|
||||
- Marks entry
|
||||
- Portal integration for students to view results and schedule.
|
||||
""",
|
||||
'author': 'Antigravity',
|
||||
'website': 'https://www.example.com',
|
||||
'depends': ['education_base', 'education_student', 'education_portal', 'portal'],
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/grading_data.xml',
|
||||
'views/education_grading_views.xml',
|
||||
'views/education_subject_views.xml',
|
||||
'views/education_exam_views.xml',
|
||||
'views/education_exam_result_views.xml',
|
||||
'views/education_student_inherit_views.xml',
|
||||
'views/portal_exam_templates.xml',
|
||||
'views/menu_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
BIN
addons/education_exam/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
addons/education_exam/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
1
addons/education_exam/controllers/__init__.py
Normal file
1
addons/education_exam/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import portal_exam
|
||||
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