Skip to content

feat(attendance): auto-populate employee shift#4443

Open
krishna-254 wants to merge 1 commit intofrappe:developfrom
krishna-254:feat/auto-populate-employee-shift
Open

feat(attendance): auto-populate employee shift#4443
krishna-254 wants to merge 1 commit intofrappe:developfrom
krishna-254:feat/auto-populate-employee-shift

Conversation

@krishna-254
Copy link
Copy Markdown
Collaborator

@krishna-254 krishna-254 commented Apr 24, 2026

Closes #4413
Changes:

  • Auto populate employee shift when employee or date is selected.
  • In Attendance shift is auto populated according to shift assignment for the selected employee and attendance date.
  • In Attendance request shift is auto populated according to the shift assignment for the selected employee and from date.

Summary by CodeRabbit

Release Notes

  • New Features
    • Shift field is now automatically populated on Attendance forms when employee or attendance date is set or changed.
    • Shift field is now automatically populated on Attendance Request forms when employee or date is set, eliminating manual entry.

@krishna-254 krishna-254 marked this pull request as ready for review April 24, 2026 04:37
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Walkthrough

This pull request introduces automatic shift population functionality across two forms: Attendance and Attendance Request. When an employee is assigned or the attendance/from date is modified, and the shift field remains unset, the forms trigger a server call to fetch the applicable shift type. A new whitelisted backend method get_employee_shift retrieves the employee's active shift assignment for the specified date, defaulting to today if no date is provided. The method includes permission checks and returns the shift type or None if no matching assignment exists.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: automatic population of employee shift based on employee and date selection across Attendance and Attendance Request forms.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
hrms/hr/doctype/attendance_request/attendance_request.js (1)

27-37: Minor: the two handlers are identical and can just call set_employee_shift directly.

employee(frm) and from_date(frm) run the exact same guard, and set_employee_shift already re-checks employee/from_date before the server call. You can collapse both to frm.trigger("set_employee_shift") and centralize the !frm.doc.shift guard there to avoid drift.

