# XENHR → Odoo HR: Full Comparison & Gap Task List

Date: 2026-07-14. Built from two parallel verified inventories:
- **Odoo side**: every `.py`/`.xml` of the 7 local modules + the handoff zip's
  `xeno_theme_slate`/`xeno_sapb1_sync` read in full (`scratchpad/odoo_inventory.md`).
- **XENHR side**: reconstructed from the prior sessions' actual survey of the
  cloned `xenhr_backend`/`xenhr_frontend` repos (~192 routes, 34 models, 30+
  React pages), live API calls, and direct HRSystem SQL probes
  (`scratchpad/xenhr_inventory.md`).

Supersedes HANDOFF.md's commit-history-guess gap table. Complements (does not
replace) `MIGRATION_PLAN.md` — this doc is the feature-by-feature diff; new
tasks found here are registered there as #34–#41.

---

## 1. Comparison matrix

Status: ✅ parity reached · 🟡 partial / decision open · ❌ gap, task created · ⬜ deliberately not migrating

| # | XENHR feature (verified) | Odoo status | Verdict |
|---|---|---|---|
| 1 | Login by employee_code + password (Sanctum) | Odoo native login (email-based); **most employees have no `res.users` account at all** | ❌ **#34** — self-service accounts + login convention |
| 2 | Email-link approve/reject with **no auth** (`approve-step-email`) | Not replicated | ⬜ deliberate — it's an unauthenticated write endpoint; Odoo's authenticated flow replaces it. Confirm with susu. |
| 3 | Admin/Employee dashboards (`/dashboard`, leave-attendance widget) | `xeno_theme_slate` app-dashboard (live on server) — profile, balances, mini calendar, announcements | 🟡 **#36** — module not in local repo; its holiday feed is a stale duplicate model |
| 4 | Employees CRUD + export/template/import + reset-password | Core `hr` + `xeno_data_io` presets + `base_import` | ✅ |
| 5 | `employee_foreigner_details` (passport/visa/work-permit + expiries) | Core `hr` has visa/permit fields (on hr.version in Odoo 19); **no data migrated, no expiry alerts** (handoff §5 flags this: "มี field, ยังไม่แจ้งเตือน") | ❌ **#37** |
| 6 | Employee taxonomy: Level / Type / Group / Department | Departments ✅ (real ones from B1). Levels/Types/Groups have no mapped Odoo equivalent (candidates: job grades, employee categories/tags) | 🟡 fold into **#17/#18** master-data reconciliation |
| 7 | `EmployeeModuleAccess`/`HrModule` per-employee feature gating | Odoo groups + record rules cover the same need natively | ✅ (different mechanism, no migration — semantics never fully read, spot-check during parallel run #15/#16) |
| 8 | Resignations (no approval workflow; archive + deactivate login) | `xeno_resignation` — exact behavioral parity incl. non-reactivating delete | ✅ |
| 9 | Leave types (6) + advance/backdate rules + base types (4 durations) | `xeno_leave`: all 6 types, timing limits w/ real values, flexible-duration pills, balance strip | ✅ |
| 10 | Leave request/approve flow, force approve/reject (`override`), `hr-update`, My Leaves, My Team Leaves, approver config | `xeno_leave`: full set incl. self-approval block, content lock, Approve/Reject terminology | ✅ |
| 11 | `leave_approval_steps` — schema implies sequential N-step chains | Odoo build deliberately scoped to single-current-approver (matches Odoo's 2-level model); **XENHR's actual step logic never read** | 🟡 **#38** — read the controller once, confirm scope-down is acceptable |
| 12 | Leave balances (index/my/allocate/import/export, manual revise) | Allocations + Used/Remaining + pivot grid + HR-revisable `xeno_used_days` | ✅ feature-side; **data** migration pending (#17 — source table still unlocated; concrete lead: grep `xenhr_backend` migrations for `leave_balances`, the routes/Export classes prove it exists) |
| 13 | Monthly attendance pivot (`sp_get_attendance_month_V3`) + Excel export | `xeno_attendance` `/odoo/attendances/bymonth` + export + unmapped-code diagnostic | ✅ (read-through, not import — accepted for now, scope decision in #17 checklist) |
| 14 | My Team Attendance (recursive `supervisor_id` walk) | `/odoo/attendances/myteam` via explicit viewer config (not org tree — tree was empty; see #17 org re-match) | ✅ v1 — revisit after org tree lands if "reports to me" semantics wanted |
| 15 | HR inline-edit of attendance (`AttendanceReportsPage`) | Override-as-new-record model + inline time edit, deployed & verified | ✅ |
| 16 | **My Attendance** (employee sees own attendance) | **Nothing** — company view is officer-gated, myteam needs viewer config; a plain employee has no view of their own attendance | ❌ **#35** |
| 17 | Per-employee attendance export (`sp_employee_attendance`, separate SP) | bymonth export has an `employee` filter param; the dedicated SP's output shape never captured | 🟡 **#40** — pull SP body, confirm parity or add export |
| 18 | GPS check-in/out + geofencing (`locations`, GpsErrorModal) | Not started | ❌ existing **#8/#9(plan)** — unchanged |
| 19 | Attendance auto-rules (`/attendance-auto-rules` + `/process`) | Not started; **rule logic never read** — blocks scoping (OT boundary risk) | ❌ existing **#12(plan #13)**, prerequisite folded into **#38** |
| 20 | Work shifts CRUD (`late_threshold_minutes`, `break_minutes`, work_days) | 2 real calendars imported; per-employee `work_shift_id` assignment **not migrated**; late threshold hard-coded in SP | 🟡 fold into **#17** (assignment = `resource_calendar_id` per employee) |
| 21 | Holidays (Public/Company, calendar view, Thai remarks, live 2026 data) | `xeno_holidays` + live-API import — done incl. remarks | ✅ (`is_recurring` flag not carried — trivial, note only) |
| 22 | Company detail (logo/letterhead) | `res.company` native | ✅ |
| 23 | Company activities (lunch-break/shift segments) | Consumed into `resource.calendar` breaks at import; no standalone feature needed | ✅ |
| 24 | Announcements (active toggle, audited) | **Only** in `xeno_theme_slate` (live, not in repo): minimal title/body/date model, no audit wiring | 🟡 **#36** + audit in **#41** |
| 25 | Email settings / SMTP / templates / approval-mail toggle | Odoo native outgoing-mail + templates; **live config state unverified** | 🟡 **#39** |
| 26 | Custom EN/TH i18n (`website_languages`, partial coverage) | Core Odoo Thai ✅ (ahead of XENHR); custom modules 0% | 🟡 existing **#22(plan #23)** |
| 27 | Audit log + table filter + admin DB introspection | `xeno_audit_log` on 8 models, survives deletion, manager-only. DB-introspection endpoints deliberately not replicated (security) | ✅ core; coverage gaps → **#41** |
| 28 | Import/export triad (employees/holidays/balances/leaves/attendance) | `base_import` + `xeno_data_io`; attendance intentionally excluded (own export) | ✅ |
| 29 | Role-aware sidebar, Manager/Employee view toggle | Odoo groups + menus | ✅ |
| 30 | Notifications routes | Odoo activities/inbox native | ✅ (verify during parallel run) |
| 31 | Mobile (React Native?) — `xenhr_application` repo **never inspected** | PWA manifest via `xeno_theme_slate` only | 🟡 existing open decision — confirm the repo's existence first (fold into #36's session or #1 stakeholder sync) |
| 32 | Schema-only stubs: OT, payroll runs, training, performance, recruitment (zero controllers/routes — confirmed dead) | Nothing | ⬜ out of scope, correctly — nothing to migrate |

**Bottom line**: of 32 feature areas, 17 are at parity, 5 are deliberate
non-migrations, and 10 carry open work — 4 of them genuinely new (#34–#37),
the rest small verifications (#38–#41) or already-tracked items.

## 2. New tasks (registered in MIGRATION_PLAN.md as #34–#41)

Ordered by cutover-criticality:

| # | Task | Est. | Why / notes |
|---|---|---|---|
| 34 | **Employee self-service accounts + login convention** | 1–2d | Cutover-blocker: XENHR users log in with employee_code; most Odoo employees have **no user account**. Decide internal-user vs portal (Community: internal users are free), generate accounts for active employees, link `employee_id`, set login convention (employee_code vs email — HRSystem emails are 85% placeholder, so **employee_code-derived logins are safer**), plan password distribution. Depends on #17's employee base (75/78 linked ✓). |
| 35 | **"My Attendance" self-view page** | 0.5–1d | Reuse `xeno_attendance`'s existing `restrict_codes` render path (same trick as My Team) restricted to the caller's own `xeno_employee_code`; menu visible to all internal users. Needs #34 (employees must have logins to benefit). |
| 36 | **Bring `xeno_theme_slate` under version control + kill the duplicate holiday model** | 0.5–1d | The live server runs a module that exists in no repo (handoff zip copy is the only artifact — and its sibling `xeno_sapb1_sync` copy in that zip is pre-security-fix, proving zip-drift is real). Pull the **live server copy** (not the zip) into the local repo; repoint its dashboard/My-Profile holiday feed from `xeno.hr.holiday` → `resource.calendar.leaves` (the maintained source since `xeno_holidays`); keep announcements. Also settle whether `xenhr_application` (mobile) exists while in GitLab. |
| 37 | **Foreigner details migration + work-permit/visa expiry alerts** | 1d | `employee_foreigner_details` → Odoo's native visa/permit fields (hr.version in 19 — same pattern as the audit-log lesson); scheduled-activity alerts on expiry dates. Statutory-adjacent (handoff §5); Xenoptics has foreign staff. |
| 38 | **Read the 2 unread XENHR logic blobs before their Odoo counterparts are built** | 0.5d | (a) `AttendanceAutoRule` rule semantics — blocks scoping plan-#13 and settles the OT-boundary risk; (b) `leave_approval_steps` step-transition logic — confirms whether xeno_leave's single-approver scope-down loses real behavior. Needs GitLab access to `xenhr_backend` (creds from susu, as on 2026-07-09). |
| 39 | **Outgoing-email parity check** | 0.5d | Verify Odoo's outgoing mail server is configured on the live instance and leave-approval notification mails fire; XENHR had per-event toggles (`toggleApproval`) + test-send. Decide the fate of XENHR's unauthenticated email-link approve (recommend: drop, document). |
| 40 | **Per-employee attendance export parity** | 0.5d | Pull `sp_employee_attendance`'s body (3-arg signature known, body never captured); confirm the bymonth `employee=` filter covers it or add a dedicated export. |
| 41 | **Audit coverage extension** | 0.5d | Wire `xeno.audit.mixin` onto `xeno.attendance.code.map`, `xeno.attendance.viewer`, and announcements once #36 lands. **Correction (2026-07-14)**: `hr.employee`'s `b1_*` salary/bank fields were checked, not assumed — they're stored fields and the mixin enumerates `self._fields` at runtime, so they were **already audited** regardless of module load order; this row's original "currently unwatched" claim was wrong. |

**Folded into existing #17/#18 (reconciliation), not new tasks**: employee
taxonomy mapping (Level/Type/Group), per-employee `work_shift_id` →
`resource_calendar_id` assignment. **`leave_balances` table location — resolved
2026-07-14**: `xenhr_backend`'s `2024_01_01_000004_create_leave_balances_table`
migration confirms `employee_id`/`leave_type_id`/`year`/`allocated_days`/
`used_days`/`pending_days`/`carried_days`; `.env.example` confirms it's in the
same SQL Server DB as `employees` (`DB_DATABASE=hr_system`, i.e. the known
HRSystem instance) — no separate database to locate. Grant + dump is Session A.

## 3. Suggested execution order (next 3 server sessions)

1. **Session A (needs SSH + HRSystem)**: org-tree re-match by employee_code
   (#17's highest-value step, unblocks true "reports to me") → then #34
   account generation (same session, same data) → retry the 50 XENDEMO pushes
   if the Service Layer is up (trim the 3 long job titles first).
2. **Session B (needs GitLab) — DONE 2026-07-14**: #38 read auto-rule +
   approval-step logic (OT boundary cleared; approval scope-down safe for the
   common single-approver-per-department case, one residual risk flagged —
   see #38's row; unauthenticated email-approve confirmed a real hole, not a
   guess), confirmed `xenhr_application` exists in GitLab, confirmed
   `xeno_theme_slate` does **not** exist in GitLab (still needs SSH/Session A
   to pull the live copy), located `leave_balances` migration (same HRSystem
   DB as `employees`, no separate database).
3. **Session C (pure Odoo build)**: #35 My Attendance, #37 foreigner
   details + alerts, #39/#40/#41 small parity checks.

## 4. Standing warnings (from this comparison)

- **Never deploy from the handoff zip** — its `xeno_sapb1_sync` predates the
  2026-07-10 security review (no production-write guard, no groups= on
  salary/bank fields, empty-recordset = push-everyone bug present).
- The biometric linked server (`192.168.67.51`) maps every HRSystem login to
  remote **`sa`** — pre-existing architecture, but any credential handed out
  for HRSystem implicitly grants full rights on the scanner DB.
- HRSystem `users.email` remains unusable for identity (85% placeholder).
