diff --git a/spp_case_session/views/session_views.xml b/spp_case_session/views/session_views.xml index 3dd9db1b..a1bb9964 100644 --- a/spp_case_session/views/session_views.xml +++ b/spp_case_session/views/session_views.xml @@ -6,17 +6,15 @@ spp.session - -
- -
+ +
diff --git a/spp_session_tracking/README.rst b/spp_session_tracking/README.rst index 163d9544..b6b2ca0f 100644 --- a/spp_session_tracking/README.rst +++ b/spp_session_tracking/README.rst @@ -10,9 +10,9 @@ OpenSPP Session Tracking !! source digest: sha256:696866918edb0694e649b8f7e2a237a7007f35de1ff18a503d87efc0cad14f7d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png :target: https://odoo-community.org/page/development-status - :alt: Beta + :alt: Production/Stable .. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 @@ -70,7 +70,8 @@ After installing: 1. Navigate to **Session Tracking > Configuration > Session Types** 2. Review pre-configured session types (Training, Family Development Session, Group Meeting, Workshop) -3. Add or modify session types and topics as needed +3. Add or modify session types as needed; topics are managed within each + session type form when topic tracking is enabled 4. Adjust required attendance percentage per session type Four session types are pre-configured with sample topics for Family @@ -84,7 +85,8 @@ UI Location to current user as facilitator) - **Configuration**: Session Tracking > Configuration > Session Types (managers only) -- **Views**: List, form, calendar (by date), kanban (grouped by state) +- **Views**: List, form, calendar (by date), kanban (grouped by state), + graph, pivot Security ~~~~~~~~ @@ -93,18 +95,22 @@ Security | Group | Access | +================================================+==================================+ | ``spp_session_tracking.group_session_user`` | Read all sessions and session | -| | types/topics; write own | -| | facilitated sessions; | -| | read/write/create attendance (no | +| | types/topics; create/write own | +| | facilitated or co-facilitated | +| | sessions; read/write/create | +| | attendance for own sessions (no | | | delete) | +------------------------------------------------+----------------------------------+ | ``spp_session_tracking.group_session_manager`` | Full CRUD on all sessions, | | | types, topics, and attendance | +------------------------------------------------+----------------------------------+ -The session user group can view all sessions but only edit sessions they -facilitate (via record rule). The ``spp_security.group_spp_admin`` group -implies manager access. +The session user group can view all sessions but only edit sessions +where they are the facilitator or co-facilitator (via record rules). +Session creation requires manager access. The +``spp_security.group_spp_admin`` group implies manager access. +Multi-company record rules ensure users only see sessions belonging to +their company. Extension Points ~~~~~~~~~~~~~~~~ @@ -125,6 +131,798 @@ Dependencies .. contents:: :local: +Usage +===== + +This guide covers all testable areas of the ``spp_session_tracking`` +module. It is organized by functional area, with step-by-step +instructions and expected results. + +Prerequisites +~~~~~~~~~~~~~ + +- Module ``spp_session_tracking`` is installed +- Two test users are available: + + - **Session User** (group: ``Session Tracking / User``) + - **Session Manager** (group: ``Session Tracking / Manager``) + +- Admin account is available for setup steps +- At least two contacts (res.partner) exist for use as participants + +1. Module Installation and Demo Data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**1.1 Verify Installation** + +1. Log in as admin +2. Go to **Apps**, search for "Session Tracking" +3. Confirm the module is installed + +**Expected**: Module appears as installed. + +**1.2 Verify Pre-configured Session Types** + +1. Navigate to **Session Tracking > Configuration > Session Types** +2. Verify 4 session types exist: + ++-------------+----------+-----------+----------+--------------+---------+ +| Name | Code | Frequency | Duration | Attendance % | Topics? | ++=============+==========+===========+==========+==============+=========+ +| Training | TRAINING | Monthly | 3.0h | 80% | Yes | +| Session | | | | | | ++-------------+----------+-----------+----------+--------------+---------+ +| Group | MEETING | Bi-weekly | 2.0h | 75% | No | +| Meeting | | | | | | ++-------------+----------+-----------+----------+--------------+---------+ +| Family | FDS | Monthly | 2.5h | 85% | Yes | +| Development | | | | | | +| Session | | | | | | ++-------------+----------+-----------+----------+--------------+---------+ +| Workshop | WORKSHOP | One-time | 4.0h | 90% | Yes | ++-------------+----------+-----------+----------+--------------+---------+ + +**Expected**: All 4 types are present with the values above. + +**1.3 Verify Pre-configured Topics** + +1. Open the **Family Development Session** type +2. Check the Topics section (visible because Track Topics is enabled) + +**Expected**: 4 topics exist: Nutrition and Health, Education and Child +Development, Financial Literacy, Positive Parenting. + +3. Open the **Training Session** type + +**Expected**: 2 topics exist: Livelihood Skills, Basic Business +Management. + +4. Open the **Group Meeting** type + +**Expected**: No Topics section is visible (Track Topics is disabled). + +2. Menu Structure and Navigation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**2.1 Main Menu** + +1. Log in as Session Manager +2. Check the main menu bar + +**Expected**: "Session Tracking" menu item is visible with the module +icon. + +**2.2 Submenu Structure** + +1. Click **Session Tracking** + +**Expected**: Two submenu groups are visible: + +- **Sessions**: All Sessions, My Sessions +- **Configuration**: Session Types + +**2.3 Configuration Menu Visibility** + +1. Log in as **Session User** +2. Navigate to Session Tracking + +**Expected**: The "Configuration" submenu is NOT visible. Only +"Sessions" submenu is shown. + +3. Log in as **Session Manager** + +**Expected**: Both "Sessions" and "Configuration" submenus are visible. + +3. Session Type Management (Manager Only) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**3.1 Create a Session Type** + +1. Log in as Session Manager +2. Go to **Session Tracking > Configuration > Session Types** +3. Click **New** +4. Fill in: + + - Name: "Health Workshop" + - Code: "HEALTH" + - Frequency: Quarterly + - Duration: 1.5 + - Required Attendance %: 70 + - Track Topics: enabled + +5. Save + +**Expected**: Session type is created. Topics section appears below. + +**3.2 Add Topics to a Session Type** + +1. Open the "Health Workshop" type created above +2. In the Topics section, click **Add a line** +3. Add topics: "Hygiene Practices", "First Aid Basics" +4. Save + +**Expected**: Topics are saved and listed under the session type. + +**3.3 Unique Code Constraint** + +1. Go to **Session Tracking > Configuration > Session Types** +2. Click **New** +3. Enter Name: "Duplicate Code Test", Code: "HEALTH" (same as above) +4. Save + +**Expected**: Error message: "Session type code must be unique." + +**3.4 Archive a Session Type** + +1. Open an existing session type +2. Use **Action > Archive** +3. Go to the list view and remove the "Active" filter + +**Expected**: The archived type shows as inactive (greyed out in list +view with muted decoration). + +**3.5 Session Count Stat Button** + +1. Open a session type that has sessions (e.g., create a session first) +2. Check the "Sessions" stat button in the top-right area + +**Expected**: The button shows the correct count of sessions for this +type. + +3. Click the stat button + +**Expected**: A filtered list of sessions for this type opens. + +**3.6 Session Type List View** + +1. Go to **Session Tracking > Configuration > Session Types** + +**Expected**: List displays columns: Name, Code, Frequency, Duration, +Required Attendance %, Sessions, Active. Inactive types are greyed out. + +**3.7 User Cannot Create Session Types** + +1. Log in as **Session User** +2. Try to access **Session Tracking > Configuration > Session Types** + +**Expected**: The Configuration menu is not visible. If accessed via +URL, an access error is raised. + +4. Session Creation and Form View +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**4.1 Create a Session (Manager)** + +1. Log in as Session Manager +2. Go to **Session Tracking > Sessions > All Sessions** +3. Click **New** +4. Fill in: + + - Name: "FDS - Barangay San Jose - March 2026" + - Session Type: Family Development Session + - Date: today's date + - Facilitator: (select a user) + - Co-Facilitators: (select one or more users) + - Location: "Barangay Hall" + - Start Time: 09:00 + - End Time: 11:30 + - Max Participants: 25 + +5. Save + +**Expected**: + +- Session is created in "Scheduled" state +- Duration shows 2.5 hours (auto-computed) +- Statusbar shows: Scheduled > In Progress > Completed +- "Start" button is visible and highlighted +- "Cancel" button is visible (not highlighted) +- "Complete" button is NOT visible + +**4.2 User Cannot Create a Session** + +1. Log in as **Session User** +2. Go to **Session Tracking > Sessions > All Sessions** +3. Try to click **New** + +**Expected**: An access error is raised. Session users cannot create +sessions. + +**4.3 Form View Structure** + +1. Open any session in form view + +**Expected**: + +- **Header**: Action buttons (Start/Complete/Cancel based on state) + + statusbar +- **Ribbons**: Green "Completed" ribbon on completed sessions, red + "Cancelled" ribbon on cancelled +- **Button Box**: "Attended" stat button with user icon (shows + attendance count) +- **Title**: Session name in large text +- **Main Info**: Two-column layout + + - Left: Session Type, Date, Location, Area + - Right: Facilitator, Co-Facilitators, Start Time, End Time, Duration, + Max Participants, Company + +- **Tabs**: Participants, Attendance, Topics (conditional), Notes +- **Chatter**: Message log and activity tracking at the bottom + +**4.4 Duration Auto-computation** + +1. Create or edit a session +2. Set Start Time: 09:00, End Time: 12:00 + +**Expected**: Duration shows 3.0 hours automatically. + +3. Clear the End Time + +**Expected**: Duration shows 0.0. + +**4.5 Time Validation** + +1. Create or edit a session +2. Set Start Time: 14:00, End Time: 10:00 +3. Save + +**Expected**: Validation error: "End time must be after start time." + +**4.6 Topics Tab Visibility** + +1. Open a session with type "Family Development Session" + (track_topics=True) + +**Expected**: "Topics" tab is visible. Can select from the FDS topics. + +2. Open a session with type "Group Meeting" (track_topics=False) + +**Expected**: "Topics" tab is NOT visible. + +**4.7 Participants Tab** + +1. Open a session and go to the **Participants** tab +2. Click **Add a line** +3. Select contacts as participants + +**Expected**: Participants are listed by name in a simple list format. + +**4.8 Attended Stat Button** + +1. Open a session that has attendance records +2. Click the "Attended" stat button + +**Expected**: A popup list opens showing all attendance records for this +session. + +5. Session State Workflow +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**5.1 Scheduled to In Progress** + +1. Open a session in "Scheduled" state +2. Click the **Start** button + +**Expected**: + +- State changes to "In Progress" +- "Start" button disappears +- "Complete" button appears (highlighted) +- "Cancel" button remains visible +- Statusbar updates to show "In Progress" as current step + +**5.2 In Progress to Completed** + +1. Open a session in "In Progress" state +2. Click the **Complete** button + +**Expected**: + +- State changes to "Completed" +- Green "Completed" ribbon appears on the form +- All "Start", "Complete", and "Cancel" buttons disappear +- All fields become read-only (name, type, date, location, participants, + attendance, etc.) + +**5.3 Cancel from Scheduled** + +1. Open a session in "Scheduled" state +2. Click **Cancel** + +**Expected**: + +- A confirmation dialog appears: "Are you sure you want to cancel this + session?" +- Click OK +- State changes to "Cancelled" +- Red "Cancelled" ribbon appears +- All fields become read-only +- All action buttons disappear + +**5.4 Cancel from In Progress** + +1. Open a session in "In Progress" state +2. Click **Cancel** + +**Expected**: Same confirmation and behavior as 5.3. + +**5.5 Invalid Transitions (Negative Tests)** + +These transitions should NOT be possible via the UI (buttons are +hidden), but verify: + +1. A "Completed" session has no Start, Complete, or Cancel buttons +2. A "Cancelled" session has no Start, Complete, or Cancel buttons +3. A "Scheduled" session has no Complete button (must Start first) + +**Expected**: Buttons are hidden for invalid transitions. The form is +fully read-only for Completed and Cancelled sessions. + +**5.6 Chatter Tracking** + +1. Perform any state transition on a session + +**Expected**: The chatter logs the state change (e.g., "State: Scheduled +-> In Progress"). Changes to Name, Session Type, Date, and Facilitator +are also tracked. + +6. Attendance Management +~~~~~~~~~~~~~~~~~~~~~~~~ + +**6.1 Record Attendance** + +1. Open a session in "Scheduled" or "In Progress" state +2. Go to the **Attendance** tab +3. Click **Add a line** in the attendance list +4. Select a participant +5. Check "Attended" checkbox +6. Optionally set Attendance Time, Excused, Excuse Reason, and Notes +7. Save + +**Expected**: Attendance record is created. Row is highlighted green +(attended). + +**6.2 Attendance Decorations** + +1. Create attendance records with different statuses: + + - Participant A: Attended = True + - Participant B: Attended = False, Excused = True + - Participant C: Attended = False, Excused = False + +**Expected**: + +- Participant A row: green (success decoration) +- Participant B row: yellow/orange (warning decoration) +- Participant C row: grey/muted + +**6.3 Attendance Count and Rate** + +1. Create a session with 4 expected participants +2. Record attendance: 3 attended, 1 did not + +**Expected**: + +- Attendance Count shows 3 (in the tab and in the stat button) +- Attendance Rate shows 75.0% + +**6.4 Attendance Rate with Zero Expected Participants** + +1. Create a session with NO expected participants +2. Record one attendance as attended + +**Expected**: Attendance Rate shows 0.0% (no expected participants to +calculate against). + +**6.5 Duplicate Attendance Prevention** + +1. Open a session and go to the Attendance tab +2. Add an attendance record for "Participant A" +3. Try to add another attendance record for the same "Participant A" +4. Save + +**Expected**: Error: "A participant can only have one attendance record +per session." + +**6.6 Attendance Read-only on Completed/Cancelled** + +1. Open a completed or cancelled session +2. Go to the Attendance tab + +**Expected**: The attendance list is read-only. No "Add a line" button +is available. + +7. Session Views +~~~~~~~~~~~~~~~~ + +**7.1 List View** + +1. Go to **Session Tracking > Sessions > All Sessions** + +**Expected**: + +- Columns: Date, Name, Session Type, Facilitator, Location, Attendance + Count, Attendance Rate, State +- Location is visible by default (optional=show) +- Area is hidden by default (optional=hide) +- State shows as a colored badge: + + - Scheduled: blue (info) + - In Progress: yellow (warning) + - Completed: green (success) + - Cancelled: grey (muted) + +- Row colors match the state badge colors +- Default sort: Date descending (newest first) + +**7.2 Optional Column Toggles** + +1. In the list view, click the column selector (gear icon or right-click + header) +2. Toggle Location off, Area on + +**Expected**: Location column hides, Area column appears. Settings +persist within the session. + +**7.3 Calendar View** + +1. Switch to Calendar view (click the calendar icon in the view + switcher) + +**Expected**: + +- Monthly calendar is displayed +- Sessions appear on their scheduled dates +- Sessions are color-coded by Session Type +- Clicking a date does NOT open a quick-create popup (quick_create is + disabled) +- Clicking a session shows: Name, Facilitator, State + +**7.4 Kanban View** + +1. Switch to Kanban view + +**Expected**: + +- Cards are grouped by State (Scheduled, In Progress, Completed, + Cancelled columns) +- Each card shows: Name (bold), Session Type, Date, Facilitator, + Attendance Rate % +- Progress bar at the top of each column shows color-coded distribution +- Quick-create is disabled (no "+" button at top of columns) +- Cards cannot be dragged between columns (state changes via buttons + only) + +**7.5 Graph View** + +1. Switch to Graph view + +**Expected**: A bar chart showing Duration Hours grouped by Session +Type. + +**7.6 Pivot View** + +1. Switch to Pivot view + +**Expected**: A pivot table with Session Type as rows, Facilitator as +columns, and Duration Hours as the measure. + +**7.7 My Sessions View** + +1. Go to **Session Tracking > Sessions > My Sessions** + +**Expected**: Only sessions where the current user is the facilitator +are shown (My Sessions filter is pre-applied). + +8. Search and Filtering +~~~~~~~~~~~~~~~~~~~~~~~ + +**8.1 Quick Search Fields** + +1. In the search bar, type a session name + +**Expected**: Results are filtered by name. + +2. Clear and search by session type, facilitator, location, or area + +**Expected**: Each field filters correctly. + +**8.2 State Filters** + +1. Click **Filters** in the search bar +2. Apply "Scheduled" filter + +**Expected**: Only scheduled sessions are shown. + +3. Test "In Progress", "Completed", and "Cancelled" filters individually + +**Expected**: Each filter works correctly. + +**8.3 Ownership Filters** + +1. Apply "My Sessions" filter + +**Expected**: Only sessions where you are the facilitator are shown. + +2. Apply "My Co-facilitated Sessions" filter + +**Expected**: Only sessions where you are a co-facilitator are shown. + +**8.4 Date Filter** + +1. Apply "This Month" filter + +**Expected**: Only sessions with dates in the current month are shown. + +**8.5 Group By** + +1. Use **Group By > Session Type** + +**Expected**: Sessions are grouped by their session type with counts. + +2. Test Group By: Facilitator, State, and Date + +**Expected**: Each grouping works correctly. + +**8.6 Combined Filters** + +1. Apply "Scheduled" filter AND "My Sessions" filter together + +**Expected**: Only your scheduled sessions are shown. Filters combine +with AND logic. + +9. Security and Access Control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**9.1 Session User - Read All Sessions** + +1. Log in as **Session User** +2. Go to **Session Tracking > Sessions > All Sessions** + +**Expected**: All sessions are visible (including ones facilitated by +others), regardless of facilitator. + +**9.2 Session User - Edit Own Facilitated Session** + +1. Log in as **Session User** +2. Open a session where you are the **facilitator** +3. Edit the Location field and save + +**Expected**: Save succeeds. You can edit sessions you facilitate. + +**9.3 Session User - Edit Co-facilitated Session** + +1. Log in as **Session User** +2. Open a session where you are a **co-facilitator** (but not the main + facilitator) +3. Edit the Location field and save + +**Expected**: Save succeeds. You can edit sessions you co-facilitate. + +**9.4 Session User - Cannot Edit Another's Session** + +1. Log in as **Session User** +2. Open a session where you are NOT the facilitator or co-facilitator +3. Try to edit the Location field and save + +**Expected**: Access error is raised. You cannot modify sessions you +don't facilitate. + +**9.5 Session User - Cannot Create Sessions** + +1. Log in as **Session User** +2. Try to create a new session + +**Expected**: Access error. Only managers can create sessions. + +**9.6 Session User - Cannot Delete Sessions** + +1. Log in as **Session User** +2. Open a session you facilitate +3. Try to delete it (Action > Delete) + +**Expected**: Access error. Users cannot delete sessions. + +**9.7 Session User - Attendance on Own Sessions** + +1. Log in as **Session User** +2. Open a session you facilitate +3. Go to the Attendance tab and add an attendance record + +**Expected**: Attendance record is created successfully. + +**9.8 Session User - Cannot Add Attendance on Others' Sessions** + +1. Log in as **Session User** +2. Open a session you do NOT facilitate or co-facilitate +3. Try to add an attendance record + +**Expected**: Access error. You cannot create attendance on sessions you +don't facilitate. + +**9.9 Session User - Cannot Delete Attendance** + +1. Log in as **Session User** +2. Open a session with attendance records +3. Try to delete an attendance record + +**Expected**: Access error. Users cannot delete attendance records. + +**9.10 Session Manager - Full Access** + +1. Log in as **Session Manager** +2. Create, read, update, and delete sessions, session types, topics, and + attendance + +**Expected**: All operations succeed. Managers have full CRUD on +everything. + +**9.11 Admin Inherits Manager** + +1. Log in as **OpenSPP Admin** (spp_security.group_spp_admin) +2. Verify you can access all Session Tracking features + +**Expected**: Admin has full access (manager group is implied by admin). + +**9.12 Multi-Company Isolation** + +*Requires multi-company setup with at least 2 companies.* + +1. Log in as a user belonging to Company A only +2. Go to All Sessions + +**Expected**: Only sessions belonging to Company A (or no company) are +visible. Sessions belonging to Company B are NOT visible. + +10. Data Integrity Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**10.1 Delete Session Type with Existing Sessions** + +1. Create a session using a specific session type +2. Try to delete that session type + +**Expected**: Error preventing deletion: the session type cannot be +deleted while sessions reference it (ondelete=restrict). + +**10.2 Delete Participant with Attendance Records** + +1. Create an attendance record for a participant +2. Try to delete that participant (contact) from the system + +**Expected**: Error preventing deletion: the participant cannot be +deleted while attendance records reference them (ondelete=restrict). + +**10.3 Delete Facilitator with Sessions** + +1. Create a session with a specific facilitator +2. Try to delete that user from the system + +**Expected**: Error preventing deletion (ondelete=restrict). + +11. Cross-module Integration (spp_case_session) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Only applicable if ``spp_case_session`` module is also installed.* + +**11.1 Case Stat Button on Session Form** + +1. Open any session in form view +2. Check the button box area + +**Expected**: If spp_case_session is installed, a "Cases" stat button +appears alongside the "Attended" stat button, inside the same button +box. There should NOT be duplicate button boxes. + +12. Edge Cases +~~~~~~~~~~~~~~ + +**12.1 Session with No Times** + +1. Create a session without setting Start Time or End Time + +**Expected**: Duration shows 0.0. No validation error. + +**12.2 Session with Only Start Time** + +1. Create a session with Start Time = 09:00 but no End Time + +**Expected**: Duration shows 0.0. No validation error. + +**12.3 Session with Only End Time** + +1. Create a session with End Time = 12:00 but no Start Time + +**Expected**: Duration shows 0.0. No validation error. + +**12.4 Empty Session (No Participants, No Attendance)** + +1. Create a session with no expected participants and no attendance + +**Expected**: Attendance Count = 0, Attendance Rate = 0.0%. Session +functions normally. + +**12.5 Session Type Code Left Blank** + +1. Create two session types with Code left blank + +**Expected**: Both are created successfully. The unique constraint +allows multiple blank codes. + +Test Summary Checklist +~~~~~~~~~~~~~~~~~~~~~~ + +Use this checklist to track testing progress: + +- ☐ + + 1. Module installation and demo data (1.1 - 1.3) + +- ☐ + + 2. Menu structure and navigation (2.1 - 2.3) + +- ☐ + + 3. Session type management (3.1 - 3.7) + +- ☐ + + 4. Session creation and form view (4.1 - 4.8) + +- ☐ + + 5. Session state workflow (5.1 - 5.6) + +- ☐ + + 6. Attendance management (6.1 - 6.6) + +- ☐ + + 7. Session views (7.1 - 7.7) + +- ☐ + + 8. Search and filtering (8.1 - 8.6) + +- ☐ + + 9. Security and access control (9.1 - 9.12) + +- ☐ + + 10. Data integrity constraints (10.1 - 10.3) + +- ☐ + + 11. Cross-module integration (11.1) + +- ☐ + + 12. Edge cases (12.1 - 12.5) + Bug Tracker =========== diff --git a/spp_session_tracking/__manifest__.py b/spp_session_tracking/__manifest__.py index 5d3a00c9..00097446 100644 --- a/spp_session_tracking/__manifest__.py +++ b/spp_session_tracking/__manifest__.py @@ -9,7 +9,7 @@ "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", "license": "LGPL-3", - "development_status": "Beta", + "development_status": "Production/Stable", "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], "depends": [ "base", diff --git a/spp_session_tracking/data/session_data.xml b/spp_session_tracking/data/session_data.xml index 13be4325..451443a4 100644 --- a/spp_session_tracking/data/session_data.xml +++ b/spp_session_tracking/data/session_data.xml @@ -9,7 +9,7 @@ >General training and capacity building sessions for beneficiaries
monthly 3.0 - 80.0 + 80.0 True @@ -21,7 +21,7 @@ >Regular support group meetings for community members biweekly 2.0 - 75.0 + 75.0 False @@ -33,7 +33,7 @@ >Family Development Sessions for conditional cash transfer programs monthly 2.5 - 85.0 + 85.0 True @@ -43,7 +43,7 @@ Specialized workshops on various topics one_time 4.0 - 90.0 + 90.0 True diff --git a/spp_session_tracking/models/session.py b/spp_session_tracking/models/session.py index b2160e3e..c6e4a6ee 100644 --- a/spp_session_tracking/models/session.py +++ b/spp_session_tracking/models/session.py @@ -1,6 +1,12 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. +import logging + from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) class Session(models.Model): @@ -10,14 +16,26 @@ class Session(models.Model): _order = "date desc" name = fields.Char(required=True, tracking=True) - session_type_id = fields.Many2one("spp.session.type", required=True, string="Session Type", tracking=True) + session_type_id = fields.Many2one( + "spp.session.type", + required=True, + ondelete="restrict", + string="Session Type", + tracking=True, + ) date = fields.Date(required=True, default=fields.Date.today, tracking=True) start_time = fields.Float() end_time = fields.Float() duration_hours = fields.Float(compute="_compute_duration", store=True, string="Duration (Hours)") - facilitator_id = fields.Many2one("res.users", required=True, string="Facilitator", tracking=True) + facilitator_id = fields.Many2one( + "res.users", + required=True, + ondelete="restrict", + string="Facilitator", + tracking=True, + ) co_facilitator_ids = fields.Many2many( "res.users", "session_co_facilitator_rel", @@ -30,6 +48,7 @@ class Session(models.Model): area_id = fields.Many2one("spp.area", string="Area") # Topics covered (if tracking enabled) + track_topics = fields.Boolean(related="session_type_id.track_topics") topic_ids = fields.Many2many( "spp.session.topic", "session_topic_rel", @@ -65,16 +84,22 @@ class Session(models.Model): ) notes = fields.Text() - company_id = fields.Many2one("res.company", default=lambda self: self.env.company) + company_id = fields.Many2one("res.company", default=lambda self: self.env.company, ondelete="restrict") @api.depends("start_time", "end_time") def _compute_duration(self): for rec in self: if rec.start_time and rec.end_time: - rec.duration_hours = rec.end_time - rec.start_time + rec.duration_hours = max(0.0, rec.end_time - rec.start_time) else: rec.duration_hours = 0.0 + @api.constrains("start_time", "end_time") + def _check_time_range(self): + for rec in self: + if rec.start_time and rec.end_time and rec.end_time < rec.start_time: + raise ValidationError(_("End time must be after start time.")) + @api.depends("attendance_ids", "attendance_ids.is_attended", "expected_participant_ids") def _compute_attendance(self): for rec in self: @@ -87,11 +112,45 @@ def _compute_attendance(self): else: rec.attendance_rate = 0.0 + _VALID_TRANSITIONS = { + "scheduled": {"in_progress", "cancelled"}, + "in_progress": {"completed", "cancelled"}, + "completed": set(), + "cancelled": set(), + } + + def write(self, vals): + if "state" in vals: + new_state = vals["state"] + for rec in self: + valid = self._VALID_TRANSITIONS.get(rec.state, set()) + if new_state not in valid: + raise UserError( + _( + "Cannot transition session from '%(current)s' to '%(target)s'.", + current=rec.state, + target=new_state, + ) + ) + return super().write(vals) + def action_start(self): - self.state = "in_progress" + for rec in self: + if rec.state != "scheduled": + raise UserError(_("Only scheduled sessions can be started.")) + _logger.info("Session id=%s transitioning from scheduled to in_progress.", rec.id) + rec.state = "in_progress" def action_complete(self): - self.state = "completed" + for rec in self: + if rec.state != "in_progress": + raise UserError(_("Only sessions in progress can be completed.")) + _logger.info("Session id=%s transitioning from in_progress to completed.", rec.id) + rec.state = "completed" def action_cancel(self): - self.state = "cancelled" + for rec in self: + if rec.state not in ("scheduled", "in_progress"): + raise UserError(_("Only scheduled or in-progress sessions can be cancelled.")) + _logger.info("Session id=%s transitioning from %s to cancelled.", rec.id, rec.state) + rec.state = "cancelled" diff --git a/spp_session_tracking/models/session_attendance.py b/spp_session_tracking/models/session_attendance.py index 9ea27385..4d81354d 100644 --- a/spp_session_tracking/models/session_attendance.py +++ b/spp_session_tracking/models/session_attendance.py @@ -9,12 +9,12 @@ class SessionAttendance(models.Model): _rec_name = "participant_id" session_id = fields.Many2one("spp.session", required=True, ondelete="cascade", string="Session") - participant_id = fields.Many2one("res.partner", required=True, string="Participant") + participant_id = fields.Many2one("res.partner", required=True, ondelete="restrict", string="Participant") - is_attended = fields.Boolean(default=False) + is_attended = fields.Boolean() attendance_time = fields.Datetime(string="Time of Attendance") - is_excused = fields.Boolean(default=False) + is_excused = fields.Boolean() excuse_reason = fields.Char() notes = fields.Text() diff --git a/spp_session_tracking/models/session_topic.py b/spp_session_tracking/models/session_topic.py index 24edc255..2bbcdaaf 100644 --- a/spp_session_tracking/models/session_topic.py +++ b/spp_session_tracking/models/session_topic.py @@ -11,6 +11,6 @@ class SessionTopic(models.Model): name = fields.Char(required=True) code = fields.Char() description = fields.Text() - session_type_id = fields.Many2one("spp.session.type", string="Session Type") + session_type_id = fields.Many2one("spp.session.type", string="Session Type", ondelete="cascade") sequence = fields.Integer(default=10) active = fields.Boolean(default=True) diff --git a/spp_session_tracking/models/session_type.py b/spp_session_tracking/models/session_type.py index 513a921d..62742c75 100644 --- a/spp_session_tracking/models/session_type.py +++ b/spp_session_tracking/models/session_type.py @@ -1,6 +1,7 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models +from odoo.tools.translate import _ class SessionType(models.Model): @@ -23,7 +24,7 @@ class SessionType(models.Model): default="monthly", ) - required_attendance_pct = fields.Float( + required_attendance_percentage = fields.Float( default=80.0, help="Minimum attendance percentage required for compliance", ) @@ -37,21 +38,30 @@ class SessionType(models.Model): session_count = fields.Integer(compute="_compute_counts", string="Number of Sessions") - company_id = fields.Many2one("res.company", default=lambda self: self.env.company) + company_id = fields.Many2one("res.company", default=lambda self: self.env.company, ondelete="restrict") - @api.depends("topic_ids") + _unique_code = models.Constraint( + "UNIQUE(code)", + "Session type code must be unique.", + ) + + @api.depends() def _compute_counts(self): + data = self.env["spp.session"].read_group( + [("session_type_id", "in", self.ids)], ["session_type_id"], ["session_type_id"] + ) + mapped = {d["session_type_id"][0]: d["session_type_id_count"] for d in data} for rec in self: - rec.session_count = self.env["spp.session"].search_count([("session_type_id", "=", rec.id)]) + rec.session_count = mapped.get(rec.id, 0) def action_view_sessions(self): """Open view with sessions of this type.""" self.ensure_one() return { "type": "ir.actions.act_window", - "name": f"Sessions - {self.name}", + "name": _("Sessions - %s", self.name), "res_model": "spp.session", - "view_mode": "tree,form,calendar,kanban", + "view_mode": "list,form,calendar,kanban", "domain": [("session_type_id", "=", self.id)], "context": {"default_session_type_id": self.id}, } diff --git a/spp_session_tracking/readme/DESCRIPTION.md b/spp_session_tracking/readme/DESCRIPTION.md index 5c7acc0d..fca84fbf 100644 --- a/spp_session_tracking/readme/DESCRIPTION.md +++ b/spp_session_tracking/readme/DESCRIPTION.md @@ -26,7 +26,7 @@ After installing: 1. Navigate to **Session Tracking > Configuration > Session Types** 2. Review pre-configured session types (Training, Family Development Session, Group Meeting, Workshop) -3. Add or modify session types and topics as needed +3. Add or modify session types as needed; topics are managed within each session type form when topic tracking is enabled 4. Adjust required attendance percentage per session type Four session types are pre-configured with sample topics for Family Development Sessions and Training Sessions. @@ -36,16 +36,16 @@ Four session types are pre-configured with sample topics for Family Development - **Menu**: Session Tracking > Sessions > All Sessions - **My Sessions**: Session Tracking > Sessions > My Sessions (filtered to current user as facilitator) - **Configuration**: Session Tracking > Configuration > Session Types (managers only) -- **Views**: List, form, calendar (by date), kanban (grouped by state) +- **Views**: List, form, calendar (by date), kanban (grouped by state), graph, pivot ### Security | Group | Access | | ---------------------------------------------- | ------------------------------------- | -| `spp_session_tracking.group_session_user` | Read all sessions and session types/topics; write own facilitated sessions; read/write/create attendance (no delete) | +| `spp_session_tracking.group_session_user` | Read all sessions and session types/topics; create/write own facilitated or co-facilitated sessions; read/write/create attendance for own sessions (no delete) | | `spp_session_tracking.group_session_manager` | Full CRUD on all sessions, types, topics, and attendance | -The session user group can view all sessions but only edit sessions they facilitate (via record rule). The `spp_security.group_spp_admin` group implies manager access. +The session user group can view all sessions but only edit sessions where they are the facilitator or co-facilitator (via record rules). Session creation requires manager access. The `spp_security.group_spp_admin` group implies manager access. Multi-company record rules ensure users only see sessions belonging to their company. ### Extension Points diff --git a/spp_session_tracking/readme/USAGE.md b/spp_session_tracking/readme/USAGE.md new file mode 100644 index 00000000..d2304c96 --- /dev/null +++ b/spp_session_tracking/readme/USAGE.md @@ -0,0 +1,678 @@ +This guide covers all testable areas of the ``spp_session_tracking`` module. It is organized by +functional area, with step-by-step instructions and expected results. + +### Prerequisites + +- Module ``spp_session_tracking`` is installed +- Two test users are available: + - **Session User** (group: ``Session Tracking / User``) + - **Session Manager** (group: ``Session Tracking / Manager``) +- Admin account is available for setup steps +- At least two contacts (res.partner) exist for use as participants + +### 1. Module Installation and Demo Data + +**1.1 Verify Installation** + +1. Log in as admin +2. Go to **Apps**, search for "Session Tracking" +3. Confirm the module is installed + +**Expected**: Module appears as installed. + +**1.2 Verify Pre-configured Session Types** + +1. Navigate to **Session Tracking > Configuration > Session Types** +2. Verify 4 session types exist: + +| Name | Code | Frequency | Duration | Attendance % | Topics? | +|-----------------------------|----------|-----------|----------|--------------|---------| +| Training Session | TRAINING | Monthly | 3.0h | 80% | Yes | +| Group Meeting | MEETING | Bi-weekly | 2.0h | 75% | No | +| Family Development Session | FDS | Monthly | 2.5h | 85% | Yes | +| Workshop | WORKSHOP | One-time | 4.0h | 90% | Yes | + +**Expected**: All 4 types are present with the values above. + +**1.3 Verify Pre-configured Topics** + +1. Open the **Family Development Session** type +2. Check the Topics section (visible because Track Topics is enabled) + +**Expected**: 4 topics exist: Nutrition and Health, Education and Child Development, Financial +Literacy, Positive Parenting. + +3. Open the **Training Session** type + +**Expected**: 2 topics exist: Livelihood Skills, Basic Business Management. + +4. Open the **Group Meeting** type + +**Expected**: No Topics section is visible (Track Topics is disabled). + +### 2. Menu Structure and Navigation + +**2.1 Main Menu** + +1. Log in as Session Manager +2. Check the main menu bar + +**Expected**: "Session Tracking" menu item is visible with the module icon. + +**2.2 Submenu Structure** + +1. Click **Session Tracking** + +**Expected**: Two submenu groups are visible: +- **Sessions**: All Sessions, My Sessions +- **Configuration**: Session Types + +**2.3 Configuration Menu Visibility** + +1. Log in as **Session User** +2. Navigate to Session Tracking + +**Expected**: The "Configuration" submenu is NOT visible. Only "Sessions" submenu is shown. + +3. Log in as **Session Manager** + +**Expected**: Both "Sessions" and "Configuration" submenus are visible. + +### 3. Session Type Management (Manager Only) + +**3.1 Create a Session Type** + +1. Log in as Session Manager +2. Go to **Session Tracking > Configuration > Session Types** +3. Click **New** +4. Fill in: + - Name: "Health Workshop" + - Code: "HEALTH" + - Frequency: Quarterly + - Duration: 1.5 + - Required Attendance %: 70 + - Track Topics: enabled +5. Save + +**Expected**: Session type is created. Topics section appears below. + +**3.2 Add Topics to a Session Type** + +1. Open the "Health Workshop" type created above +2. In the Topics section, click **Add a line** +3. Add topics: "Hygiene Practices", "First Aid Basics" +4. Save + +**Expected**: Topics are saved and listed under the session type. + +**3.3 Unique Code Constraint** + +1. Go to **Session Tracking > Configuration > Session Types** +2. Click **New** +3. Enter Name: "Duplicate Code Test", Code: "HEALTH" (same as above) +4. Save + +**Expected**: Error message: "Session type code must be unique." + +**3.4 Archive a Session Type** + +1. Open an existing session type +2. Use **Action > Archive** +3. Go to the list view and remove the "Active" filter + +**Expected**: The archived type shows as inactive (greyed out in list view with muted decoration). + +**3.5 Session Count Stat Button** + +1. Open a session type that has sessions (e.g., create a session first) +2. Check the "Sessions" stat button in the top-right area + +**Expected**: The button shows the correct count of sessions for this type. + +3. Click the stat button + +**Expected**: A filtered list of sessions for this type opens. + +**3.6 Session Type List View** + +1. Go to **Session Tracking > Configuration > Session Types** + +**Expected**: List displays columns: Name, Code, Frequency, Duration, Required Attendance %, Sessions, +Active. Inactive types are greyed out. + +**3.7 User Cannot Create Session Types** + +1. Log in as **Session User** +2. Try to access **Session Tracking > Configuration > Session Types** + +**Expected**: The Configuration menu is not visible. If accessed via URL, an access error is raised. + +### 4. Session Creation and Form View + +**4.1 Create a Session (Manager)** + +1. Log in as Session Manager +2. Go to **Session Tracking > Sessions > All Sessions** +3. Click **New** +4. Fill in: + - Name: "FDS - Barangay San Jose - March 2026" + - Session Type: Family Development Session + - Date: today's date + - Facilitator: (select a user) + - Co-Facilitators: (select one or more users) + - Location: "Barangay Hall" + - Start Time: 09:00 + - End Time: 11:30 + - Max Participants: 25 +5. Save + +**Expected**: + +- Session is created in "Scheduled" state +- Duration shows 2.5 hours (auto-computed) +- Statusbar shows: Scheduled > In Progress > Completed +- "Start" button is visible and highlighted +- "Cancel" button is visible (not highlighted) +- "Complete" button is NOT visible + +**4.2 User Cannot Create a Session** + +1. Log in as **Session User** +2. Go to **Session Tracking > Sessions > All Sessions** +3. Try to click **New** + +**Expected**: An access error is raised. Session users cannot create sessions. + +**4.3 Form View Structure** + +1. Open any session in form view + +**Expected**: + +- **Header**: Action buttons (Start/Complete/Cancel based on state) + statusbar +- **Ribbons**: Green "Completed" ribbon on completed sessions, red "Cancelled" ribbon on cancelled +- **Button Box**: "Attended" stat button with user icon (shows attendance count) +- **Title**: Session name in large text +- **Main Info**: Two-column layout + - Left: Session Type, Date, Location, Area + - Right: Facilitator, Co-Facilitators, Start Time, End Time, Duration, Max Participants, Company +- **Tabs**: Participants, Attendance, Topics (conditional), Notes +- **Chatter**: Message log and activity tracking at the bottom + +**4.4 Duration Auto-computation** + +1. Create or edit a session +2. Set Start Time: 09:00, End Time: 12:00 + +**Expected**: Duration shows 3.0 hours automatically. + +3. Clear the End Time + +**Expected**: Duration shows 0.0. + +**4.5 Time Validation** + +1. Create or edit a session +2. Set Start Time: 14:00, End Time: 10:00 +3. Save + +**Expected**: Validation error: "End time must be after start time." + +**4.6 Topics Tab Visibility** + +1. Open a session with type "Family Development Session" (track_topics=True) + +**Expected**: "Topics" tab is visible. Can select from the FDS topics. + +2. Open a session with type "Group Meeting" (track_topics=False) + +**Expected**: "Topics" tab is NOT visible. + +**4.7 Participants Tab** + +1. Open a session and go to the **Participants** tab +2. Click **Add a line** +3. Select contacts as participants + +**Expected**: Participants are listed by name in a simple list format. + +**4.8 Attended Stat Button** + +1. Open a session that has attendance records +2. Click the "Attended" stat button + +**Expected**: A popup list opens showing all attendance records for this session. + +### 5. Session State Workflow + +**5.1 Scheduled to In Progress** + +1. Open a session in "Scheduled" state +2. Click the **Start** button + +**Expected**: + +- State changes to "In Progress" +- "Start" button disappears +- "Complete" button appears (highlighted) +- "Cancel" button remains visible +- Statusbar updates to show "In Progress" as current step + +**5.2 In Progress to Completed** + +1. Open a session in "In Progress" state +2. Click the **Complete** button + +**Expected**: + +- State changes to "Completed" +- Green "Completed" ribbon appears on the form +- All "Start", "Complete", and "Cancel" buttons disappear +- All fields become read-only (name, type, date, location, participants, attendance, etc.) + +**5.3 Cancel from Scheduled** + +1. Open a session in "Scheduled" state +2. Click **Cancel** + +**Expected**: + +- A confirmation dialog appears: "Are you sure you want to cancel this session?" +- Click OK +- State changes to "Cancelled" +- Red "Cancelled" ribbon appears +- All fields become read-only +- All action buttons disappear + +**5.4 Cancel from In Progress** + +1. Open a session in "In Progress" state +2. Click **Cancel** + +**Expected**: Same confirmation and behavior as 5.3. + +**5.5 Invalid Transitions (Negative Tests)** + +These transitions should NOT be possible via the UI (buttons are hidden), but verify: + +1. A "Completed" session has no Start, Complete, or Cancel buttons +2. A "Cancelled" session has no Start, Complete, or Cancel buttons +3. A "Scheduled" session has no Complete button (must Start first) + +**Expected**: Buttons are hidden for invalid transitions. The form is fully read-only for Completed +and Cancelled sessions. + +**5.6 Chatter Tracking** + +1. Perform any state transition on a session + +**Expected**: The chatter logs the state change (e.g., "State: Scheduled -> In Progress"). Changes to +Name, Session Type, Date, and Facilitator are also tracked. + +### 6. Attendance Management + +**6.1 Record Attendance** + +1. Open a session in "Scheduled" or "In Progress" state +2. Go to the **Attendance** tab +3. Click **Add a line** in the attendance list +4. Select a participant +5. Check "Attended" checkbox +6. Optionally set Attendance Time, Excused, Excuse Reason, and Notes +7. Save + +**Expected**: Attendance record is created. Row is highlighted green (attended). + +**6.2 Attendance Decorations** + +1. Create attendance records with different statuses: + - Participant A: Attended = True + - Participant B: Attended = False, Excused = True + - Participant C: Attended = False, Excused = False + +**Expected**: + +- Participant A row: green (success decoration) +- Participant B row: yellow/orange (warning decoration) +- Participant C row: grey/muted + +**6.3 Attendance Count and Rate** + +1. Create a session with 4 expected participants +2. Record attendance: 3 attended, 1 did not + +**Expected**: + +- Attendance Count shows 3 (in the tab and in the stat button) +- Attendance Rate shows 75.0% + +**6.4 Attendance Rate with Zero Expected Participants** + +1. Create a session with NO expected participants +2. Record one attendance as attended + +**Expected**: Attendance Rate shows 0.0% (no expected participants to calculate against). + +**6.5 Duplicate Attendance Prevention** + +1. Open a session and go to the Attendance tab +2. Add an attendance record for "Participant A" +3. Try to add another attendance record for the same "Participant A" +4. Save + +**Expected**: Error: "A participant can only have one attendance record per session." + +**6.6 Attendance Read-only on Completed/Cancelled** + +1. Open a completed or cancelled session +2. Go to the Attendance tab + +**Expected**: The attendance list is read-only. No "Add a line" button is available. + +### 7. Session Views + +**7.1 List View** + +1. Go to **Session Tracking > Sessions > All Sessions** + +**Expected**: + +- Columns: Date, Name, Session Type, Facilitator, Location, Attendance Count, Attendance Rate, State +- Location is visible by default (optional=show) +- Area is hidden by default (optional=hide) +- State shows as a colored badge: + - Scheduled: blue (info) + - In Progress: yellow (warning) + - Completed: green (success) + - Cancelled: grey (muted) +- Row colors match the state badge colors +- Default sort: Date descending (newest first) + +**7.2 Optional Column Toggles** + +1. In the list view, click the column selector (gear icon or right-click header) +2. Toggle Location off, Area on + +**Expected**: Location column hides, Area column appears. Settings persist within the session. + +**7.3 Calendar View** + +1. Switch to Calendar view (click the calendar icon in the view switcher) + +**Expected**: + +- Monthly calendar is displayed +- Sessions appear on their scheduled dates +- Sessions are color-coded by Session Type +- Clicking a date does NOT open a quick-create popup (quick_create is disabled) +- Clicking a session shows: Name, Facilitator, State + +**7.4 Kanban View** + +1. Switch to Kanban view + +**Expected**: + +- Cards are grouped by State (Scheduled, In Progress, Completed, Cancelled columns) +- Each card shows: Name (bold), Session Type, Date, Facilitator, Attendance Rate % +- Progress bar at the top of each column shows color-coded distribution +- Quick-create is disabled (no "+" button at top of columns) +- Cards cannot be dragged between columns (state changes via buttons only) + +**7.5 Graph View** + +1. Switch to Graph view + +**Expected**: A bar chart showing Duration Hours grouped by Session Type. + +**7.6 Pivot View** + +1. Switch to Pivot view + +**Expected**: A pivot table with Session Type as rows, Facilitator as columns, and Duration Hours as +the measure. + +**7.7 My Sessions View** + +1. Go to **Session Tracking > Sessions > My Sessions** + +**Expected**: Only sessions where the current user is the facilitator are shown (My Sessions filter +is pre-applied). + +### 8. Search and Filtering + +**8.1 Quick Search Fields** + +1. In the search bar, type a session name + +**Expected**: Results are filtered by name. + +2. Clear and search by session type, facilitator, location, or area + +**Expected**: Each field filters correctly. + +**8.2 State Filters** + +1. Click **Filters** in the search bar +2. Apply "Scheduled" filter + +**Expected**: Only scheduled sessions are shown. + +3. Test "In Progress", "Completed", and "Cancelled" filters individually + +**Expected**: Each filter works correctly. + +**8.3 Ownership Filters** + +1. Apply "My Sessions" filter + +**Expected**: Only sessions where you are the facilitator are shown. + +2. Apply "My Co-facilitated Sessions" filter + +**Expected**: Only sessions where you are a co-facilitator are shown. + +**8.4 Date Filter** + +1. Apply "This Month" filter + +**Expected**: Only sessions with dates in the current month are shown. + +**8.5 Group By** + +1. Use **Group By > Session Type** + +**Expected**: Sessions are grouped by their session type with counts. + +2. Test Group By: Facilitator, State, and Date + +**Expected**: Each grouping works correctly. + +**8.6 Combined Filters** + +1. Apply "Scheduled" filter AND "My Sessions" filter together + +**Expected**: Only your scheduled sessions are shown. Filters combine with AND logic. + +### 9. Security and Access Control + +**9.1 Session User - Read All Sessions** + +1. Log in as **Session User** +2. Go to **Session Tracking > Sessions > All Sessions** + +**Expected**: All sessions are visible (including ones facilitated by others), regardless of +facilitator. + +**9.2 Session User - Edit Own Facilitated Session** + +1. Log in as **Session User** +2. Open a session where you are the **facilitator** +3. Edit the Location field and save + +**Expected**: Save succeeds. You can edit sessions you facilitate. + +**9.3 Session User - Edit Co-facilitated Session** + +1. Log in as **Session User** +2. Open a session where you are a **co-facilitator** (but not the main facilitator) +3. Edit the Location field and save + +**Expected**: Save succeeds. You can edit sessions you co-facilitate. + +**9.4 Session User - Cannot Edit Another's Session** + +1. Log in as **Session User** +2. Open a session where you are NOT the facilitator or co-facilitator +3. Try to edit the Location field and save + +**Expected**: Access error is raised. You cannot modify sessions you don't facilitate. + +**9.5 Session User - Cannot Create Sessions** + +1. Log in as **Session User** +2. Try to create a new session + +**Expected**: Access error. Only managers can create sessions. + +**9.6 Session User - Cannot Delete Sessions** + +1. Log in as **Session User** +2. Open a session you facilitate +3. Try to delete it (Action > Delete) + +**Expected**: Access error. Users cannot delete sessions. + +**9.7 Session User - Attendance on Own Sessions** + +1. Log in as **Session User** +2. Open a session you facilitate +3. Go to the Attendance tab and add an attendance record + +**Expected**: Attendance record is created successfully. + +**9.8 Session User - Cannot Add Attendance on Others' Sessions** + +1. Log in as **Session User** +2. Open a session you do NOT facilitate or co-facilitate +3. Try to add an attendance record + +**Expected**: Access error. You cannot create attendance on sessions you don't facilitate. + +**9.9 Session User - Cannot Delete Attendance** + +1. Log in as **Session User** +2. Open a session with attendance records +3. Try to delete an attendance record + +**Expected**: Access error. Users cannot delete attendance records. + +**9.10 Session Manager - Full Access** + +1. Log in as **Session Manager** +2. Create, read, update, and delete sessions, session types, topics, and attendance + +**Expected**: All operations succeed. Managers have full CRUD on everything. + +**9.11 Admin Inherits Manager** + +1. Log in as **OpenSPP Admin** (spp_security.group_spp_admin) +2. Verify you can access all Session Tracking features + +**Expected**: Admin has full access (manager group is implied by admin). + +**9.12 Multi-Company Isolation** + +*Requires multi-company setup with at least 2 companies.* + +1. Log in as a user belonging to Company A only +2. Go to All Sessions + +**Expected**: Only sessions belonging to Company A (or no company) are visible. Sessions belonging to +Company B are NOT visible. + +### 10. Data Integrity Constraints + +**10.1 Delete Session Type with Existing Sessions** + +1. Create a session using a specific session type +2. Try to delete that session type + +**Expected**: Error preventing deletion: the session type cannot be deleted while sessions reference +it (ondelete=restrict). + +**10.2 Delete Participant with Attendance Records** + +1. Create an attendance record for a participant +2. Try to delete that participant (contact) from the system + +**Expected**: Error preventing deletion: the participant cannot be deleted while attendance records +reference them (ondelete=restrict). + +**10.3 Delete Facilitator with Sessions** + +1. Create a session with a specific facilitator +2. Try to delete that user from the system + +**Expected**: Error preventing deletion (ondelete=restrict). + +### 11. Cross-module Integration (spp_case_session) + +*Only applicable if ``spp_case_session`` module is also installed.* + +**11.1 Case Stat Button on Session Form** + +1. Open any session in form view +2. Check the button box area + +**Expected**: If spp_case_session is installed, a "Cases" stat button appears alongside the +"Attended" stat button, inside the same button box. There should NOT be duplicate button boxes. + +### 12. Edge Cases + +**12.1 Session with No Times** + +1. Create a session without setting Start Time or End Time + +**Expected**: Duration shows 0.0. No validation error. + +**12.2 Session with Only Start Time** + +1. Create a session with Start Time = 09:00 but no End Time + +**Expected**: Duration shows 0.0. No validation error. + +**12.3 Session with Only End Time** + +1. Create a session with End Time = 12:00 but no Start Time + +**Expected**: Duration shows 0.0. No validation error. + +**12.4 Empty Session (No Participants, No Attendance)** + +1. Create a session with no expected participants and no attendance + +**Expected**: Attendance Count = 0, Attendance Rate = 0.0%. Session functions normally. + +**12.5 Session Type Code Left Blank** + +1. Create two session types with Code left blank + +**Expected**: Both are created successfully. The unique constraint allows multiple blank codes. + +### Test Summary Checklist + +Use this checklist to track testing progress: + +- [ ] 1. Module installation and demo data (1.1 - 1.3) +- [ ] 2. Menu structure and navigation (2.1 - 2.3) +- [ ] 3. Session type management (3.1 - 3.7) +- [ ] 4. Session creation and form view (4.1 - 4.8) +- [ ] 5. Session state workflow (5.1 - 5.6) +- [ ] 6. Attendance management (6.1 - 6.6) +- [ ] 7. Session views (7.1 - 7.7) +- [ ] 8. Search and filtering (8.1 - 8.6) +- [ ] 9. Security and access control (9.1 - 9.12) +- [ ] 10. Data integrity constraints (10.1 - 10.3) +- [ ] 11. Cross-module integration (11.1) +- [ ] 12. Edge cases (12.1 - 12.5) diff --git a/spp_session_tracking/security/ir.model.access.csv b/spp_session_tracking/security/ir.model.access.csv index 9198b30a..09ddab24 100644 --- a/spp_session_tracking/security/ir.model.access.csv +++ b/spp_session_tracking/security/ir.model.access.csv @@ -3,7 +3,7 @@ access_spp_session_type_user,access_spp_session_type_user,spp_session_tracking.m access_spp_session_type_manager,access_spp_session_type_manager,spp_session_tracking.model_spp_session_type,spp_session_tracking.group_session_manager,1,1,1,1 access_spp_session_topic_user,access_spp_session_topic_user,spp_session_tracking.model_spp_session_topic,spp_session_tracking.group_session_user,1,0,0,0 access_spp_session_topic_manager,access_spp_session_topic_manager,spp_session_tracking.model_spp_session_topic,spp_session_tracking.group_session_manager,1,1,1,1 -access_spp_session_user,access_spp_session_user,spp_session_tracking.model_spp_session,spp_session_tracking.group_session_user,1,0,0,0 +access_spp_session_user,access_spp_session_user,spp_session_tracking.model_spp_session,spp_session_tracking.group_session_user,1,1,0,0 access_spp_session_manager,access_spp_session_manager,spp_session_tracking.model_spp_session,spp_session_tracking.group_session_manager,1,1,1,1 access_spp_session_attendance_user,access_spp_session_attendance_user,spp_session_tracking.model_spp_session_attendance,spp_session_tracking.group_session_user,1,1,1,0 access_spp_session_attendance_manager,access_spp_session_attendance_manager,spp_session_tracking.model_spp_session_attendance,spp_session_tracking.group_session_manager,1,1,1,1 diff --git a/spp_session_tracking/security/session_rules.xml b/spp_session_tracking/security/session_rules.xml index aa5b658a..37c2c4bf 100644 --- a/spp_session_tracking/security/session_rules.xml +++ b/spp_session_tracking/security/session_rules.xml @@ -1,11 +1,52 @@ - + + + + Session: Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + + + + + + Session Type: Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + + + + + + + Session User: Read All Sessions + + [(1, '=', 1)] + + + + + + + Session User: Own Facilitated Sessions - [('facilitator_id', '=', user.id)] + ['|', ('facilitator_id', '=', user.id), ('co_facilitator_ids', 'in', user.id)] @@ -13,10 +54,24 @@ - - Session User: Read All Sessions + + + Session Manager: Full Access [(1, '=', 1)] + + + + + + + + + + + Session Attendance User: Read All + + [(1, '=', 1)] @@ -24,10 +79,23 @@ - - - Session Manager: Full Access - + + Session Attendance User: Own Facilitated Sessions + + ['|', ('session_id.facilitator_id', '=', user.id), ('session_id.co_facilitator_ids', 'in', user.id)] + + + + + + + + + + Session Attendance Manager: Full Access + [(1, '=', 1)] diff --git a/spp_session_tracking/static/description/index.html b/spp_session_tracking/static/description/index.html index 4b682d2f..abb93d71 100644 --- a/spp_session_tracking/static/description/index.html +++ b/spp_session_tracking/static/description/index.html @@ -369,7 +369,7 @@

OpenSPP Session Tracking

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:696866918edb0694e649b8f7e2a237a7007f35de1ff18a503d87efc0cad14f7d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: LGPL-3 OpenSPP/OpenSPP2

+

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

Tracks attendance at required sessions and trainings for social protection programs. Records participant attendance, computes attendance rates against expected participation, and manages session lifecycle from @@ -430,7 +430,8 @@

Configuration

  • Navigate to Session Tracking > Configuration > Session Types
  • Review pre-configured session types (Training, Family Development Session, Group Meeting, Workshop)
  • -
  • Add or modify session types and topics as needed
  • +
  • Add or modify session types as needed; topics are managed within each +session type form when topic tracking is enabled
  • Adjust required attendance percentage per session type
  • Four session types are pre-configured with sample topics for Family @@ -444,7 +445,8 @@

    UI Location

    to current user as facilitator)
  • Configuration: Session Tracking > Configuration > Session Types (managers only)
  • -
  • Views: List, form, calendar (by date), kanban (grouped by state)
  • +
  • Views: List, form, calendar (by date), kanban (grouped by state), +graph, pivot
  • @@ -462,9 +464,10 @@

    Security

    spp_session_tracking.group_session_user Read all sessions and session -types/topics; write own -facilitated sessions; -read/write/create attendance (no +types/topics; create/write own +facilitated or co-facilitated +sessions; read/write/create +attendance for own sessions (no delete) spp_session_tracking.group_session_manager @@ -473,9 +476,12 @@

    Security

    -

    The session user group can view all sessions but only edit sessions they -facilitate (via record rule). The spp_security.group_spp_admin group -implies manager access.

    +

    The session user group can view all sessions but only edit sessions +where they are the facilitator or co-facilitator (via record rules). +Session creation requires manager access. The +spp_security.group_spp_admin group implies manager access. +Multi-company record rules ensure users only see sessions belonging to +their company.

    Extension Points

    @@ -493,16 +499,783 @@

    Dependencies

    Table of contents

    +
    +

    Usage

    +

    This guide covers all testable areas of the spp_session_tracking +module. It is organized by functional area, with step-by-step +instructions and expected results.

    +
    +
    +
    +

    Prerequisites

    +
      +
    • Module spp_session_tracking is installed
    • +
    • Two test users are available:
        +
      • Session User (group: Session Tracking / User)
      • +
      • Session Manager (group: Session Tracking / Manager)
    • +
    • Admin account is available for setup steps
    • +
    • At least two contacts (res.partner) exist for use as participants
    +
    +

    1. Module Installation and Demo Data

    +

    1.1 Verify Installation

    +
      +
    1. Log in as admin
    2. +
    3. Go to Apps, search for “Session Tracking”
    4. +
    5. Confirm the module is installed
    6. +
    +

    Expected: Module appears as installed.

    +

    1.2 Verify Pre-configured Session Types

    +
      +
    1. Navigate to Session Tracking > Configuration > Session Types
    2. +
    3. Verify 4 session types exist:
    4. +
    + ++++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameCodeFrequencyDurationAttendance %Topics?
    Training +SessionTRAININGMonthly3.0h80%Yes
    Group +MeetingMEETINGBi-weekly2.0h75%No
    Family +Development +SessionFDSMonthly2.5h85%Yes
    WorkshopWORKSHOPOne-time4.0h90%Yes
    +

    Expected: All 4 types are present with the values above.

    +

    1.3 Verify Pre-configured Topics

    +
      +
    1. Open the Family Development Session type
    2. +
    3. Check the Topics section (visible because Track Topics is enabled)
    4. +
    +

    Expected: 4 topics exist: Nutrition and Health, Education and Child +Development, Financial Literacy, Positive Parenting.

    +
      +
    1. Open the Training Session type
    2. +
    +

    Expected: 2 topics exist: Livelihood Skills, Basic Business +Management.

    +
      +
    1. Open the Group Meeting type
    2. +
    +

    Expected: No Topics section is visible (Track Topics is disabled).

    +
    + +
    +

    3. Session Type Management (Manager Only)

    +

    3.1 Create a Session Type

    +
      +
    1. Log in as Session Manager
    2. +
    3. Go to Session Tracking > Configuration > Session Types
    4. +
    5. Click New
    6. +
    7. Fill in:
        +
      • Name: “Health Workshop”
      • +
      • Code: “HEALTH”
      • +
      • Frequency: Quarterly
      • +
      • Duration: 1.5
      • +
      • Required Attendance %: 70
      • +
      • Track Topics: enabled
      • +
      +
    8. +
    9. Save
    10. +
    +

    Expected: Session type is created. Topics section appears below.

    +

    3.2 Add Topics to a Session Type

    +
      +
    1. Open the “Health Workshop” type created above
    2. +
    3. In the Topics section, click Add a line
    4. +
    5. Add topics: “Hygiene Practices”, “First Aid Basics”
    6. +
    7. Save
    8. +
    +

    Expected: Topics are saved and listed under the session type.

    +

    3.3 Unique Code Constraint

    +
      +
    1. Go to Session Tracking > Configuration > Session Types
    2. +
    3. Click New
    4. +
    5. Enter Name: “Duplicate Code Test”, Code: “HEALTH” (same as above)
    6. +
    7. Save
    8. +
    +

    Expected: Error message: “Session type code must be unique.”

    +

    3.4 Archive a Session Type

    +
      +
    1. Open an existing session type
    2. +
    3. Use Action > Archive
    4. +
    5. Go to the list view and remove the “Active” filter
    6. +
    +

    Expected: The archived type shows as inactive (greyed out in list +view with muted decoration).

    +

    3.5 Session Count Stat Button

    +
      +
    1. Open a session type that has sessions (e.g., create a session first)
    2. +
    3. Check the “Sessions” stat button in the top-right area
    4. +
    +

    Expected: The button shows the correct count of sessions for this +type.

    +
      +
    1. Click the stat button
    2. +
    +

    Expected: A filtered list of sessions for this type opens.

    +

    3.6 Session Type List View

    +
      +
    1. Go to Session Tracking > Configuration > Session Types
    2. +
    +

    Expected: List displays columns: Name, Code, Frequency, Duration, +Required Attendance %, Sessions, Active. Inactive types are greyed out.

    +

    3.7 User Cannot Create Session Types

    +
      +
    1. Log in as Session User
    2. +
    3. Try to access Session Tracking > Configuration > Session Types
    4. +
    +

    Expected: The Configuration menu is not visible. If accessed via +URL, an access error is raised.

    +
    +
    +

    4. Session Creation and Form View

    +

    4.1 Create a Session (Manager)

    +
      +
    1. Log in as Session Manager
    2. +
    3. Go to Session Tracking > Sessions > All Sessions
    4. +
    5. Click New
    6. +
    7. Fill in:
        +
      • Name: “FDS - Barangay San Jose - March 2026”
      • +
      • Session Type: Family Development Session
      • +
      • Date: today’s date
      • +
      • Facilitator: (select a user)
      • +
      • Co-Facilitators: (select one or more users)
      • +
      • Location: “Barangay Hall”
      • +
      • Start Time: 09:00
      • +
      • End Time: 11:30
      • +
      • Max Participants: 25
      • +
      +
    8. +
    9. Save
    10. +
    +

    Expected:

    +
      +
    • Session is created in “Scheduled” state
    • +
    • Duration shows 2.5 hours (auto-computed)
    • +
    • Statusbar shows: Scheduled > In Progress > Completed
    • +
    • “Start” button is visible and highlighted
    • +
    • “Cancel” button is visible (not highlighted)
    • +
    • “Complete” button is NOT visible
    • +
    +

    4.2 User Cannot Create a Session

    +
      +
    1. Log in as Session User
    2. +
    3. Go to Session Tracking > Sessions > All Sessions
    4. +
    5. Try to click New
    6. +
    +

    Expected: An access error is raised. Session users cannot create +sessions.

    +

    4.3 Form View Structure

    +
      +
    1. Open any session in form view
    2. +
    +

    Expected:

    +
      +
    • Header: Action buttons (Start/Complete/Cancel based on state) + +statusbar
    • +
    • Ribbons: Green “Completed” ribbon on completed sessions, red +“Cancelled” ribbon on cancelled
    • +
    • Button Box: “Attended” stat button with user icon (shows +attendance count)
    • +
    • Title: Session name in large text
    • +
    • Main Info: Two-column layout
        +
      • Left: Session Type, Date, Location, Area
      • +
      • Right: Facilitator, Co-Facilitators, Start Time, End Time, Duration, +Max Participants, Company
      • +
      +
    • +
    • Tabs: Participants, Attendance, Topics (conditional), Notes
    • +
    • Chatter: Message log and activity tracking at the bottom
    • +
    +

    4.4 Duration Auto-computation

    +
      +
    1. Create or edit a session
    2. +
    3. Set Start Time: 09:00, End Time: 12:00
    4. +
    +

    Expected: Duration shows 3.0 hours automatically.

    +
      +
    1. Clear the End Time
    2. +
    +

    Expected: Duration shows 0.0.

    +

    4.5 Time Validation

    +
      +
    1. Create or edit a session
    2. +
    3. Set Start Time: 14:00, End Time: 10:00
    4. +
    5. Save
    6. +
    +

    Expected: Validation error: “End time must be after start time.”

    +

    4.6 Topics Tab Visibility

    +
      +
    1. Open a session with type “Family Development Session” +(track_topics=True)
    2. +
    +

    Expected: “Topics” tab is visible. Can select from the FDS topics.

    +
      +
    1. Open a session with type “Group Meeting” (track_topics=False)
    2. +
    +

    Expected: “Topics” tab is NOT visible.

    +

    4.7 Participants Tab

    +
      +
    1. Open a session and go to the Participants tab
    2. +
    3. Click Add a line
    4. +
    5. Select contacts as participants
    6. +
    +

    Expected: Participants are listed by name in a simple list format.

    +

    4.8 Attended Stat Button

    +
      +
    1. Open a session that has attendance records
    2. +
    3. Click the “Attended” stat button
    4. +
    +

    Expected: A popup list opens showing all attendance records for this +session.

    +
    +
    +

    5. Session State Workflow

    +

    5.1 Scheduled to In Progress

    +
      +
    1. Open a session in “Scheduled” state
    2. +
    3. Click the Start button
    4. +
    +

    Expected:

    +
      +
    • State changes to “In Progress”
    • +
    • “Start” button disappears
    • +
    • “Complete” button appears (highlighted)
    • +
    • “Cancel” button remains visible
    • +
    • Statusbar updates to show “In Progress” as current step
    • +
    +

    5.2 In Progress to Completed

    +
      +
    1. Open a session in “In Progress” state
    2. +
    3. Click the Complete button
    4. +
    +

    Expected:

    +
      +
    • State changes to “Completed”
    • +
    • Green “Completed” ribbon appears on the form
    • +
    • All “Start”, “Complete”, and “Cancel” buttons disappear
    • +
    • All fields become read-only (name, type, date, location, participants, +attendance, etc.)
    • +
    +

    5.3 Cancel from Scheduled

    +
      +
    1. Open a session in “Scheduled” state
    2. +
    3. Click Cancel
    4. +
    +

    Expected:

    +
      +
    • A confirmation dialog appears: “Are you sure you want to cancel this +session?”
    • +
    • Click OK
    • +
    • State changes to “Cancelled”
    • +
    • Red “Cancelled” ribbon appears
    • +
    • All fields become read-only
    • +
    • All action buttons disappear
    • +
    +

    5.4 Cancel from In Progress

    +
      +
    1. Open a session in “In Progress” state
    2. +
    3. Click Cancel
    4. +
    +

    Expected: Same confirmation and behavior as 5.3.

    +

    5.5 Invalid Transitions (Negative Tests)

    +

    These transitions should NOT be possible via the UI (buttons are +hidden), but verify:

    +
      +
    1. A “Completed” session has no Start, Complete, or Cancel buttons
    2. +
    3. A “Cancelled” session has no Start, Complete, or Cancel buttons
    4. +
    5. A “Scheduled” session has no Complete button (must Start first)
    6. +
    +

    Expected: Buttons are hidden for invalid transitions. The form is +fully read-only for Completed and Cancelled sessions.

    +

    5.6 Chatter Tracking

    +
      +
    1. Perform any state transition on a session
    2. +
    +

    Expected: The chatter logs the state change (e.g., “State: Scheduled +-> In Progress”). Changes to Name, Session Type, Date, and Facilitator +are also tracked.

    +
    +
    +

    6. Attendance Management

    +

    6.1 Record Attendance

    +
      +
    1. Open a session in “Scheduled” or “In Progress” state
    2. +
    3. Go to the Attendance tab
    4. +
    5. Click Add a line in the attendance list
    6. +
    7. Select a participant
    8. +
    9. Check “Attended” checkbox
    10. +
    11. Optionally set Attendance Time, Excused, Excuse Reason, and Notes
    12. +
    13. Save
    14. +
    +

    Expected: Attendance record is created. Row is highlighted green +(attended).

    +

    6.2 Attendance Decorations

    +
      +
    1. Create attendance records with different statuses:
        +
      • Participant A: Attended = True
      • +
      • Participant B: Attended = False, Excused = True
      • +
      • Participant C: Attended = False, Excused = False
      • +
      +
    2. +
    +

    Expected:

    +
      +
    • Participant A row: green (success decoration)
    • +
    • Participant B row: yellow/orange (warning decoration)
    • +
    • Participant C row: grey/muted
    • +
    +

    6.3 Attendance Count and Rate

    +
      +
    1. Create a session with 4 expected participants
    2. +
    3. Record attendance: 3 attended, 1 did not
    4. +
    +

    Expected:

    +
      +
    • Attendance Count shows 3 (in the tab and in the stat button)
    • +
    • Attendance Rate shows 75.0%
    • +
    +

    6.4 Attendance Rate with Zero Expected Participants

    +
      +
    1. Create a session with NO expected participants
    2. +
    3. Record one attendance as attended
    4. +
    +

    Expected: Attendance Rate shows 0.0% (no expected participants to +calculate against).

    +

    6.5 Duplicate Attendance Prevention

    +
      +
    1. Open a session and go to the Attendance tab
    2. +
    3. Add an attendance record for “Participant A”
    4. +
    5. Try to add another attendance record for the same “Participant A”
    6. +
    7. Save
    8. +
    +

    Expected: Error: “A participant can only have one attendance record +per session.”

    +

    6.6 Attendance Read-only on Completed/Cancelled

    +
      +
    1. Open a completed or cancelled session
    2. +
    3. Go to the Attendance tab
    4. +
    +

    Expected: The attendance list is read-only. No “Add a line” button +is available.

    +
    +
    +

    7. Session Views

    +

    7.1 List View

    +
      +
    1. Go to Session Tracking > Sessions > All Sessions
    2. +
    +

    Expected:

    +
      +
    • Columns: Date, Name, Session Type, Facilitator, Location, Attendance +Count, Attendance Rate, State
    • +
    • Location is visible by default (optional=show)
    • +
    • Area is hidden by default (optional=hide)
    • +
    • State shows as a colored badge:
        +
      • Scheduled: blue (info)
      • +
      • In Progress: yellow (warning)
      • +
      • Completed: green (success)
      • +
      • Cancelled: grey (muted)
      • +
      +
    • +
    • Row colors match the state badge colors
    • +
    • Default sort: Date descending (newest first)
    • +
    +

    7.2 Optional Column Toggles

    +
      +
    1. In the list view, click the column selector (gear icon or right-click +header)
    2. +
    3. Toggle Location off, Area on
    4. +
    +

    Expected: Location column hides, Area column appears. Settings +persist within the session.

    +

    7.3 Calendar View

    +
      +
    1. Switch to Calendar view (click the calendar icon in the view +switcher)
    2. +
    +

    Expected:

    +
      +
    • Monthly calendar is displayed
    • +
    • Sessions appear on their scheduled dates
    • +
    • Sessions are color-coded by Session Type
    • +
    • Clicking a date does NOT open a quick-create popup (quick_create is +disabled)
    • +
    • Clicking a session shows: Name, Facilitator, State
    • +
    +

    7.4 Kanban View

    +
      +
    1. Switch to Kanban view
    2. +
    +

    Expected:

    +
      +
    • Cards are grouped by State (Scheduled, In Progress, Completed, +Cancelled columns)
    • +
    • Each card shows: Name (bold), Session Type, Date, Facilitator, +Attendance Rate %
    • +
    • Progress bar at the top of each column shows color-coded distribution
    • +
    • Quick-create is disabled (no “+” button at top of columns)
    • +
    • Cards cannot be dragged between columns (state changes via buttons +only)
    • +
    +

    7.5 Graph View

    +
      +
    1. Switch to Graph view
    2. +
    +

    Expected: A bar chart showing Duration Hours grouped by Session +Type.

    +

    7.6 Pivot View

    +
      +
    1. Switch to Pivot view
    2. +
    +

    Expected: A pivot table with Session Type as rows, Facilitator as +columns, and Duration Hours as the measure.

    +

    7.7 My Sessions View

    +
      +
    1. Go to Session Tracking > Sessions > My Sessions
    2. +
    +

    Expected: Only sessions where the current user is the facilitator +are shown (My Sessions filter is pre-applied).

    +
    +
    +

    8. Search and Filtering

    +

    8.1 Quick Search Fields

    +
      +
    1. In the search bar, type a session name
    2. +
    +

    Expected: Results are filtered by name.

    +
      +
    1. Clear and search by session type, facilitator, location, or area
    2. +
    +

    Expected: Each field filters correctly.

    +

    8.2 State Filters

    +
      +
    1. Click Filters in the search bar
    2. +
    3. Apply “Scheduled” filter
    4. +
    +

    Expected: Only scheduled sessions are shown.

    +
      +
    1. Test “In Progress”, “Completed”, and “Cancelled” filters individually
    2. +
    +

    Expected: Each filter works correctly.

    +

    8.3 Ownership Filters

    +
      +
    1. Apply “My Sessions” filter
    2. +
    +

    Expected: Only sessions where you are the facilitator are shown.

    +
      +
    1. Apply “My Co-facilitated Sessions” filter
    2. +
    +

    Expected: Only sessions where you are a co-facilitator are shown.

    +

    8.4 Date Filter

    +
      +
    1. Apply “This Month” filter
    2. +
    +

    Expected: Only sessions with dates in the current month are shown.

    +

    8.5 Group By

    +
      +
    1. Use Group By > Session Type
    2. +
    +

    Expected: Sessions are grouped by their session type with counts.

    +
      +
    1. Test Group By: Facilitator, State, and Date
    2. +
    +

    Expected: Each grouping works correctly.

    +

    8.6 Combined Filters

    +
      +
    1. Apply “Scheduled” filter AND “My Sessions” filter together
    2. +
    +

    Expected: Only your scheduled sessions are shown. Filters combine +with AND logic.

    +
    +
    +

    9. Security and Access Control

    +

    9.1 Session User - Read All Sessions

    +
      +
    1. Log in as Session User
    2. +
    3. Go to Session Tracking > Sessions > All Sessions
    4. +
    +

    Expected: All sessions are visible (including ones facilitated by +others), regardless of facilitator.

    +

    9.2 Session User - Edit Own Facilitated Session

    +
      +
    1. Log in as Session User
    2. +
    3. Open a session where you are the facilitator
    4. +
    5. Edit the Location field and save
    6. +
    +

    Expected: Save succeeds. You can edit sessions you facilitate.

    +

    9.3 Session User - Edit Co-facilitated Session

    +
      +
    1. Log in as Session User
    2. +
    3. Open a session where you are a co-facilitator (but not the main +facilitator)
    4. +
    5. Edit the Location field and save
    6. +
    +

    Expected: Save succeeds. You can edit sessions you co-facilitate.

    +

    9.4 Session User - Cannot Edit Another’s Session

    +
      +
    1. Log in as Session User
    2. +
    3. Open a session where you are NOT the facilitator or co-facilitator
    4. +
    5. Try to edit the Location field and save
    6. +
    +

    Expected: Access error is raised. You cannot modify sessions you +don’t facilitate.

    +

    9.5 Session User - Cannot Create Sessions

    +
      +
    1. Log in as Session User
    2. +
    3. Try to create a new session
    4. +
    +

    Expected: Access error. Only managers can create sessions.

    +

    9.6 Session User - Cannot Delete Sessions

    +
      +
    1. Log in as Session User
    2. +
    3. Open a session you facilitate
    4. +
    5. Try to delete it (Action > Delete)
    6. +
    +

    Expected: Access error. Users cannot delete sessions.

    +

    9.7 Session User - Attendance on Own Sessions

    +
      +
    1. Log in as Session User
    2. +
    3. Open a session you facilitate
    4. +
    5. Go to the Attendance tab and add an attendance record
    6. +
    +

    Expected: Attendance record is created successfully.

    +

    9.8 Session User - Cannot Add Attendance on Others’ Sessions

    +
      +
    1. Log in as Session User
    2. +
    3. Open a session you do NOT facilitate or co-facilitate
    4. +
    5. Try to add an attendance record
    6. +
    +

    Expected: Access error. You cannot create attendance on sessions you +don’t facilitate.

    +

    9.9 Session User - Cannot Delete Attendance

    +
      +
    1. Log in as Session User
    2. +
    3. Open a session with attendance records
    4. +
    5. Try to delete an attendance record
    6. +
    +

    Expected: Access error. Users cannot delete attendance records.

    +

    9.10 Session Manager - Full Access

    +
      +
    1. Log in as Session Manager
    2. +
    3. Create, read, update, and delete sessions, session types, topics, and +attendance
    4. +
    +

    Expected: All operations succeed. Managers have full CRUD on +everything.

    +

    9.11 Admin Inherits Manager

    +
      +
    1. Log in as OpenSPP Admin (spp_security.group_spp_admin)
    2. +
    3. Verify you can access all Session Tracking features
    4. +
    +

    Expected: Admin has full access (manager group is implied by admin).

    +

    9.12 Multi-Company Isolation

    +

    Requires multi-company setup with at least 2 companies.

    +
      +
    1. Log in as a user belonging to Company A only
    2. +
    3. Go to All Sessions
    4. +
    +

    Expected: Only sessions belonging to Company A (or no company) are +visible. Sessions belonging to Company B are NOT visible.

    +
    +
    +

    10. Data Integrity Constraints

    +

    10.1 Delete Session Type with Existing Sessions

    +
      +
    1. Create a session using a specific session type
    2. +
    3. Try to delete that session type
    4. +
    +

    Expected: Error preventing deletion: the session type cannot be +deleted while sessions reference it (ondelete=restrict).

    +

    10.2 Delete Participant with Attendance Records

    +
      +
    1. Create an attendance record for a participant
    2. +
    3. Try to delete that participant (contact) from the system
    4. +
    +

    Expected: Error preventing deletion: the participant cannot be +deleted while attendance records reference them (ondelete=restrict).

    +

    10.3 Delete Facilitator with Sessions

    +
      +
    1. Create a session with a specific facilitator
    2. +
    3. Try to delete that user from the system
    4. +
    +

    Expected: Error preventing deletion (ondelete=restrict).

    +
    +
    +

    11. Cross-module Integration (spp_case_session)

    +

    Only applicable if ``spp_case_session`` module is also installed.

    +

    11.1 Case Stat Button on Session Form

    +
      +
    1. Open any session in form view
    2. +
    3. Check the button box area
    4. +
    +

    Expected: If spp_case_session is installed, a “Cases” stat button +appears alongside the “Attended” stat button, inside the same button +box. There should NOT be duplicate button boxes.

    +
    +
    +

    12. Edge Cases

    +

    12.1 Session with No Times

    +
      +
    1. Create a session without setting Start Time or End Time
    2. +
    +

    Expected: Duration shows 0.0. No validation error.

    +

    12.2 Session with Only Start Time

    +
      +
    1. Create a session with Start Time = 09:00 but no End Time
    2. +
    +

    Expected: Duration shows 0.0. No validation error.

    +

    12.3 Session with Only End Time

    +
      +
    1. Create a session with End Time = 12:00 but no Start Time
    2. +
    +

    Expected: Duration shows 0.0. No validation error.

    +

    12.4 Empty Session (No Participants, No Attendance)

    +
      +
    1. Create a session with no expected participants and no attendance
    2. +
    +

    Expected: Attendance Count = 0, Attendance Rate = 0.0%. Session +functions normally.

    +

    12.5 Session Type Code Left Blank

    +
      +
    1. Create two session types with Code left blank
    2. +
    +

    Expected: Both are created successfully. The unique constraint +allows multiple blank codes.

    +
    +
    +

    Test Summary Checklist

    +

    Use this checklist to track testing progress:

    +
      +
      1. +
      2. Module installation and demo data (1.1 - 1.3)
      3. +
      +
    • +
      1. +
      2. Menu structure and navigation (2.1 - 2.3)
      3. +
      +
    • +
      1. +
      2. Session type management (3.1 - 3.7)
      3. +
      +
    • +
      1. +
      2. Session creation and form view (4.1 - 4.8)
      3. +
      +
    • +
      1. +
      2. Session state workflow (5.1 - 5.6)
      3. +
      +
    • +
      1. +
      2. Attendance management (6.1 - 6.6)
      3. +
      +
    • +
      1. +
      2. Session views (7.1 - 7.7)
      3. +
      +
    • +
      1. +
      2. Search and filtering (8.1 - 8.6)
      3. +
      +
    • +
      1. +
      2. Security and access control (9.1 - 9.12)
      3. +
      +
    • +
      1. +
      2. Data integrity constraints (10.1 - 10.3)
      3. +
      +
    • +
      1. +
      2. Cross-module integration (11.1)
      3. +
      +
    • +
      1. +
      2. Edge cases (12.1 - 12.5)
      3. +
      +
    • +
    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -510,15 +1283,15 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • OpenSPP.org
    -

    Maintainers

    +

    Maintainers

    Current maintainers:

    jeremi gonzalesedwin1123 emjay0921

    This module is part of the OpenSPP/OpenSPP2 project on GitHub.

    diff --git a/spp_session_tracking/tests/__init__.py b/spp_session_tracking/tests/__init__.py index 35c2ad2a..23f6bb15 100644 --- a/spp_session_tracking/tests/__init__.py +++ b/spp_session_tracking/tests/__init__.py @@ -2,3 +2,5 @@ from . import test_session from . import test_attendance +from . import test_security +from . import test_constraints diff --git a/spp_session_tracking/tests/test_attendance.py b/spp_session_tracking/tests/test_attendance.py index 5cb4c76c..d3a49e54 100644 --- a/spp_session_tracking/tests/test_attendance.py +++ b/spp_session_tracking/tests/test_attendance.py @@ -1,6 +1,6 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. -from odoo import fields +from odoo import Command, fields from odoo.tests.common import TransactionCase @@ -12,6 +12,7 @@ def setUpClass(cls): super().setUpClass() cls.Session = cls.env["spp.session"] cls.SessionType = cls.env["spp.session.type"] + cls.SessionTopic = cls.env["spp.session.topic"] cls.Attendance = cls.env["spp.session.attendance"] cls.Partner = cls.env["res.partner"] @@ -34,8 +35,8 @@ def setUpClass(cls): "date": fields.Date.today(), "facilitator_id": cls.facilitator.id, "expected_participant_ids": [ - (4, cls.participant1.id), - (4, cls.participant2.id), + Command.link(cls.participant1.id), + Command.link(cls.participant2.id), ], } ) @@ -122,3 +123,203 @@ def test_attendance_with_notes(self): } ) self.assertEqual(attendance.notes, "Called in sick") + + # --- Edge case tests --- + + def test_attendance_rate_zero_expected(self): + """Test attendance rate is 0.0 when session has no expected participants.""" + empty_session = self.Session.create( + { + "name": "No Expected Participants Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + self.Attendance.create( + { + "session_id": empty_session.id, + "participant_id": self.participant1.id, + "is_attended": True, + } + ) + empty_session.invalidate_recordset(["attendance_count", "attendance_rate"]) + self.assertEqual(empty_session.attendance_rate, 0.0) + + def test_attendance_rate_no_records(self): + """Test attendance rate and count are 0 when expected participants exist but no records.""" + no_records_session = self.Session.create( + { + "name": "No Attendance Records Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "expected_participant_ids": [ + Command.link(self.participant1.id), + Command.link(self.participant2.id), + ], + } + ) + self.assertEqual(no_records_session.attendance_rate, 0.0) + self.assertEqual(no_records_session.attendance_count, 0) + + def test_attendance_count_all_excused(self): + """Test attendance count is 0 when all records have is_attended=False.""" + excused_session = self.Session.create( + { + "name": "All Excused Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "expected_participant_ids": [ + Command.link(self.participant1.id), + Command.link(self.participant2.id), + ], + } + ) + self.Attendance.create( + { + "session_id": excused_session.id, + "participant_id": self.participant1.id, + "is_attended": False, + "is_excused": True, + } + ) + self.Attendance.create( + { + "session_id": excused_session.id, + "participant_id": self.participant2.id, + "is_attended": False, + "is_excused": True, + } + ) + excused_session.invalidate_recordset(["attendance_count", "attendance_rate"]) + self.assertEqual(excused_session.attendance_count, 0) + + # --- Topic tests --- + + def test_topic_assignment_with_track_topics_enabled(self): + """Test assigning topics to a session with a type that has track_topics=True.""" + topic_type = self.SessionType.create( + { + "name": "Topic Tracking Type", + "code": "TOPTRACK", + "track_topics": True, + } + ) + topic1 = self.SessionTopic.create( + { + "name": "Introduction", + "session_type_id": topic_type.id, + } + ) + topic2 = self.SessionTopic.create( + { + "name": "Advanced Techniques", + "session_type_id": topic_type.id, + } + ) + session_with_topics = self.Session.create( + { + "name": "Session With Topics", + "session_type_id": topic_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "topic_ids": [ + Command.link(topic1.id), + Command.link(topic2.id), + ], + } + ) + self.assertEqual(len(session_with_topics.topic_ids), 2) + self.assertIn(topic1, session_with_topics.topic_ids) + self.assertIn(topic2, session_with_topics.topic_ids) + + def test_topic_assignment_type_without_tracking(self): + """Test that topics can be assigned to a session even when track_topics is False.""" + no_track_type = self.SessionType.create( + { + "name": "No Topic Tracking Type", + "code": "NOTRACK", + "track_topics": False, + } + ) + standalone_topic = self.SessionTopic.create( + { + "name": "Standalone Topic", + } + ) + session_without_tracking = self.Session.create( + { + "name": "Session Without Topic Tracking", + "session_type_id": no_track_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "topic_ids": [Command.link(standalone_topic.id)], + } + ) + self.assertEqual(len(session_without_tracking.topic_ids), 1) + self.assertIn(standalone_topic, session_without_tracking.topic_ids) + + # --- Related field and field coverage tests --- + + def test_attendance_related_fields(self): + """Test session_date and session_type_id are populated from session.""" + attendance = self.Attendance.create( + { + "session_id": self.session.id, + "participant_id": self.participant1.id, + "is_attended": True, + } + ) + self.assertEqual(attendance.session_date, self.session.date) + self.assertEqual(attendance.session_type_id, self.session_type) + + def test_attendance_with_time(self): + """Test attendance record with attendance_time.""" + now = fields.Datetime.now() + attendance = self.Attendance.create( + { + "session_id": self.session.id, + "participant_id": self.participant1.id, + "is_attended": True, + "attendance_time": now, + } + ) + self.assertEqual(attendance.attendance_time, now) + + def test_attendance_excused_with_reason(self): + """Test excused attendance with reason.""" + attendance = self.Attendance.create( + { + "session_id": self.session.id, + "participant_id": self.participant1.id, + "is_attended": False, + "is_excused": True, + "excuse_reason": "Medical appointment", + } + ) + self.assertTrue(attendance.is_excused) + self.assertEqual(attendance.excuse_reason, "Medical appointment") + + def test_topic_full_creation(self): + """Test topic creation with all fields.""" + topic = self.SessionTopic.create( + { + "name": "Full Topic", + "code": "FULL", + "description": "A complete topic", + "sequence": 5, + } + ) + self.assertEqual(topic.code, "FULL") + self.assertEqual(topic.description, "A complete topic") + self.assertEqual(topic.sequence, 5) + self.assertTrue(topic.active) + + def test_topic_archive(self): + """Test topic can be archived.""" + topic = self.SessionTopic.create({"name": "Archivable Topic"}) + self.assertTrue(topic.active) + topic.active = False + self.assertFalse(topic.active) diff --git a/spp_session_tracking/tests/test_constraints.py b/spp_session_tracking/tests/test_constraints.py new file mode 100644 index 00000000..6b2358a7 --- /dev/null +++ b/spp_session_tracking/tests/test_constraints.py @@ -0,0 +1,135 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from psycopg2 import IntegrityError + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestSessionConstraints(TransactionCase): + """Test database and model constraints for session tracking.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Session = cls.env["spp.session"] + cls.SessionType = cls.env["spp.session.type"] + cls.Attendance = cls.env["spp.session.attendance"] + cls.Partner = cls.env["res.partner"] + + cls.session_type = cls.SessionType.create( + { + "name": "Constraint Test Type", + "code": "CONSTR", + } + ) + + cls.facilitator = cls.env.user + + cls.participant1 = cls.Partner.create({"name": "Constraint Participant 1"}) + cls.participant2 = cls.Partner.create({"name": "Constraint Participant 2"}) + + cls.session = cls.Session.create( + { + "name": "Constraint Test Session", + "session_type_id": cls.session_type.id, + "date": fields.Date.today(), + "facilitator_id": cls.facilitator.id, + } + ) + + def test_duplicate_attendance_raises_integrity_error(self): + """Test that creating two attendance records for the same participant and session raises IntegrityError.""" + self.Attendance.create( + { + "session_id": self.session.id, + "participant_id": self.participant1.id, + "is_attended": True, + } + ) + with self.assertRaises(IntegrityError), self.cr.savepoint(): + self.Attendance.create( + { + "session_id": self.session.id, + "participant_id": self.participant1.id, + "is_attended": False, + } + ) + + def test_end_time_before_start_time_raises_validation_error(self): + """Test that setting end_time before start_time raises ValidationError.""" + with self.assertRaises(ValidationError): + self.Session.create( + { + "name": "Invalid Time Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "start_time": 14.0, + "end_time": 10.0, + } + ) + + def test_end_time_before_start_time_on_write_raises_validation_error(self): + """Test that writing end_time before start_time raises ValidationError.""" + session = self.Session.create( + { + "name": "Valid Time Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "start_time": 9.0, + "end_time": 11.0, + } + ) + with self.assertRaises(ValidationError): + session.write({"end_time": 8.0}) + + def test_delete_session_type_with_sessions_raises_integrity_error(self): + """Test that deleting a session type that has sessions raises IntegrityError.""" + session_type_to_delete = self.SessionType.create( + { + "name": "Type With Sessions", + "code": "TWSESS", + } + ) + self.Session.create( + { + "name": "Session Blocking Deletion", + "session_type_id": session_type_to_delete.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + with self.assertRaises(IntegrityError), self.cr.savepoint(): + session_type_to_delete.unlink() + + def test_delete_participant_with_attendance_raises_integrity_error(self): + """Test that deleting a participant who has attendance records raises IntegrityError.""" + participant_to_delete = self.Partner.create({"name": "Participant With Attendance"}) + self.Attendance.create( + { + "session_id": self.session.id, + "participant_id": participant_to_delete.id, + "is_attended": True, + } + ) + with self.assertRaises(IntegrityError), self.cr.savepoint(): + participant_to_delete.unlink() + + def test_unique_session_type_code_raises_integrity_error(self): + """Test that creating two session types with the same code raises IntegrityError.""" + self.SessionType.create( + { + "name": "Unique Code Type A", + "code": "DUPCODE", + } + ) + with self.assertRaises(IntegrityError), self.cr.savepoint(): + self.SessionType.create( + { + "name": "Unique Code Type B", + "code": "DUPCODE", + } + ) diff --git a/spp_session_tracking/tests/test_security.py b/spp_session_tracking/tests/test_security.py new file mode 100644 index 00000000..a1ffb967 --- /dev/null +++ b/spp_session_tracking/tests/test_security.py @@ -0,0 +1,273 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from odoo import Command, fields +from odoo.exceptions import AccessError +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSessionSecurity(TransactionCase): + """Test access control for session tracking module.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.SessionType = cls.env["spp.session.type"] + cls.Session = cls.env["spp.session"] + cls.Attendance = cls.env["spp.session.attendance"] + cls.Partner = cls.env["res.partner"] + + cls.session_user = cls.env["res.users"].create( + { + "name": "Session User", + "login": "test_session_user", + "group_ids": [ + Command.link(cls.env.ref("spp_session_tracking.group_session_user").id), + ], + } + ) + cls.session_manager = cls.env["res.users"].create( + { + "name": "Session Manager", + "login": "test_session_manager", + "group_ids": [ + Command.link(cls.env.ref("spp_session_tracking.group_session_manager").id), + ], + } + ) + + cls.session_type = cls.SessionType.create( + { + "name": "Security Test Type", + "code": "SECTEST", + } + ) + + cls.participant1 = cls.Partner.create({"name": "Security Participant 1"}) + cls.participant2 = cls.Partner.create({"name": "Security Participant 2"}) + + # Session facilitated by session_user + cls.own_session = cls.Session.create( + { + "name": "Own Facilitated Session", + "session_type_id": cls.session_type.id, + "date": fields.Date.today(), + "facilitator_id": cls.session_user.id, + } + ) + + # Session where session_user is co-facilitator + cls.co_facilitated_session = cls.Session.create( + { + "name": "Co-Facilitated Session", + "session_type_id": cls.session_type.id, + "date": fields.Date.today(), + "facilitator_id": cls.session_manager.id, + "co_facilitator_ids": [Command.link(cls.session_user.id)], + } + ) + + # Session facilitated by someone else entirely (manager) + cls.other_session = cls.Session.create( + { + "name": "Another Facilitator Session", + "session_type_id": cls.session_type.id, + "date": fields.Date.today(), + "facilitator_id": cls.session_manager.id, + } + ) + + # Second company for multi-company tests + cls.company_b = cls.env["res.company"].create({"name": "Company B"}) + + cls.company_b_session = cls.Session.create( + { + "name": "Company B Session", + "session_type_id": cls.session_type.id, + "date": fields.Date.today(), + "facilitator_id": cls.session_manager.id, + "company_id": cls.company_b.id, + } + ) + + def test_group_hierarchy_manager_has_user(self): + """Test manager group implies user group permissions.""" + self.assertTrue( + self.session_manager.has_group("spp_session_tracking.group_session_user"), + "Manager should have user group permissions", + ) + + def test_admin_has_manager(self): + """Test OpenSPP admin has session manager access.""" + admin = self.env["res.users"].create( + { + "name": "Admin Test User", + "login": "test_admin_session", + "group_ids": [ + Command.link(self.env.ref("spp_security.group_spp_admin").id), + ], + } + ) + self.assertTrue( + admin.has_group("spp_session_tracking.group_session_manager"), + "SPP admin should have session manager access", + ) + + def test_user_can_read_all_sessions(self): + """Test session user can read all sessions including ones they don't facilitate.""" + sessions_as_user = self.Session.with_user(self.session_user).search([]) + session_ids = sessions_as_user.ids + self.assertIn(self.own_session.id, session_ids, "User should read own session") + self.assertIn(self.other_session.id, session_ids, "User should read sessions from other facilitators") + self.assertIn(self.co_facilitated_session.id, session_ids, "User should read co-facilitated sessions") + + def test_user_can_read_session_not_facilitated(self): + """Test session user can read a session where they have no facilitation role.""" + session_as_user = self.other_session.with_user(self.session_user) + # Reading a field should not raise + self.assertEqual(session_as_user.name, "Another Facilitator Session") + + def test_user_can_write_own_facilitated_session(self): + """Test session user can write a session they facilitate.""" + self.own_session.with_user(self.session_user).write({"location": "Room A"}) + self.assertEqual(self.own_session.location, "Room A") + + def test_user_can_write_co_facilitated_session(self): + """Test session user can write a session where they are co-facilitator.""" + self.co_facilitated_session.with_user(self.session_user).write({"location": "Room B"}) + self.assertEqual(self.co_facilitated_session.location, "Room B") + + def test_user_cannot_write_other_facilitator_session(self): + """Test session user cannot write a session they do not facilitate or co-facilitate.""" + with self.assertRaises(AccessError): + self.other_session.with_user(self.session_user).write({"location": "Unauthorized"}) + + def test_user_can_create_attendance_on_own_session(self): + """Test session user can create attendance records on their own facilitated session.""" + attendance = self.Attendance.with_user(self.session_user).create( + { + "session_id": self.own_session.id, + "participant_id": self.participant1.id, + "is_attended": True, + } + ) + self.assertTrue(attendance.is_attended) + + def test_user_can_create_attendance_on_co_facilitated_session(self): + """Test session user can create attendance records on a co-facilitated session.""" + attendance = self.Attendance.with_user(self.session_user).create( + { + "session_id": self.co_facilitated_session.id, + "participant_id": self.participant1.id, + "is_attended": True, + } + ) + self.assertTrue(attendance.is_attended) + + def test_user_cannot_create_attendance_on_other_session(self): + """Test session user cannot create attendance on a session they do not facilitate.""" + with self.assertRaises(AccessError): + self.Attendance.with_user(self.session_user).create( + { + "session_id": self.other_session.id, + "participant_id": self.participant1.id, + "is_attended": True, + } + ) + + def test_manager_can_create_session(self): + """Test session manager can create sessions.""" + session = self.Session.with_user(self.session_manager).create( + { + "name": "Manager Created Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.session_manager.id, + } + ) + self.assertEqual(session.name, "Manager Created Session") + + def test_manager_can_read_all_sessions(self): + """Test session manager can read all sessions.""" + sessions_as_manager = self.Session.with_user(self.session_manager).search([]) + session_ids = sessions_as_manager.ids + self.assertIn(self.own_session.id, session_ids, "Manager should read user-facilitated session") + self.assertIn(self.other_session.id, session_ids, "Manager should read all sessions") + + def test_manager_can_write_any_session(self): + """Test session manager can write sessions regardless of facilitator.""" + self.own_session.with_user(self.session_manager).write({"location": "Manager Edit"}) + self.assertEqual(self.own_session.location, "Manager Edit") + + def test_manager_can_delete_session(self): + """Test session manager can delete sessions.""" + session_to_delete = self.Session.create( + { + "name": "Session To Delete", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.session_manager.id, + } + ) + session_to_delete.with_user(self.session_manager).unlink() + self.assertFalse(session_to_delete.exists(), "Session should have been deleted") + + def test_manager_can_create_session_type(self): + """Test session manager can create session types.""" + session_type = self.SessionType.with_user(self.session_manager).create( + { + "name": "Manager Created Type", + "code": "MGRTYPE", + } + ) + self.assertEqual(session_type.name, "Manager Created Type") + + def test_user_cannot_create_session_type(self): + """Test session user cannot create session types.""" + with self.assertRaises(AccessError): + self.SessionType.with_user(self.session_user).create( + { + "name": "User Created Type", + "code": "USRTYPE", + } + ) + + def test_user_cannot_create_session(self): + """Test session user cannot create sessions (manager-only).""" + with self.assertRaises(AccessError): + self.Session.with_user(self.session_user).create( + { + "name": "User Created Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.session_user.id, + } + ) + + def test_user_cannot_delete_session(self): + """Test session user cannot delete sessions.""" + with self.assertRaises(AccessError): + self.own_session.with_user(self.session_user).unlink() + + def test_user_cannot_delete_attendance(self): + """Test session user cannot delete attendance records.""" + attendance = self.Attendance.create( + { + "session_id": self.own_session.id, + "participant_id": self.participant1.id, + "is_attended": True, + } + ) + with self.assertRaises(AccessError): + attendance.with_user(self.session_user).unlink() + + def test_multi_company_user_cannot_read_other_company_session(self): + """Test user in company A cannot see sessions belonging to company B.""" + # session_user belongs to the default company (company A) + # company_b_session belongs to company B + sessions_as_user = self.Session.with_user(self.session_user).search([]) + self.assertNotIn( + self.company_b_session.id, + sessions_as_user.ids, + "User should not see sessions from a company they do not belong to", + ) diff --git a/spp_session_tracking/tests/test_session.py b/spp_session_tracking/tests/test_session.py index 8c69e3a2..4791302b 100644 --- a/spp_session_tracking/tests/test_session.py +++ b/spp_session_tracking/tests/test_session.py @@ -1,6 +1,7 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. -from odoo import fields +from odoo import Command, fields +from odoo.exceptions import UserError from odoo.tests.common import TransactionCase @@ -103,9 +104,275 @@ def test_session_with_expected_participants(self): "date": fields.Date.today(), "facilitator_id": self.facilitator.id, "expected_participant_ids": [ - (4, self.participant1.id), - (4, self.participant2.id), + Command.link(self.participant1.id), + Command.link(self.participant2.id), ], } ) self.assertEqual(len(session.expected_participant_ids), 2) + + # --- State transition tests --- + + def test_valid_transition_start(self): + """Test valid transition from scheduled to in_progress.""" + session = self.Session.create( + { + "name": "Start Transition Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + self.assertEqual(session.state, "scheduled") + session.action_start() + self.assertEqual(session.state, "in_progress") + + def test_valid_transition_complete(self): + """Test valid transition from in_progress to completed.""" + session = self.Session.create( + { + "name": "Complete Transition Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session.action_start() + session.action_complete() + self.assertEqual(session.state, "completed") + + def test_valid_transition_cancel_from_scheduled(self): + """Test valid transition from scheduled to cancelled.""" + session = self.Session.create( + { + "name": "Cancel From Scheduled Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session.action_cancel() + self.assertEqual(session.state, "cancelled") + + def test_valid_transition_cancel_from_in_progress(self): + """Test valid transition from in_progress to cancelled.""" + session = self.Session.create( + { + "name": "Cancel From In Progress Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session.action_start() + session.action_cancel() + self.assertEqual(session.state, "cancelled") + + def test_invalid_transition_start_from_completed(self): + """Test that starting a completed session raises UserError.""" + session = self.Session.create( + { + "name": "Start From Completed Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session.action_start() + session.action_complete() + with self.assertRaises(UserError): + session.action_start() + + def test_invalid_transition_start_from_cancelled(self): + """Test that starting a cancelled session raises UserError.""" + session = self.Session.create( + { + "name": "Start From Cancelled Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session.action_cancel() + with self.assertRaises(UserError): + session.action_start() + + def test_invalid_transition_complete_from_scheduled(self): + """Test that completing a scheduled session raises UserError.""" + session = self.Session.create( + { + "name": "Complete From Scheduled Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + with self.assertRaises(UserError): + session.action_complete() + + def test_invalid_transition_cancel_from_completed(self): + """Test that cancelling a completed session raises UserError.""" + session = self.Session.create( + { + "name": "Cancel From Completed Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session.action_start() + session.action_complete() + with self.assertRaises(UserError): + session.action_cancel() + + def test_invalid_transition_cancel_from_cancelled(self): + """Test that cancelling an already cancelled session raises UserError.""" + session = self.Session.create( + { + "name": "Cancel From Cancelled Test", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session.action_cancel() + with self.assertRaises(UserError): + session.action_cancel() + + # --- Computed field edge cases --- + + def test_duration_only_start_time(self): + """Test that duration is 0.0 when only start_time is set.""" + session = self.Session.create( + { + "name": "Only Start Time Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "start_time": 9.0, + } + ) + self.assertEqual(session.duration_hours, 0.0) + + def test_duration_only_end_time(self): + """Test that duration is 0.0 when only end_time is set.""" + session = self.Session.create( + { + "name": "Only End Time Session", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + "end_time": 12.0, + } + ) + self.assertEqual(session.duration_hours, 0.0) + + def test_session_type_session_count(self): + """Test that session_count on session type reflects actual sessions.""" + dedicated_type = self.SessionType.create( + { + "name": "Counted Type", + "code": "CNTTYPE", + } + ) + initial_count = dedicated_type.session_count + self.Session.create( + { + "name": "Count Session 1", + "session_type_id": dedicated_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + self.Session.create( + { + "name": "Count Session 2", + "session_type_id": dedicated_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + dedicated_type.invalidate_recordset(["session_count"]) + self.assertEqual(dedicated_type.session_count, initial_count + 2) + + def test_action_view_sessions(self): + """Test action_view_sessions returns correct action dictionary.""" + action = self.session_type.action_view_sessions() + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "spp.session") + self.assertEqual(action["domain"], [("session_type_id", "=", self.session_type.id)]) + self.assertEqual(action["context"]["default_session_type_id"], self.session_type.id) + + def test_session_type_frequency(self): + """Test session type frequency field.""" + session_type = self.SessionType.create( + { + "name": "Weekly Type", + "frequency": "weekly", + } + ) + self.assertEqual(session_type.frequency, "weekly") + + def test_session_type_archive(self): + """Test session type can be archived.""" + session_type = self.SessionType.create({"name": "Archivable Type"}) + self.assertTrue(session_type.active) + session_type.active = False + self.assertFalse(session_type.active) + + def test_session_type_count_multiple_types(self): + """Test session_count works correctly across multiple types simultaneously.""" + type_a = self.SessionType.create({"name": "Type A"}) + type_b = self.SessionType.create({"name": "Type B"}) + self.Session.create( + { + "name": "S1", + "session_type_id": type_a.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + self.Session.create( + { + "name": "S2", + "session_type_id": type_a.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + self.Session.create( + { + "name": "S3", + "session_type_id": type_b.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + (type_a | type_b).invalidate_recordset(["session_count"]) + self.assertEqual(type_a.session_count, 2) + self.assertEqual(type_b.session_count, 1) + + def test_write_state_multi_record_invalid(self): + """Test that write() on multiple records rejects invalid transition for any record.""" + session1 = self.Session.create( + { + "name": "Multi 1", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session2 = self.Session.create( + { + "name": "Multi 2", + "session_type_id": self.session_type.id, + "date": fields.Date.today(), + "facilitator_id": self.facilitator.id, + } + ) + session1.action_start() + session1.action_complete() + # session1 is completed, session2 is scheduled + # Both to in_progress should fail because session1 cannot transition + with self.assertRaises(UserError): + (session1 | session2).write({"state": "in_progress"}) diff --git a/spp_session_tracking/views/session_type_views.xml b/spp_session_tracking/views/session_type_views.xml index 4d15456d..88763a54 100644 --- a/spp_session_tracking/views/session_type_views.xml +++ b/spp_session_tracking/views/session_type_views.xml @@ -36,16 +36,17 @@ />
    - - + + + - + - + - - - - - - - - + + + - - - spp.session.type.tree + + + spp.session.type.list spp.session.type - + - + @@ -91,6 +87,7 @@ + Session Types spp.session.type - list,form {'search_default_active': 1} diff --git a/spp_session_tracking/views/session_views.xml b/spp_session_tracking/views/session_views.xml index 9d6d6553..a4e2d215 100644 --- a/spp_session_tracking/views/session_views.xml +++ b/spp_session_tracking/views/session_views.xml @@ -1,5 +1,14 @@ + + + Attendance Records + spp.session.attendance + list + {'default_session_id': active_id} + [('session_id', '=', active_id)] + + spp.session.form @@ -26,6 +35,7 @@ string="Cancel" type="object" invisible="state in ['completed', 'cancelled']" + confirm="Are you sure you want to cancel this session?" /> - - + + +
    + +
    +
    +

    +

    +
    + + - + - - - - + + + + + + - + - + - + @@ -123,7 +173,8 @@ - +
    - - - spp.session.tree + + + spp.session.list spp.session - + - - - + + + + @@ -194,32 +254,35 @@ spp.session.kanban spp.session - + + -
    -
    -
    - - - -
    -
    - -
    - -
    - -
    - Attendance: % -
    +
    + + + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    Attendance: %
    @@ -238,6 +301,7 @@ + + + + + + + spp.session.graph + spp.session + + + + + + + + + + + spp.session.pivot + spp.session + + + + + + + + + Sessions spp.session - - list,form,calendar,kanban + list,form,calendar,kanban,graph,pivot

    Create a new session @@ -310,8 +409,7 @@ My Sessions spp.session - - list,form,calendar,kanban + list,form,calendar,kanban,graph,pivot {'search_default_my_sessions': 1}