docs(fusion_clock): bi-weekly attendance filter spec (reuse pay-period config, filters + picker)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-31 11:05:02 -04:00
parent a858693d9c
commit 06346cfa6b

View File

@@ -0,0 +1,105 @@
# Fusion Clock — Bi-Weekly Attendance Filter Design
**Date:** 2026-05-31
**Module:** `fusion_clock`
**Status:** Approved (brainstorming) — ready for implementation plan
---
## 1. Problem
Operators reviewing **All Attendances** have no quick way to scope the list to a pay period. In Canada most payroll runs **bi-weekly**, so the common need is "show me this two-week pay period's attendances" (and step to the previous/next, or jump to an arbitrary period). The module already computes bi-weekly windows for its reports and already has the configuration — but none of it is exposed as a filter on the attendance list.
## 2. Existing state (reused, not rebuilt)
- **Period math already exists:** `fusion.clock.report._calculate_current_period(frequency, anchor_str, reference_date) → (start, end)` (`models/clock_report.py:457`). Handles `weekly` (7d), `biweekly` (14d), `semi_monthly`, `monthly`; uses floor division so dates *before* the anchor resolve correctly; anchor defaults to first-of-month when unset.
- **Setting already exists:** Settings → Fusion Clock → Pay Period: **Frequency** (`fusion_clock.pay_period_type`, default `biweekly`) + **Anchor Date** (`fusion_clock.pay_period_start`, `YYYY-MM-DD`). Decision (brainstorming): **reuse this as the single source of truth** — no new setting.
- **Attendance search view already inherited:** `view_hr_attendance_search_fusion_clock` (`views/hr_attendance_views.xml`) inherits `hr_attendance.hr_attendance_view_filter` and already adds custom filters — the natural home for the new period filters.
- **TZ helpers:** `get_local_day_boundaries(env, date[, employee])` and `get_local_today(env)` in `models/tz_utils.py`.
Decisions from brainstorming: reuse the Pay Period setting; provide **both** quick filters and a picker; the window **follows the Frequency setting** (one pay period; 2 weeks by default).
## 3. Design
### A. Shared period math (DRY)
Extract the body of `_calculate_current_period` into a reusable, model-free helper so reports, filters, and the wizard share one implementation and never drift.
`models/pay_period.py` (new):
```python
def compute_pay_period(frequency, anchor_str, reference_date) -> (date, date)
# identical logic to _calculate_current_period; pure function
def period_length_days(frequency) -> int | None
# 7 for 'weekly', 14 for 'biweekly'/default, None for calendar-based (semi_monthly/monthly)
def current_prev_next(frequency, anchor_str, today) -> dict
# {'current': (s,e), 'previous': (s,e), 'next': (s,e)} where
# previous = compute_pay_period(..., current_start - 1 day),
# next = compute_pay_period(..., current_end + 1 day) # works for ALL frequencies
```
`fusion.clock.report._calculate_current_period` becomes a thin delegator to `compute_pay_period` (no behaviour change).
### B. Quick filters on the attendance list
On `hr.attendance`, three **non-stored computed Boolean** fields, each with a `search` method (the compute returns `False` — display only; the search method does the work):
- `x_fclk_in_current_period`, `x_fclk_in_previous_period`, `x_fclk_in_next_period`
Each `_search_*(operator, value)`:
1. Read `pay_period_type` + `pay_period_start` via `ICP.sudo().get_param`.
2. Compute the window with `current_prev_next(...)` keyed on `get_local_today(env)`.
3. Convert the date window to UTC bounds with `get_local_day_boundaries` (start → 00:00 local, end → next-day 00:00 / inclusive end-of-day).
4. Return `['&', ('check_in', '>=', start_utc), ('check_in', '<', end_excl_utc)]`.
Add three filters to `view_hr_attendance_search_fusion_clock`:
```xml
<filter name="fclk_period_current" string="Current Pay Period" domain="[('x_fclk_in_current_period','=',True)]"/>
<filter name="fclk_period_previous" string="Previous Pay Period" domain="[('x_fclk_in_previous_period','=',True)]"/>
<filter name="fclk_period_next" string="Next Pay Period" domain="[('x_fclk_in_next_period','=',True)]"/>
```
### C. "Pick Pay Period" wizard
`wizard/clock_period_picker_wizard.py` (new) — transient `fusion.clock.period.picker`:
- `date_start` (Date, required) — default = current period start (`current_prev_next(...)['current'][0]`).
- `date_end` (Date, required) — default = current period end; **editable**.
- `@api.onchange('date_start')`: if `period_length_days(freq)` is not None → `date_end = date_start + length - 1`; else (calendar frequencies) → `date_end = compute_pay_period(freq, anchor, date_start)[1]`. (User can still override `date_end` → covers "set both, or just the start and auto-calc".)
- `action_apply()` returns an `ir.actions.act_window`:
```python
return {
'type': 'ir.actions.act_window', 'name': f"Attendances · {date_start} {date_end}",
'res_model': 'hr.attendance', 'view_mode': 'list,form',
'domain': ['&', ('check_in','>=', start_utc), ('check_in','<', end_excl_utc)],
'target': 'current',
}
```
(`start_utc`/`end_excl_utc` from `get_local_day_boundaries` on `date_start` / `date_end`.)
`wizard/clock_period_picker_views.xml` (new): a small form (date_start, date_end, **Apply** + **Cancel**) and an `ir.actions.act_window` opening it as a dialog (`target="new"`).
### D. Entry points
- **Menu:** `views/clock_menus.xml` — add **Fusion Clock → Attendance → Bi-Weekly Period** (`sequence` after All Attendances), `groups="group_fusion_clock_manager,group_fusion_clock_team_lead"`, action = the picker wizard.
- **Dashboard tile:** add an **onViewBiweekly()** handler (opens the picker wizard act_window) and a "🗓 Bi-Weekly Period" tile in the dashboard Quick Actions, inside the existing `t-if="state.team"` block (so only leads/managers see it).
### E. Settings
No new setting. Clarify the Anchor Date help/label in `res_config_settings_views.xml` to note it is the bi-weekly week-start used by **both** the reports and the attendance period filter.
## 4. Permissions
Filters, menu, and dashboard tile are gated to **manager + team-lead** (the attendance list itself is already gated to them in `clock_menus.xml`). Search methods read `ir.config_parameter` via `sudo()` (config only — no employee data). The returned domains run through Odoo's normal ACL/record rules, so a team-lead still sees only their own reports' attendance rows. No new data exposure.
## 5. Edge cases
- **No anchor set** → `compute_pay_period` falls back to first-of-month (existing behaviour); filters and picker still resolve a sane window. The picker pre-fills `date_start` with the computed current start so it is never blank.
- **Frequency = semi_monthly / monthly** → window follows it; previous/next via `current_start 1` / `current_end + 1` handles calendar stepping; picker auto-end uses the calendar period end containing the chosen start.
- **TZ / DST** → date windows convert through `get_local_day_boundaries`, so a UTC `check_in` is matched against local pay-period days; end is exclusive next-day-00:00 to include the whole last day.
- **date_end before date_start** in the picker → `@api.constrains` raises a friendly `ValidationError`.
## 6. Testing (`tests/test_pay_period.py`, `@tagged('-at_install','post_install','fusion_clock')`)
- `compute_pay_period` for weekly / biweekly / semi_monthly / monthly, including a reference date **before** the anchor (negative offset) and exact boundary days.
- `current_prev_next` returns contiguous, non-overlapping windows for biweekly (prev_end + 1 day == current_start, current_end + 1 == next_start).
- Create attendances spanning two bi-weekly periods; assert the **Current** search filter returns only current-period rows and **Previous** only previous-period rows.
- Wizard: default `date_start` == current period start; `onchange` sets `date_end = start + 13` for biweekly; `action_apply` returns an act_window whose domain bounds equal the local-day UTC boundaries of the chosen window.
## 7. Out of scope (YAGNI)
Custom OWL toolbar dropdown on the list (native Filters menu + wizard instead); per-employee differing pay periods; editing the anchor from the picker; saving favourite/named periods; touching the gantt view.
## 8. Files touched
- New: `models/pay_period.py`, `wizard/clock_period_picker_wizard.py`, `wizard/clock_period_picker_views.xml`, `tests/test_pay_period.py`
- Modify: `models/__init__.py`, `models/clock_report.py` (delegate), `models/hr_attendance.py` (3 fields + search methods), `wizard/__init__.py`, `views/hr_attendance_views.xml` (3 filters), `views/clock_menus.xml` (menu item), `views/res_config_settings_views.xml` (label text), `static/src/js/fusion_clock_dashboard.js` + `static/src/xml/fusion_clock_dashboard.xml` (tile), `__manifest__.py` (data entry for the wizard view + version bump)
## 9. Deployment
Local test on the dev container (when available), then the standard entech path: bump version, `git commit --only` the explicit paths, push **origin + gitea**, upgrade entech (`pct exec 111`, native `odoo.service`, DB `admin`, `--http-port=0 --gevent-port=0`), verify web 200 + installed version, hard-refresh.