A proper shared-device PIN kiosk for clients who don't want NFC: photo-tile grid (+search) -> tap -> PIN (or first-use create) -> optional master-gated selfie -> clock, in the NFC kiosk's dark glass + brand-gradient style. Built as an Odoo 19 Interaction; new pin_kiosk.scss (scoped); reworked clock_kiosk.py (search +avatar/has_pin, verify_pin needs_setup, set_pin, clock via kiosk location). Drops the redundant kiosk_pin_required (PIN always required); relabels the company kiosk location; adds a PIN-kiosk app icon. Opt-in via enable_kiosk (off by default). HttpCase tests added. Bump 19.0.4.0.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
364 lines
16 KiB
Markdown
364 lines
16 KiB
Markdown
# Fusion Clock - Claude Code Instructions
|
||
|
||
> Read together with the repo-root `../CLAUDE.md` for global Odoo 19 rules, asset-cache handling, Supabase KB notes, and shared Fusion conventions. This file is only for the `fusion_clock` module.
|
||
|
||
## 1. What This Module Is
|
||
|
||
- **Name**: Fusion Clock.
|
||
- **Version**: `19.0.3.3.0`.
|
||
- **Category**: Human Resources/Attendances.
|
||
- **License**: OPL-1, Nexa Systems Inc.
|
||
- **Purpose**: complete time and attendance app built on Odoo `hr.attendance`.
|
||
- **Top-level menu**: `Fusion Clock`.
|
||
- **Main surfaces**:
|
||
- Portal clock page at `/my/clock`.
|
||
- Portal timesheets at `/my/clock/timesheets`.
|
||
- Portal reports at `/my/clock/reports`.
|
||
- Shared PIN kiosk at `/fusion_clock/kiosk`.
|
||
- NFC tap kiosk at `/fusion_clock/kiosk/nfc`.
|
||
- Backend systray clock widget.
|
||
- Backend manager/team-lead dashboard client action.
|
||
|
||
Core behaviours: geofenced clock-in/out, IP whitelist fallback, shift scheduling, break deduction, penalties, overtime, auto clock-out, absence detection, leave requests, correction workflow, payroll CSV export, PDF reports, weekly summaries, shared kiosk, NFC kiosk with photo capture, and activity audit logs.
|
||
|
||
## 2. Dependencies
|
||
|
||
Declared in `__manifest__.py`:
|
||
|
||
```
|
||
hr_attendance, hr, portal, mail, resource
|
||
```
|
||
|
||
External Python used directly:
|
||
|
||
- `pytz` for timezone-safe local day boundaries.
|
||
- `requests` for Google Geocoding, OpenStreetMap/Nominatim fallback, and IP metadata.
|
||
- `dateutil.relativedelta` inside pay-period calculations.
|
||
|
||
External browser APIs:
|
||
|
||
- Browser geolocation.
|
||
- `ipapi.co` fallback geolocation in frontend/backend clock widgets.
|
||
- Google Maps/Places when `fusion_clock.google_maps_api_key` is configured.
|
||
- Web NFC and camera APIs for the NFC kiosk.
|
||
|
||
## 3. Naming And Field Prefixes
|
||
|
||
This module uses the module-specific prefix **`x_fclk_*`** on inherited Odoo models, not `x_fc_*`.
|
||
|
||
Examples:
|
||
|
||
- `hr.employee.x_fclk_enable_clock`
|
||
- `hr.employee.x_fclk_nfc_card_uid`
|
||
- `hr.attendance.x_fclk_clock_source`
|
||
- `res.company.x_fclk_nfc_kiosk_location_id`
|
||
|
||
New inherited fields in this module should keep the `x_fclk_*` prefix unless there is a strong migration reason not to.
|
||
|
||
## 4. Model Map
|
||
|
||
Custom models:
|
||
|
||
| Model | File | Purpose |
|
||
|---|---|---|
|
||
| `fusion.clock.location` | `models/clock_location.py` | Geofenced/IP-whitelisted clock locations. |
|
||
| `fusion.clock.shift` | `models/clock_shift.py` | Shift start/end/break schedule assigned to employees. |
|
||
| `fusion.clock.penalty` | `models/clock_penalty.py` | Late clock-in / early clock-out penalty records. |
|
||
| `fusion.clock.activity.log` | `models/clock_activity_log.py` | Append-style audit log for clock activity, geofence misses, absences, NFC enrolment, corrections. |
|
||
| `fusion.clock.leave.request` | `models/clock_leave_request.py` | Portal leave requests, auto-approved but office-notified. |
|
||
| `fusion.clock.correction` | `models/clock_correction.py` | Timesheet correction requests with approve/reject workflow. |
|
||
| `fusion.clock.report` | `models/clock_report.py` | Employee or batch pay-period report with PDF/CSV export and email send. |
|
||
| `fusion.clock.nfc.enrollment.wizard` | `wizard/clock_nfc_enrollment_wizard.py` | Backend NFC card enrolment/reassignment wizard. |
|
||
|
||
Inherited models:
|
||
|
||
- `hr.employee`: enable clock, default location, shift, kiosk PIN, NFC UID, pending reason flag, streaks, absence/overtime counters, and One2many links.
|
||
- `hr.attendance`: clock source, location, distances, photos, break minutes, net hours, penalties, auto clock-out flag, overtime fields.
|
||
- `res.config.settings`: all `fusion_clock.*` settings.
|
||
- `res.company`: NFC kiosk location binding.
|
||
|
||
Timezone helpers live in `models/tz_utils.py`. Use `get_local_today()` and `get_local_day_boundaries()` for attendance domains instead of comparing UTC dates directly.
|
||
|
||
## 5. Clocking Flow
|
||
|
||
Primary API endpoint: `/fusion_clock/clock_action` in `controllers/clock_api.py`.
|
||
|
||
Clock-in flow:
|
||
|
||
1. Resolve current user to `hr.employee`.
|
||
2. Block if `x_fclk_enable_clock` is false.
|
||
3. If `x_fclk_pending_reason` is true, return `requires_reason`.
|
||
4. Verify location against allowed active `fusion.clock.location` records.
|
||
5. Call Odoo's `_attendance_action_change()`.
|
||
6. Write location, distance, source, and optional photo to `hr.attendance`.
|
||
7. Log `clock_in`.
|
||
8. Create `late_in` penalty when outside grace.
|
||
9. Increment/reset on-time streak; log milestone at 5, 10, 20, 50, 100.
|
||
10. Notify office user for very-late clock-ins.
|
||
|
||
Clock-out flow:
|
||
|
||
1. Verify location again.
|
||
2. Call `_attendance_action_change()`.
|
||
3. Write out-distance.
|
||
4. Apply break deduction when configured.
|
||
5. Create `early_out` penalty when outside grace.
|
||
6. Log `clock_out`.
|
||
7. Log overtime if computed overtime is positive.
|
||
|
||
Location verification uses GPS when coordinates are available and geocoded locations exist. IP whitelist matching is attempted when a client IP is available. Error types include `no_locations`, `gps_unavailable`, `no_geocoded`, and `outside`.
|
||
|
||
## 6. Kiosk And NFC
|
||
|
||
PIN kiosk (opt-in alternative to NFC; v19.0.4.0.0+):
|
||
|
||
- Page: `/fusion_clock/kiosk` — polished photo-tile → PIN flow (logo, brand-hue
|
||
gradient, live clock), matching the NFC kiosk style; built as an Odoo 19
|
||
Interaction (`#pin_kiosk_root`, `static/src/js/fusion_clock_kiosk.js`,
|
||
`static/src/scss/pin_kiosk.scss`, brand-hue var `--pk-h`).
|
||
- JSON routes:
|
||
- `/fusion_clock/kiosk/search` (grid rows: +`avatar_url`, +`has_pin`; also used by the NFC kiosk's employee_search — keep additive)
|
||
- `/fusion_clock/kiosk/verify_pin` (returns `needs_setup` when the employee has no PIN)
|
||
- `/fusion_clock/kiosk/set_pin` (first-use PIN creation, 4–6 digits)
|
||
- `/fusion_clock/kiosk/clock` (uses the company kiosk location, no GPS geofence; optional master-gated selfie)
|
||
- Requires `group_fusion_clock_manager` or `group_fusion_clock_kiosk_app`; has its own app icon.
|
||
- Opt-in via `fusion_clock.enable_kiosk`. PIN is ALWAYS required (the old
|
||
`kiosk_pin_required` setting was removed). Selfie capture is gated by the
|
||
master `fusion_clock.enable_photo_verification`. Kiosk location =
|
||
`res.company.x_fclk_nfc_kiosk_location_id` (shared with the NFC kiosk).
|
||
- Uses `hr.employee.x_fclk_kiosk_pin` (manager-editable; created on first tap otherwise).
|
||
|
||
NFC kiosk:
|
||
|
||
- Page: `/fusion_clock/kiosk/nfc`
|
||
- JSON routes:
|
||
- `/fusion_clock/kiosk/nfc/enroll`
|
||
- `/fusion_clock/kiosk/nfc/tap`
|
||
- `/fusion_clock/kiosk/nfc/employee_search`
|
||
- Requires `fusion_clock.group_fusion_clock_manager`.
|
||
- Controlled by:
|
||
- `fusion_clock.enable_nfc_kiosk`
|
||
- `fusion_clock.nfc_photo_required`
|
||
- `fusion_clock.nfc_enroll_password`
|
||
- `fusion_clock.nfc_kiosk_debug`
|
||
- `res.company.x_fclk_nfc_kiosk_location_id`
|
||
- Card UID canonical format is uppercase colon-separated hex, e.g. `04:A2:B5:62:C1:80`.
|
||
- Normalization lives in `FusionClockNfcKiosk._normalize_uid()` and is reused by the backend wizard.
|
||
- Tap debounce is module-level memory in `controllers/clock_nfc_kiosk.py`: same UID within 5 seconds returns `debounce`.
|
||
- Photo data URLs are stripped before writing binary fields.
|
||
- NFC clock-ins write `x_fclk_check_in_photo`; NFC clock-outs write `x_fclk_check_out_photo`.
|
||
|
||
Important: unknown-card taps currently return `card_unknown`; the `unknown_card_tap` log type exists but is not written by the endpoint.
|
||
|
||
## 7. Reports And Payroll Export
|
||
|
||
`fusion.clock.report` supports:
|
||
|
||
- Employee reports when `employee_id` is set.
|
||
- Batch reports when `employee_id` is empty.
|
||
- PDF generation through QWeb reports:
|
||
- `fusion_clock.action_report_clock_employee`
|
||
- `fusion_clock.action_report_clock_batch`
|
||
- CSV export via `action_export_csv()`.
|
||
- Custom CSV headings via JSON in `fusion_clock.csv_column_mapping`.
|
||
- Email send with generated PDF attached.
|
||
|
||
Pay period types:
|
||
|
||
```
|
||
weekly, biweekly, semi_monthly, monthly
|
||
```
|
||
|
||
The anchor date setting is `fusion_clock.pay_period_start` as a string in `YYYY-MM-DD` format.
|
||
|
||
Historical report generation is exposed through the `Generate Historical Reports` menu action and creates draft reports for completed attendance periods. The scheduled report cron only generates when yesterday is the period end.
|
||
|
||
## 8. Scheduled Automation
|
||
|
||
Configured in `data/ir_cron_data.xml`:
|
||
|
||
| Cron | Model method | Frequency |
|
||
|---|---|---|
|
||
| Fusion Clock: Auto Clock-Out | `hr.attendance._cron_fusion_auto_clock_out()` | Every 15 minutes |
|
||
| Fusion Clock: Generate Period Reports | `fusion.clock.report._cron_generate_period_reports()` | Daily |
|
||
| Fusion Clock: Daily Absence Check | `hr.attendance._cron_fusion_check_absences()` | Daily |
|
||
| Fusion Clock: Employee Reminders | `hr.attendance._cron_fusion_employee_reminders()` | Every 15 minutes |
|
||
| Fusion Clock: Weekly Summary | `hr.attendance._cron_fusion_weekly_summary()` | Daily, internally sends Mondays |
|
||
|
||
Auto clock-out closes open attendances after scheduled end plus grace, capped by max shift hours. It sets `x_fclk_pending_reason` so the employee must explain before clocking in again.
|
||
|
||
Absence detection checks enabled employees, skips weekends and global resource calendar leaves, and logs `absent` when no attendance or leave request exists.
|
||
|
||
## 9. Security
|
||
|
||
Groups:
|
||
|
||
- `group_fusion_clock_user`
|
||
- `group_fusion_clock_team_lead`
|
||
- `group_fusion_clock_manager`
|
||
|
||
Admin is auto-assigned to manager in `security/security.xml`.
|
||
|
||
Access pattern:
|
||
|
||
- Users and portal users can read their own clock data.
|
||
- Team leads can read direct reports for penalties, activity logs, corrections, and dashboard data.
|
||
- Managers have full model access and all configuration/kiosk/report menus.
|
||
- Portal rules are defined for `hr.attendance`, `fusion.clock.location`, `fusion.clock.report`, `fusion.clock.penalty`, `fusion.clock.activity.log`, `fusion.clock.leave.request`, `fusion.clock.correction`, and `fusion.clock.shift`.
|
||
|
||
Backend dashboard access is checked in `/fusion_clock/dashboard_data`: manager sees all enabled employees; team lead sees employees where `parent_id` is the current user's employee.
|
||
|
||
## 10. Frontend Assets
|
||
|
||
Frontend bundle:
|
||
|
||
- `static/src/css/portal_clock.css`
|
||
- `static/src/scss/nfc_kiosk.scss`
|
||
- `static/src/js/fusion_clock_portal.js`
|
||
- `static/src/js/fusion_clock_kiosk.js`
|
||
- `static/src/js/fusion_clock_nfc_kiosk.js`
|
||
|
||
Backend bundle:
|
||
|
||
- `static/src/scss/fusion_clock.scss`
|
||
- `static/src/js/fusion_clock_systray.js`
|
||
- `static/src/xml/systray_clock.xml`
|
||
- `static/src/js/fusion_clock_dashboard.js`
|
||
- `static/src/xml/fusion_clock_dashboard.xml`
|
||
- `static/src/js/fusion_clock_location_map.js`
|
||
- `static/src/js/fusion_clock_location_places.js`
|
||
- `static/src/xml/fusion_clock_location.xml`
|
||
|
||
Patterns:
|
||
|
||
- Public portal/kiosk JS should use `Interaction` from `@web/public/interaction` and register in `registry.category("public.interactions")`.
|
||
- Backend OWL client actions and field widgets use standalone `rpc()` from `@web/core/network/rpc`.
|
||
- `fusion_clock_systray.js` is a systray OWL component registered as `fusion_clock.ClockSystray`.
|
||
- `fusion_clock_dashboard.js` is a client action registered as `fusion_clock.Dashboard`.
|
||
- Location widgets are registered field widgets: `fclk_location_map` and `fclk_places_autocomplete`.
|
||
|
||
Known technical debt:
|
||
|
||
- `static/src/js/fusion_clock_nfc_kiosk.js` is currently an isolated IIFE. If touching it, prefer migrating to an Odoo 19 `Interaction` instead of expanding the IIFE pattern.
|
||
- `static/src/css/portal_clock.css` and `static/src/scss/fusion_clock.scss` contain runtime dark-mode selectors/media rules. For backend SCSS changes, follow the repo-root Odoo 19 compile-time dark bundle guidance.
|
||
- `fusion_clock.scss` uses some Bootstrap CSS vars for status accents. Avoid relying on Bootstrap vars for card/background/border surfaces in new dashboard work.
|
||
|
||
## 11. Settings Keys
|
||
|
||
Important `ir.config_parameter` keys:
|
||
|
||
```
|
||
fusion_clock.default_clock_in_time
|
||
fusion_clock.default_clock_out_time
|
||
fusion_clock.default_break_minutes
|
||
fusion_clock.auto_deduct_break
|
||
fusion_clock.break_threshold_hours
|
||
fusion_clock.enable_auto_clockout
|
||
fusion_clock.max_shift_hours
|
||
fusion_clock.enable_penalties
|
||
fusion_clock.penalty_grace_minutes
|
||
fusion_clock.penalty_deduction_minutes
|
||
fusion_clock.enable_overtime
|
||
fusion_clock.daily_overtime_threshold
|
||
fusion_clock.office_user_id
|
||
fusion_clock.very_late_threshold_minutes
|
||
fusion_clock.max_monthly_absences
|
||
fusion_clock.enable_employee_notifications
|
||
fusion_clock.reminder_before_shift_minutes
|
||
fusion_clock.reminder_before_end_minutes
|
||
fusion_clock.send_weekly_summary
|
||
fusion_clock.enable_ip_fallback
|
||
fusion_clock.enable_photo_verification
|
||
fusion_clock.google_maps_api_key
|
||
fusion_clock.enable_kiosk
|
||
fusion_clock.enable_correction_requests
|
||
fusion_clock.enable_sounds
|
||
fusion_clock.pay_period_type
|
||
fusion_clock.pay_period_start
|
||
fusion_clock.auto_generate_reports
|
||
fusion_clock.send_employee_reports
|
||
fusion_clock.report_recipient_user_ids
|
||
fusion_clock.report_recipient_emails
|
||
fusion_clock.csv_column_mapping
|
||
fusion_clock.enable_nfc_kiosk
|
||
fusion_clock.nfc_photo_required
|
||
fusion_clock.nfc_enroll_password
|
||
fusion_clock.nfc_kiosk_debug
|
||
```
|
||
|
||
`fclk_report_recipient_user_ids` is a Many2many on settings but is persisted manually as comma-separated user IDs in `fusion_clock.report_recipient_user_ids`.
|
||
|
||
## 12. Routes
|
||
|
||
HTTP pages:
|
||
|
||
```
|
||
/my/clock
|
||
/my/clock/timesheets
|
||
/my/clock/reports
|
||
/my/clock/reports/<report_id>/download
|
||
/fusion_clock/kiosk
|
||
/fusion_clock/kiosk/nfc
|
||
```
|
||
|
||
JSON-RPC endpoints:
|
||
|
||
```
|
||
/fusion_clock/verify_location
|
||
/fusion_clock/clock_action
|
||
/fusion_clock/submit_reason
|
||
/fusion_clock/request_leave
|
||
/fusion_clock/request_correction
|
||
/fusion_clock/get_status
|
||
/fusion_clock/get_locations
|
||
/fusion_clock/get_settings
|
||
/fusion_clock/dashboard_data
|
||
/fusion_clock/kiosk/search
|
||
/fusion_clock/kiosk/verify_pin
|
||
/fusion_clock/kiosk/clock
|
||
/fusion_clock/kiosk/nfc/enroll
|
||
/fusion_clock/kiosk/nfc/tap
|
||
/fusion_clock/kiosk/nfc/employee_search
|
||
```
|
||
|
||
All new JSON endpoints must use `type="jsonrpc"`, not deprecated `type="json"`.
|
||
|
||
## 13. Gotchas
|
||
|
||
- Always use local-day helpers for date domains. UTC midnight boundaries will break attendance totals around timezone offsets.
|
||
- `hr.employee._get_fclk_scheduled_times(date)` returns naive UTC datetimes suitable for Odoo comparisons.
|
||
- Break deduction is stored as minutes in `hr.attendance.x_fclk_break_minutes`; penalties add to that same field.
|
||
- `x_fclk_net_hours` is computed from Odoo `worked_hours` minus break minutes.
|
||
- Daily overtime compares net hours to the employee's scheduled hours or the daily threshold. (The old `weekly_overtime_threshold` and `grace_period_minutes` settings were removed 2026-05-31 — they were defined/shown but never consumed.)
|
||
- `fusion_clock.enable_ip_fallback` is honoured: `_verify_location()` only attempts IP-whitelist matching when the toggle is on (default on).
|
||
- **All fusion_clock Boolean settings are persisted explicitly** (`'True'`/`'False'`) via the `_FCLK_BOOL_PARAMS` loop in `res.config.settings.get_values/set_values`, NOT via `config_parameter=`. Reason: a `config_parameter` Boolean can't be turned OFF (Odoo deletes the param row on a falsy value, so `get_param` returns the default and the feature stays on). When adding a new Boolean setting, add it to `_FCLK_BOOL_PARAMS` with its default; don't use `config_parameter=`.
|
||
- NFC kiosk needs a company-level `x_fclk_nfc_kiosk_location_id`; without it taps return `no_location_configured`.
|
||
- Kiosk routes are authenticated (`auth='user'`) and manager-gated; wall tablets need a manager-authorised kiosk user.
|
||
- Portal report download manually streams the PDF binary rather than using `fusion_pdf_preview`.
|
||
- If CSS/assets change, bump `__manifest__.py` version so Odoo rebuilds bundles.
|
||
|
||
## 14. Tests
|
||
|
||
Tests are post-install tagged:
|
||
|
||
```
|
||
@tagged('-at_install', 'post_install', 'fusion_clock')
|
||
```
|
||
|
||
Coverage currently focuses on NFC:
|
||
|
||
- `tests/test_nfc_models.py`: employee UID uniqueness, attendance NFC source/photo fields, company kiosk location field.
|
||
- `tests/test_clock_nfc_kiosk.py`: kiosk page gating, UID normalization, enroll endpoint, tap happy path, tap errors, photo-required handling, employee search.
|
||
|
||
Run locally:
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_clock --test-tags fusion_clock --stop-after-init
|
||
```
|
||
|
||
For a normal module upgrade:
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_clock --stop-after-init
|
||
```
|