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:
@@ -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.
|
||||
Reference in New Issue
Block a user