♻️ Possible refactor
-	employee(frm) {
-		if (frm.doc.employee && frm.doc.from_date && !frm.doc.shift) {
-			frm.trigger("set_employee_shift");
-		}
-	},
-
-	from_date(frm) {
-		if (frm.doc.employee && frm.doc.from_date && !frm.doc.shift) {
-			frm.trigger("set_employee_shift");
-		}
-	},
-
-	set_employee_shift(frm) {
-		if (!frm.doc.employee || !frm.doc.from_date) return;
+	employee(frm) {
+		frm.trigger("set_employee_shift");
+	},
+
+	from_date(frm) {
+		frm.trigger("set_employee_shift");
+	},
+
+	set_employee_shift(frm) {
+		if (!frm.doc.employee || !frm.doc.from_date || frm.doc.shift) return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hrms/hr/doctype/attendance_request/attendance_request.js` around lines 27 -
37, Both employee(frm) and from_date(frm) duplicate the same guard and can
simply trigger set_employee_shift; update the two handlers (employee and
from_date) to call frm.trigger("set_employee_shift") directly, then move the
!frm.doc.shift check into the start of the set_employee_shift handler so that
set_employee_shift re-checks employee, from_date, and !frm.doc.shift before
making any server calls.
hrms/hr/doctype/attendance/attendance.py (1)

478-478: Nit: redundant employee and in permission check.

The earlier guard on line 475 already returns when employee is falsy, so the employee and conjunct here is dead. Minor readability:

🧹 Tiny cleanup
-	if employee and not frappe.has_permission("Employee", "read", employee):
+	if not frappe.has_permission("Employee", "read", employee):
 		return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hrms/hr/doctype/attendance/attendance.py` at line 478, Remove the redundant
guard in the permission check by dropping the `employee and` conjunct so the
condition becomes just `if not frappe.has_permission("Employee", "read",
employee):`; this keeps the earlier falsy-employee early return intact and
preserves the intended permission check in the attendance.py code line that
currently reads `if employee and not frappe.has_permission("Employee", "read",
employee):`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hrms/hr/doctype/attendance_request/attendance_request.js`:
- Around line 27-55: The JS shift auto-population currently calls the backend
method "hrms.hr.doctype.attendance.attendance.get_employee_shift" from
set_employee_shift (and triggered by employee and from_date handlers), but that
backend only accepts employee and for_date and drops consider_default_shift;
update the frappe.call in set_employee_shift to call
"hrms.hr.doctype.shift_assignment.shift_assignment.get_employee_shift" instead,
keeping the same args (employee, for_date from frm.doc.from_date,
consider_default_shift: true) and the same callback logic that sets
frm.set_value("shift", r.message) when r.message exists and frm.doc.shift is
empty.

In `@hrms/hr/doctype/attendance/attendance.js`:
- Line 50: The object property for_date currently uses an unreachable fallback
("for_date: frm.doc.attendance_date || frappe.datetime.get_today()"); remove the
dead fallback so it reads "for_date: frm.doc.attendance_date" (reference:
attendance.js, frm.doc.attendance_date, frappe.datetime.get_today(), for_date) —
the backend already defaults to nowdate(), so dropping the "||
frappe.datetime.get_today()" avoids misleading readers.

In `@hrms/hr/doctype/attendance/attendance.py`:
- Around line 489-500: The query fetching Shift Assignment via frappe.get_all
(assigning to shifts) can return expired records because it only filters on
"start_date" <= for_date; update the filters for the "Shift Assignment" query to
also require that "end_date" is either null/empty or >= for_date (i.e., add an
end_date condition using ("is", "NULL") OR ("<=", for_date) appropriately),
referencing the existing variables employee and for_date; alternatively, replace
this local lookup with the existing shift_assignment.get_employee_shift helper
which already enforces end_date correctly.
- Around line 473-505: The current get_employee_shift in attendance.py should be
replaced with a thin wrapper that delegates to the canonical helper
hrms.hr.doctype.shift_assignment.shift_assignment.get_employee_shift; remove the
duplicated filtering/permission logic and instead import that helper and call it
with the appropriate parameters (map for_date to the helper's for_timestamp
argument, and add the consider_default_shift and next_shift_direction parameters
so callers can pass them through), then return the helper's result; ensure the
wrapper's signature includes the extra params (consider_default_shift=False,
next_shift_direction=None) and convert for_date to the expected timestamp type
if needed before calling the helper.

---

Nitpick comments:
In `@hrms/hr/doctype/attendance_request/attendance_request.js`:
- Around line 27-37: Both employee(frm) and from_date(frm) duplicate the same
guard and can simply trigger set_employee_shift; update the two handlers
(employee and from_date) to call frm.trigger("set_employee_shift") directly,
then move the !frm.doc.shift check into the start of the set_employee_shift
handler so that set_employee_shift re-checks employee, from_date, and
!frm.doc.shift before making any server calls.

In `@hrms/hr/doctype/attendance/attendance.py`:
- Line 478: Remove the redundant guard in the permission check by dropping the
`employee and` conjunct so the condition becomes just `if not
frappe.has_permission("Employee", "read", employee):`; this keeps the earlier
falsy-employee early return intact and preserves the intended permission check
in the attendance.py code line that currently reads `if employee and not
frappe.has_permission("Employee", "read", employee):`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6d910da-7a78-4bcc-8557-80b5994bf653

📥 Commits

Reviewing files that changed from the base of the PR and between d9bef67 and c0e48ea.

📒 Files selected for processing (3)
  • hrms/hr/doctype/attendance/attendance.js
  • hrms/hr/doctype/attendance/attendance.py
  • hrms/hr/doctype/attendance_request/attendance_request.js

Comment on lines +27 to +55
employee(frm) {
if (frm.doc.employee && frm.doc.from_date && !frm.doc.shift) {
frm.trigger("set_employee_shift");
}
},

from_date(frm) {
if (frm.doc.employee && frm.doc.from_date && !frm.doc.shift) {
frm.trigger("set_employee_shift");
}
},

set_employee_shift(frm) {
if (!frm.doc.employee || !frm.doc.from_date) return;

frappe.call({
method: "hrms.hr.doctype.attendance.attendance.get_employee_shift",
args: {
employee: frm.doc.employee,
for_date: frm.doc.from_date,
consider_default_shift: true,
},
callback(r) {
if (r.message && !frm.doc.shift) {
frm.set_value("shift", r.message);
}
},
});
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm there is no existing whitelisted method on the backend accepting consider_default_shift
rg -nP --type=py -C2 '\bdef\s+get_employee_shift\s*\('

Repository: frappe/hrms

Length of output: 792


🏁 Script executed:

#!/bin/bash
# Get full signature and implementation of get_employee_shift in attendance.py
sed -n '474,510p' hrms/hr/doctype/attendance/attendance.py

Repository: frappe/hrms

Length of output: 720


🏁 Script executed:

#!/bin/bash
# Get full signature and implementation of get_employee_shift in shift_assignment.py
sed -n '455,490p' hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 1439


🏁 Script executed:

#!/bin/bash
# Check the actual JavaScript code in attendance_request.js
cat -n hrms/hr/doctype/attendance_request/attendance_request.js | head -70

Repository: frappe/hrms

Length of output: 1790


🏁 Script executed:

#!/bin/bash
# Search for any references to consider_default_shift in the codebase
rg "consider_default_shift" --type=py --type=js

Repository: frappe/hrms

Length of output: 2239


🏁 Script executed:

#!/bin/bash
# Search for default_shift handling in attendance-related files
rg "default_shift" hrms/hr/doctype/attendance/ hrms/hr/doctype/attendance_request/ --type=py --type=js

Repository: frappe/hrms

Length of output: 353


Call the correct backend endpoint for shift auto-population with default shift fallback support.

The JavaScript code passes consider_default_shift: true to hrms.hr.doctype.attendance.attendance.get_employee_shift, but the backend method signature only accepts employee and for_date. The consider_default_shift parameter will be silently dropped by Frappe, causing employees with only a default_shift (no Shift Assignment) to not get the shift auto-populated.

The correct implementation exists in hrms.hr.doctype.shift_assignment.shift_assignment.get_employee_shift, which accepts consider_default_shift and properly consults Employee.default_shift as a fallback. Switch to calling that endpoint instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hrms/hr/doctype/attendance_request/attendance_request.js` around lines 27 -
55, The JS shift auto-population currently calls the backend method
"hrms.hr.doctype.attendance.attendance.get_employee_shift" from
set_employee_shift (and triggered by employee and from_date handlers), but that
backend only accepts employee and for_date and drops consider_default_shift;
update the frappe.call in set_employee_shift to call
"hrms.hr.doctype.shift_assignment.shift_assignment.get_employee_shift" instead,
keeping the same args (employee, for_date from frm.doc.from_date,
consider_default_shift: true) and the same callback logic that sets
frm.set_value("shift", r.message) when r.message exists and frm.doc.shift is
empty.

method: "hrms.hr.doctype.attendance.attendance.get_employee_shift",
args: {
employee: frm.doc.employee,
for_date: frm.doc.attendance_date || frappe.datetime.get_today(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Dead fallback — frappe.datetime.get_today() is unreachable.

Line 44 already returns early when attendance_date is falsy, so || frappe.datetime.get_today() can never trigger here. Drop it to avoid misleading readers; the backend already defaults to nowdate() when for_date is missing.

🧹 Suggested cleanup
-				for_date: frm.doc.attendance_date || frappe.datetime.get_today(),
+				for_date: frm.doc.attendance_date,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for_date: frm.doc.attendance_date || frappe.datetime.get_today(),
for_date: frm.doc.attendance_date,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hrms/hr/doctype/attendance/attendance.js` at line 50, The object property
for_date currently uses an unreachable fallback ("for_date:
frm.doc.attendance_date || frappe.datetime.get_today()"); remove the dead
fallback so it reads "for_date: frm.doc.attendance_date" (reference:
attendance.js, frm.doc.attendance_date, frappe.datetime.get_today(), for_date) —
the backend already defaults to nowdate(), so dropping the "||
frappe.datetime.get_today()" avoids misleading readers.

Comment on lines +473 to +505
@frappe.whitelist()
def get_employee_shift(employee: str, for_date: str | date | None = None) -> str | None:
if not employee:
return None

if employee and not frappe.has_permission("Employee", "read", employee):
return None

if not for_date:
for_date = nowdate()

for_date = getdate(for_date)

if not frappe.has_permission("Shift Assignment", "read"):
return None

shifts = frappe.get_all(
"Shift Assignment",
filters={
"employee": employee,
"docstatus": 1,
"status": "Active",
"start_date": ("<=", for_date),
},
fields=["shift_type", "start_date"],
order_by="start_date desc",
limit=1,
)

if shifts:
return shifts[0].shift_type

return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the existing helper's signature and behavior (default_shift + end_date handling)
ast-grep --pattern $'def get_employee_shift($$$):
  $$$'

Repository: frappe/hrms

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Find the shift_assignment.py file
fd -type f -name "shift_assignment.py" | head -20

Repository: frappe/hrms

Length of output: 226


🏁 Script executed:

#!/bin/bash
# Find shift_assignment.py file
find . -name "shift_assignment.py" -type f

Repository: frappe/hrms

Length of output: 110


🏁 Script executed:

#!/bin/bash
# First, check file size
wc -l ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Search for the get_employee_shift function definition
rg -n "def get_employee_shift" ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 119


🏁 Script executed:

#!/bin/bash
# Read the get_employee_shift function (lines 455-540)
sed -n '455,540p' ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 3072


🏁 Script executed:

#!/bin/bash
# Find get_shift_for_timestamp and get_shift_details functions
rg -n "def get_shift_for_timestamp|def get_shift_details" ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Read get_shift_for_timestamp (lines 448-454)
sed -n '448,454p' ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 263


🏁 Script executed:

#!/bin/bash
# Find get_shifts_for_date function
rg -n "def get_shifts_for_date" ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Read get_shifts_for_date (lines 406-447)
sed -n '406,447p' ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 1427


🏁 Script executed:

#!/bin/bash
# Find get_shift_for_time function
rg -n "def get_shift_for_time" ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 217


🏁 Script executed:

#!/bin/bash
# Read get_shift_for_time (lines 301-405)
sed -n '301,405p' ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 3890


🏁 Script executed:

#!/bin/bash
# Read get_shift_details (lines 624-700)
sed -n '624,700p' ./hrms/hr/doctype/shift_assignment/shift_assignment.py

Repository: frappe/hrms

Length of output: 2760


🏁 Script executed:

#!/bin/bash
# Search for usages of get_employee_shift from shift_assignment
rg "get_employee_shift" --type py -B 2 -A 2 | head -80

Repository: frappe/hrms

Length of output: 5565


🏁 Script executed:

#!/bin/bash
# Check how the attendance.py function is called to understand parameter expectations
rg "get_employee_shift" ./hrms/hr/doctype/attendance/ -B 3 -A 3

Repository: frappe/hrms

Length of output: 1066


🏁 Script executed:

#!/bin/bash
# Read the full JS call to see if consider_default_shift is being passed
sed -n '1,600p' ./hrms/hr/doctype/attendance/attendance.js | grep -A 10 "get_employee_shift"

Repository: frappe/hrms

Length of output: 387


🏁 Script executed:

#!/bin/bash
# Check imports in attendance.py
head -50 ./hrms/hr/doctype/attendance/attendance.py | grep -E "^from|^import"

Repository: frappe/hrms

Length of output: 481


Replace this partial reimplementation with a thin wrapper around the existing shift_assignment.get_employee_shift() helper.

The existing helper at hrms.hr.doctype.shift_assignment.shift_assignment.get_employee_shift(employee, for_timestamp=None, consider_default_shift=False, next_shift_direction=None) is the canonical implementation. The current attendance.py version duplicates partial logic and critically:

  • Silently drops consider_default_shift, which the JS caller passes as true, breaking expected behavior
  • Omits end_date filtering, returning expired shift assignments
  • Lacks next_shift_direction support

Replace with a wrapper that properly delegates to the existing helper:

♻️ Suggested wrapper
+from datetime import datetime
+from hrms.hr.doctype.shift_assignment.shift_assignment import (
+	get_employee_shift as _get_employee_shift,
+)
+
 `@frappe.whitelist`()
-def get_employee_shift(employee: str, for_date: str | date | None = None) -> str | None:
-	if not employee:
-		return None
-
-	if employee and not frappe.has_permission("Employee", "read", employee):
-		return None
-
-	if not for_date:
-		for_date = nowdate()
-
-	for_date = getdate(for_date)
-
-	if not frappe.has_permission("Shift Assignment", "read"):
-		return None
-
-	shifts = frappe.get_all(
-		"Shift Assignment",
-		filters={
-			"employee": employee,
-			"docstatus": 1,
-			"status": "Active",
-			"start_date": ("<=", for_date),
-		},
-		fields=["shift_type", "start_date"],
-		order_by="start_date desc",
-		limit=1,
-	)
-
-	if shifts:
-		return shifts[0].shift_type
-
-	return None
+def get_employee_shift(
+	employee: str,
+	for_date: str | date | None = None,
+	consider_default_shift: bool = False,
+) -> str | None:
+	if not employee or not frappe.has_permission("Employee", "read", employee):
+		return None
+	if not frappe.has_permission("Shift Assignment", "read"):
+		return None
+	
+	for_timestamp = datetime.combine(
+		getdate(for_date) if for_date else getdate(nowdate()),
+		datetime.min.time()
+	)
+	shift_details = _get_employee_shift(
+		employee,
+		for_timestamp,
+		consider_default_shift=cint(consider_default_shift),
+	)
+	return shift_details.shift_type.name if shift_details else None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hrms/hr/doctype/attendance/attendance.py` around lines 473 - 505, The current
get_employee_shift in attendance.py should be replaced with a thin wrapper that
delegates to the canonical helper
hrms.hr.doctype.shift_assignment.shift_assignment.get_employee_shift; remove the
duplicated filtering/permission logic and instead import that helper and call it
with the appropriate parameters (map for_date to the helper's for_timestamp
argument, and add the consider_default_shift and next_shift_direction parameters
so callers can pass them through), then return the helper's result; ensure the
wrapper's signature includes the extra params (consider_default_shift=False,
next_shift_direction=None) and convert for_date to the expected timestamp type
if needed before calling the helper.

Comment on lines +489 to +500
shifts = frappe.get_all(
"Shift Assignment",
filters={
"employee": employee,
"docstatus": 1,
"status": "Active",
"start_date": ("<=", for_date),
},
fields=["shift_type", "start_date"],
order_by="start_date desc",
limit=1,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing end_date filter — may return an expired Shift Assignment.

Shift Assignment has an optional end_date (per the doctype schema) and a status of Active is kept even after the assignment ends (the status field is allow_on_submit and isn't automatically flipped when end_date passes). With only start_date <= for_date, this query can pick an assignment whose end_date is before for_date, populating an expired shift on the form. Add an end-date condition:

🛡️ Proposed fix (if keeping the local implementation)
 	shifts = frappe.get_all(
 		"Shift Assignment",
 		filters={
 			"employee": employee,
 			"docstatus": 1,
 			"status": "Active",
 			"start_date": ("<=", for_date),
 		},
+		or_filters=[
+			["end_date", "is", "not set"],
+			["end_date", ">=", for_date],
+		],
 		fields=["shift_type", "start_date"],
 		order_by="start_date desc",
 		limit=1,
 	)

Note: this becomes moot if you adopt the refactor in the sibling comment (the existing shift_assignment.get_employee_shift already handles end_date correctly).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hrms/hr/doctype/attendance/attendance.py` around lines 489 - 500, The query
fetching Shift Assignment via frappe.get_all (assigning to shifts) can return
expired records because it only filters on "start_date" <= for_date; update the
filters for the "Shift Assignment" query to also require that "end_date" is
either null/empty or >= for_date (i.e., add an end_date condition using ("is",
"NULL") OR ("<=", for_date) appropriately), referencing the existing variables
employee and for_date; alternatively, replace this local lookup with the
existing shift_assignment.get_employee_shift helper which already enforces
end_date correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fetch Shift from Shift Assignment Instead of Displaying All Shifts in Attendance and Attendance Request

1 participant