{
  "updated": "2026-07-17",
  "totals": {
    "migration_total": 51,
    "migration_done": 43,
    "migration_in_progress": 2,
    "infra_total": 4,
    "infra_done": 3
  },
  "phases": [
    {
      "name": "Phase 1 · Discovery",
      "done": 2,
      "total": 2
    },
    {
      "name": "Phase 2 · Build gaps",
      "done": 23,
      "total": 24
    },
    {
      "name": "Phase 3 · Validation",
      "done": 2,
      "total": 3
    },
    {
      "name": "Phase 4 · Cutover",
      "done": 0,
      "total": 4
    },
    {
      "name": "Phase 2b · Gap re-check",
      "done": 14,
      "total": 15
    }
  ],
  "watch": [
    {
      "title": "Master data reconciliation (#18) closed 2026-07-16 -- all 5 domains done",
      "detail": "Re-verified every domain live rather than trusting the checklist's own numbers, which had drifted stale and even contradicted themselves between detail sections and the summary table. Real state: employees 75/75 linked (the old '3 unaccounted for' no longer exist -- HRSystem's own candidate count dropped since 2026-07-14), org/supervisor tree still resolved, leave balances still fully imported (505 allocations / 103 employees). The one genuine open item, historical attendance & leave, was a scope decision rather than execution -- confirmed with susu that HRSystem's live read-through stays the permanent design (it's the underlying biometric-scan data source, a separate system from XENHR the web app, and keeps serving after XENHR's retirement); no import into hr.attendance/hr.leave planned. RECONCILIATION_CHECKLIST.md updated to match."
    },
    {
      "title": "Thai i18n shipped 2026-07-16 (#23) + a run of bug fixes found while touring the app in Thai",
      "detail": "All 8 content-bearing custom modules (xeno_attendance/xeno_leave/xeno_holidays/xeno_audit_log/xeno_foreigner_details/xeno_resignation/xeno_sapb1_sync/xeno_theme_slate; xeno_data_io has no translatable strings) now ship a real i18n/th.po -- 493 unique terms hand-translated, plus ~25 Python and JS strings that weren't wrapped for translation at all (fixed to use _()/_t so they're extractable going forward). Verified live under a th_TH context. While touring pages in Thai, found and fixed several unrelated real bugs: (1) kiosk check-in was broken -- the GPS geofence guard didn't distinguish the shared kiosk terminal (which can never supply real GPS on this HTTP-only, on-prem-LAN server) from personal-device check-in, now scoped to skip kiosk mode only; (2) a timezone bug in xeno_holidays made every single-day holiday display as spanning 2 days on the calendar (local midnight was stored as UTC midnight, not converted) -- fixed the code and corrected the 23 existing affected holiday rows; (3) leave approval emails showed the part-time end time as a raw decimal (\"10.50\") instead of \"10:30\"; (4) \"My Attendance\" was invisible to every regular employee (0/19 users) because the whole Attendances app ships Officer-only in stock Odoo -- relaxed the app-root + Overview menus to also allow any internal user, Officer-only pages unaffected; (5) added an actual GPS check-in/out widget (live clock, Present/Absent, button) to the self-service My Attendance page, reusing core's own check-in route rather than reimplementing it. Also: widened the sidebar and hid the redundant \"My Time\"/\"Overview\"-style app section-tabs everywhere in the topbar (sidebar already covers that navigation)."
    },
    {
      "title": "ERPDEV-751 / freeze date resolved 2026-07-15",
      "detail": "susu confirmed XENHR is already frozen for further development. No stakeholder sync needed -- plan #2 closed."
    },
    {
      "title": "Dept-taxonomy gap (#47) resolved 2026-07-15; new self-approval-deadlock finding",
      "detail": "Purchasing and Sales & Marketing now have real approver rows (Vanchan Chittam, Thin Zar Myo Lwin); Sales/Logistics/IT confirmed moot (zero active employees). While verifying, found a generic issue: any department's sole team-lead approver can never clear their OWN leave, since xeno_leave's self-approval guard blocks it and there's no other configured approver at that step -- only escape today is HR Force Approve/Reject. The new Approval Chain Status page (being deployed now) surfaces every stuck case like this company-wide."
    },
    {
      "title": "Holiday-source bug fixed and deployed 2026-07-15",
      "detail": "Attendance report (bymonth/myteam/my) was reading a stale HRSystem holidays table (only 4 rows for Jul-Dec 2026) instead of Odoo's own actively-maintained resource.calendar.leaves -- silently under-flagging real holidays. Fixed, deployed, verified live (October 2026's 2 holidays now show correctly)."
    },
    {
      "title": "XENDEMO clone is stale — now a hard blocker",
      "detail": "39 employees vs. XEN's 47 — blocks #18 data reconciliation and #16 parallel-run until refreshed."
    },
    {
      "title": "Attendance Excel export — shipped 2026-07-10",
      "detail": "/odoo/attendances/bymonth/export — xlsxwriter, same late/early/holiday/weekend formatting as the HTML grid. Verified: real HTTP download, valid file, correct data."
    },
    {
      "title": "#5 attendance inline edit — shipped 2026-07-10",
      "detail": "xeno.attendance.override model + merge into report + JSON edit endpoint + Attendance Overrides audit list, deployed to 192.168.60.33 and verified end-to-end over real HTTP (officer edit succeeds + flags cell, non-officer gets AccessError)."
    },
    {
      "title": "#7 export/template/import triad — shipped 2026-07-13, 0.5d not 3-4d",
      "detail": "Odoo's native base_import already does the whole round-trip for free. New module xeno_data_io adds curated Favorite Export field sets for Employees/Leave Applications/Leave Balances/Company Holidays so HR isn't staring at 200+ raw fields; that export doubles as the re-importable template."
    },
    {
      "title": "CRITICAL bug fixed 2026-07-13: self-service leave creation was broken for every regular employee",
      "detail": "xeno_audit_log's mixin (#14) read every audited field -- incl. hr.leave.private_name, ACL-restricted to a higher HR group -- in the acting user's own permission context, not sudo'd. Any non-privileged employee creating their own leave request hit an AccessError since that module's deployment. Fixed by reading via record.sudo() when building the audit snapshot; the audit write itself was already correctly sudo'd."
    },
    {
      "title": "Bug fixed 2026-07-13: Time Off Type dropdown crashed for regular employees",
      "detail": "Custom stored hr.employee fields without groups= (xeno_employee_code + 5 b1_* SAP fields) broke the employee-public prefetch check for any non-HR user touching employee data, e.g. the leave type dropdown. Latent since those fields were added; surfaced once erptest became a plain employee. Fixed with groups=hr.group_hr_user on all 6 -- also closes b1_salary/b1_bank_account being readable by all internal users. Rule: every custom stored hr.employee field needs a groups= restriction."
    },
    {
      "title": "XENDEMO Service Layer unstable 2026-07-14 -- 50 of 56 employee pushes failed",
      "detail": "Critical cache refresh failure / login failed errors, same ones already in the server log from an unrelated leave sync before this work started. Not a data problem -- retry action_push_to_b1() on the 50 un-synced new employees once XENDEMO's Service Layer is stable again. 3 of those 50 will also need job_title trimmed first (SAP B1 JobTitle field rejected values >=24 chars with no spaces)."
    },
    {
      "title": "Odoo asset bundle caching can mask deployed JS/CSS fixes",
      "detail": "Discovered 2026-07-14 fixing #43: web.assets_backend* bundles are cached as ir.attachment rows and don't auto-invalidate on a plain container restart. If a JS/SCSS change doesn't show up after deploy + restart, delete the stale ir.attachment rows (name like '%assets%') and restart again."
    }
  ],
  "tasks": [
    {
      "phase": "Phase 1 — Discovery",
      "id": "1",
      "task": "Read XENHR's actual code for accurate gap list",
      "status": "done",
      "est": "—",
      "note": "Cloned & reviewed both repos; verified gap list supersedes HANDOFF.md's commit-history guesses"
    },
    {
      "phase": "Phase 1 — Discovery",
      "id": "2",
      "task": "Sync with stakeholders on ERPDEV-751 + freeze date",
      "status": "done",
      "est": "0.5–1d",
      "note": "RESOLVED 2026-07-15: susu confirmed XENHR is already frozen for further development -- no outstanding stakeholder sign-off needed."
    },
    {
      "phase": "Phase 2 — Build gaps in Odoo (build order, not task ID order)",
      "id": "10",
      "task": "Biometric scanner attendance import",
      "status": "inprogress",
      "est": "~0.5d left",
      "note": "Spike, hardening, live page (now sidebar-embedded), Excel export, and code-remap table + unmapped-code diagnostic all shipped + verified. Remaining: Odoo-first sourcing (blocked on #18's identity reconciliation) UPDATE 2026-07-15: re-scoped the remaining 'Odoo-first sourcing' item into its 4 parts rather than treating it as one block. Shift stays HRSystem-sourced (bundled in the SP's own rows, not swappable). Employee directory stays HRSystem-sourced (it's the correct ground truth for the unmapped-code diagnostic -- Odoo's xeno_employee_code coverage, 75/78, is actually less complete). Leave overlay must stay HRSystem-sourced for now -- checked Odoo's hr.leave, only 7 test records exist, real leave for active employees is still tracked in HRSystem/XENHR until cutover. Holidays: found and fixed a real bug -- HRSystem's own `holidays` table is stale (only 4 rows for Jul-Dec 2026, missing Constitution Day/Chulalongkorn Memorial Day/etc.) while Odoo's resource.calendar.leaves (kept current via #29's live-API pulls) has the full set. get_month_report() now sources holidays from Odoo instead. DEPLOYED 2026-07-15 (v1.4.0, clean upgrade + restart), verified live: October 2026 now correctly shows both holidays across all 3 report pages (bymonth/myteam/my)."
    },
    {
      "phase": "Phase 2",
      "id": "3",
      "task": "\"My Team\" attendance view",
      "status": "done",
      "est": "2–3d",
      "note": "Shipped: /odoo/attendances/myteam, keyed on HRSystem employee_code (sidesteps empty parent_id, same as #4/#11). Verified end-to-end with a non-HR manager"
    },
    {
      "phase": "Phase 2",
      "id": "11",
      "task": "Configurable approval chains",
      "status": "done",
      "est": "2–3d",
      "note": "Shipped: xeno.leave.approver (dept/employee rule). Scope cut: single approver per rule, not full N-step hand-off"
    },
    {
      "phase": "Phase 2",
      "id": "4",
      "task": "\"My Team Leaves\" approval queue",
      "status": "done",
      "est": "1–2d",
      "note": "Approver-config-scoped, not manager-hierarchy-scoped (parent_id still empty) — sidesteps that blocker entirely"
    },
    {
      "phase": "Phase 2",
      "id": "6",
      "task": "Leave override + HR update",
      "status": "done",
      "est": "2–3d",
      "note": "Shipped: force-approve/reject buttons + HR Update wizard, chatter-logged. Tested (6/6 scenarios) with throwaway fixtures"
    },
    {
      "phase": "Phase 2",
      "id": "5",
      "task": "HR inline edit for attendance",
      "status": "done",
      "est": "1d",
      "note": "Shipped: HR/Admin override saves as a new record (xeno.attendance.override), source scan never touched — override always wins on the report, visibly flagged (blue dot + tooltip in HTML, blue fill + * in the xlsx export). Click-to-edit UI gated to hr_attendance.group_hr_attendance_officer, mirrors xeno_leave's force-approve gate. New 'Attendance Overrides' list under Attendances > Configuration for audit. Deployed to 192.168.60.33, verified live (officer edit succeeds, non-officer AccessError)"
    },
    {
      "phase": "Phase 2",
      "id": "9",
      "task": "GPS check-in/out with geofencing",
      "status": "done",
      "est": "1d (was 2-3d)",
      "note": "SHIPPED 2026-07-14: read XENHR's real geofencing source (AttendanceService::checkIn/validateGpsLocation, Location model, haversine formula) -- turned out Odoo core hr_attendance ALREADY captures GPS on check-in/out (in_latitude/in_longitude via the systray widget's browser geolocation) but never restricts it. Added new model xeno.attendance.location (name/lat/long/radius/active) and a single hook override on hr.employee._attendance_action_change: no active locations = unrestricted (unchanged default), otherwise GPS required and must be within some location's radius or the check-in is rejected with the closest distance shown -- exact port of XENHR's validateGpsLocation/OutsideLocationException. Check-out is never geofenced, matching XENHR. Enabled the company's Device Tracking setting so the browser actually gets asked for GPS. No new frontend needed -- reuses Odoo's existing check-in/out UI entirely. Deployed to 192.168.60.33, verified live via 5 odoo-shell scenarios."
    },
    {
      "phase": "Phase 2",
      "id": "13",
      "task": "Attendance auto-rules processing",
      "status": "done",
      "est": "0.5d (was 1-2d)",
      "note": "SHIPPED 2026-07-14: read XENHR's real AttendanceAutoRule source -- only the 'auto_fill' rule type is actually processed anywhere in that codebase; 'ignore' is validated in the API but never consumed by the scheduled command or the manual-trigger endpoint (confirmed dead code), so only auto_fill was ported. New model xeno.attendance.auto.rule (employee or department target): a working day with no scan at all for a covered employee now shows \"Present\" in all 3 reports (bymonth/myteam/my) and the My Profile calendar, instead of blank/absent -- never overrides a real scan. Also fixed a real gap surfaced while building this: an employee with zero punches all month didn't appear in the report at all (the SP only returns employees with at least one row this month) -- synthesizes a row for auto-fill-covered employees so they show up. Verified: 19 working days correctly flip to Present once configured, both via direct employee and department targeting."
    },
    {
      "phase": "Phase 2",
      "id": "7",
      "task": "Export + template + import triad",
      "status": "done",
      "est": "0.5d (was 3–4d)",
      "note": "Re-scoped: Odoo's native base_import already gives every model a full export/template/import round-trip for free (verified with a real test row). Shipped xeno_data_io — 4 curated Favorite Export field sets (Employees, Leave Applications, Leave Balances, Company Holidays) instead of Odoo's 200+ raw fields; export doubles as the import template. Attendance excluded — already covered by #10's own Excel export, native hr.attendance isn't the real data source."
    },
    {
      "phase": "Phase 2",
      "id": "12",
      "task": "Resignations module",
      "status": "done",
      "est": "1d",
      "note": "Checked XENHR's real source (ResignationController) before building: no approval workflow, just a direct HR record. Creating one archives the employee + deactivates their login (matches XENHR exactly), logged to chatter. New xeno_resignation module, gated to hr.group_hr_user. Verified end-to-end with a throwaway employee."
    },
    {
      "phase": "Phase 2",
      "id": "8",
      "task": "Confirm multi-language parity",
      "status": "done",
      "est": "0.5d",
      "note": "Confirmed: core hr/hr_holidays/hr_attendance ship real th.po files, verified live rendering. Ahead of XENHR. New finding: all 4 custom modules have zero i18n — see #23"
    },
    {
      "phase": "Phase 2",
      "id": "23",
      "task": "Add Thai i18n to custom Xenoptics modules",
      "status": "done",
      "est": "0.5-1d/module",
      "note": "SHIPPED 2026-07-16: all 8 content-bearing modules now ship a real i18n/th.po (493 unique terms translated); xeno_data_io has nothing translatable. Fixed ~25 Python/JS strings that weren't wrapped for translation at all first (_()/_t), or they'd never have been extractable regardless of the .po files. Verified live under a th_TH context (field labels, selection options, menu names all resolve correctly). See watch list for the run of unrelated bugs found while touring the app in Thai."
    },
    {
      "phase": "Phase 2",
      "id": "24",
      "task": "Per-leave-type request timing limits",
      "status": "done",
      "est": "0.5d",
      "note": "Mirrored XENHR's advance_days/max_backdate_days exactly. Bug found in susu's testing 2026-07-13: bypass checked acting user's role, not target employee, so HR/admin accounts could skip the rule for their OWN leave. Fixed to only bypass when explicitly creating for a different employee. Re-verified 4/4 scenarios incl. the reported case."
    },
    {
      "phase": "Phase 2",
      "id": "25",
      "task": "Add missing leave types (Unpaid, Maternity, Emergency)",
      "status": "done",
      "est": "0.5d",
      "note": "Odoo only had 3 of XENHR's 6 types. Added the other 3 with proper field mapping + allocations for the same 50 real employees. Emergency Leave created but inactive, mirroring XENHR's own disabled state"
    },
    {
      "phase": "Phase 2",
      "id": "26",
      "task": "Leave balance grid view on Allocations page",
      "status": "done",
      "est": "0.5d",
      "note": "Native Pivot view with Allocated/Used/Remaining measures, matching XENHR's LeaveBalancePage exactly. Auto-syncs Used when a leave is approved/refused, HR-revisable. Color-coded (red/green/blue) via a QWeb patch, since Pivot has no native per-measure styling. Verified end-to-end with a real approve+refuse cycle"
    },
    {
      "phase": "Phase 2",
      "id": "27",
      "task": "Employee identity bridge (xeno_employee_code)",
      "status": "done",
      "est": "0.5d",
      "note": "Added HRSystem-code bridge field on hr.employee, backfilled for 18 confirmed name-matches (of 78 HRSystem employees). Found HRSystem's email field is 85% placeholder data, so email matching was abandoned. Archived 3 employees confirmed inactive-in-HRSystem-but-active-in-Odoo; remaining 60 unmatched, full reconciliation belongs to master-data task"
    },
    {
      "phase": "Phase 2",
      "id": "14",
      "task": "Audit log viewer",
      "status": "done",
      "est": "1d",
      "note": "Checked XENHR's real AuditLog+Auditable trait first. New xeno_audit_log module: reusable mixin (create/write/unlink hooks) wired onto 7 models. Caught a real gap: Odoo 19 moved job_title/wage off hr.employee onto a new hr.version model, added that too. Viewer restricted to hr.group_hr_manager, read-only for all."
    },
    {
      "phase": "Phase 2",
      "id": "22",
      "task": "Security review of SAP B1 write paths",
      "status": "done",
      "est": "1d",
      "note": "4 findings fixed: push/import buttons now gated to hr.group_hr_user, empty-recordset no longer pushes everyone, production-write guard (blocks XEN unless explicitly confirmed) wired into all 4 write paths incl. the leave-approval hook. Verified, zero regression"
    },
    {
      "phase": "Phase 2",
      "id": "28",
      "task": "Leave approval terminology: Validate/Refuse -> Approve/Reject",
      "status": "done",
      "est": "0.5d",
      "note": "Relabeled hr.leave + hr.leave.allocation form/list/kanban (6 views, desktop+mobile) via pure view inheritance in xeno_leave, no logic change. Matches this module's own Force Approve/Force Reject wording. Verified via get_view(): zero Validate/Refuse left anywhere."
    },
    {
      "phase": "Phase 2",
      "id": "29",
      "task": "Holidays page: calendar view + rename + Public/Company type",
      "status": "done",
      "est": "0.5d",
      "note": "Referenced xenhr_frontend's HolidaysPage.tsx for fields/style. xeno_holidays module: xeno_holiday_type + xeno_remark fields (backfilled from real data), calendar view on the Holidays page, action/menu renamed. Plus: personal Time Off dashboard (/odoo/time-off) now shows holidays green vs weekends grey via an Owl patch on Odoo's year-calendar renderer, verified by compiling the real JS/CSS bundle server-side."
    },
    {
      "phase": "Phase 3 — Data & validation (can overlap Phase 2, per-feature)",
      "id": "15",
      "task": "Import worth-keeping XENHR test data",
      "status": "done",
      "est": "0.5d",
      "note": "Imported 2 rounds: static 2025 seeder data first, then susu pointed at live XENHR's own API for the real current 2026 calendar (logged in via API, GET /api/holidays?year=2026) -- 24 records, includes a mid-year date correction the static seeder wouldn't have had. Plus 2 confirmed-real shift calendars (Morning 8-17, Day 9-18)."
    },
    {
      "phase": "Phase 3",
      "id": "18",
      "task": "Master/historical data migration + reconciliation",
      "status": "done",
      "est": "2–4d",
      "note": "CLOSED 2026-07-16 -- re-verified all 5 domains live against the running instance rather than trusting the checklist's own stale numbers (it had internal contradictions between its detail sections and summary table). Employees: 75/75 real HRSystem rows linked via xeno_employee_code (zero gap; earlier '3 unaccounted for' no longer applies -- HRSystem's own candidate count dropped from 78 to 75 since 2026-07-14). Org/supervisor tree: confirmed still resolved (93 employees carry parent_id). Leave balances: confirmed done (505 allocation records / 103 employees). Historical attendance & leave: the one genuine open item was a scope decision, not execution -- susu confirmed the live HRSystem read-through report stays the permanent design (it's a separate system from XENHR itself, continues serving after XENHR's retirement), no import into hr.attendance/hr.leave needed. XENDEMO SAP B1 push (tracked separately, not an Odoo-reconciliation blocker): 44/76 pushed, 32 still blocked on XENDEMO's own Service Layer instability. RECONCILIATION_CHECKLIST.md updated to match."
    },
    {
      "phase": "Phase 3",
      "id": "16",
      "task": "Parallel-run Odoo vs. XENHR",
      "status": "moot",
      "est": "—",
      "note": "CLOSED 2026-07-16: susu confirmed XENHR development is stopped and it's no longer used in production. With no live system left to compare against, there's nothing to run Odoo in parallel with -- straight cutover instead. Focus stays on #18 (one-time data migration/reconciliation) and #19 (cutover runbook)."
    },
    {
      "phase": "Phase 4 — Cutover",
      "id": "19",
      "task": "Cutover runbook + rollback plan",
      "status": "notstarted",
      "est": "1d",
      "note": "Downtime window, go/no-go criteria, revert path"
    },
    {
      "phase": "Phase 4",
      "id": "20",
      "task": "User training / change management",
      "status": "notstarted",
      "est": "1–2d",
      "note": "Comms, quick-ref docs, walkthrough session"
    },
    {
      "phase": "Phase 4",
      "id": "17",
      "task": "Archive XENHR repos read-only",
      "status": "notstarted",
      "est": "0.5–1d",
      "note": "—"
    },
    {
      "phase": "Phase 4",
      "id": "21",
      "task": "Post-cutover monitoring & alerting",
      "status": "notstarted",
      "est": "1d",
      "note": "Biometric import health, SAP B1 push health, approval-queue backlog"
    },
    {
      "phase": "Infra track (parallel, not blocking migration)",
      "id": "-",
      "task": "Switch SAP B1 sync target to XENDEMO",
      "status": "done",
      "est": "—",
      "note": "sapb1.company_db set via ir.config_parameter"
    },
    {
      "phase": "Infra track",
      "id": "-",
      "task": "Diagnose + fix config-cache staleness bug",
      "status": "done",
      "est": "—",
      "note": "Fix: restart odoo container after any sapb1.* change"
    },
    {
      "phase": "Infra track",
      "id": "-",
      "task": "Clean up stray test record in production XEN",
      "status": "done",
      "est": "—",
      "note": "Deleted EmployeesInfo(51) from XEN + unlinked Odoo record 54"
    },
    {
      "phase": "Infra track",
      "id": "-",
      "task": "Refresh stale XENDEMO clone",
      "status": "flagged",
      "est": "—",
      "note": "39 employees vs. XEN's 47 — blocks #18/#16, ask SAP B1 admin to refresh first"
    },
    {
      "phase": "Phase 2",
      "id": "30",
      "task": "Day/Half-Day/Hour duration picker (Annual/Personal/Sick)",
      "status": "done",
      "est": "1d",
      "note": "Confirmed #24's advance/backdate rules already match XENHR's live API exactly. New xeno_leave field xeno_request_unit lets requester pick Day/Half-Day/Hour per request on flexible types, by overriding Odoo's related leave_type_request_unit field. Verified 11 scenarios incl. non-flexible types correctly unaffected and #24's rules still firing correctly."
    },
    {
      "phase": "Phase 2",
      "id": "31",
      "task": "Restrict leave approval to configured approver; lock post-submit edits to HR",
      "status": "done",
      "est": "1d",
      "note": "Bug found by susu: employees could approve their own leave (Odoo grants any Officer approval rights over any leave, incl. their own). Narrowed can_approve/can_validate/can_refuse to require xeno.leave.approver's configured approver, falling back to normal access only when nothing's configured. Requester can no longer edit submitted content, only cancel/delete. Also fixed a critical xeno_audit_log bug (see watch list) blocking all self-service leave creation, found while testing this."
    },
    {
      "phase": "Phase 2",
      "id": "32",
      "task": "Redesign leave form to XENHR's Apply for Leave dialog",
      "status": "done",
      "est": "0.5d",
      "note": "Balance strip (Total/Used/Pending/Remaining, live-computed) + Leave Type Basis pill picker with XENHR's exact 4 base types (Full-day/Part-time/First-half/Second-half). Half-day bases auto-set AM/PM. Selection keys migrated on 9 existing rows. Verified all 4 bases compute correct durations; strip matches independent sums."
    },
    {
      "phase": "Phase 2",
      "id": "33",
      "task": "Nest Directory under Employees in the sidebar",
      "status": "done",
      "est": "trivial",
      "note": "hr.menu_hr_employee (Directory) was a top-level sibling of Employees, not a child. Reparented via xeno_resignation. Verified in the actual menu tree."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "34",
      "task": "Employee self-service accounts + login convention",
      "status": "notstarted",
      "est": "1-2d",
      "note": "Cutover-blocker found by full-inventory gap re-check (XENHR_ODOO_GAP_ANALYSIS.md): XENHR logs in by employee_code, most Odoo employees have NO res.users account. Decide internal vs portal, employee_code-derived logins (HRSystem emails 85% placeholder), password distribution."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "35",
      "task": "\"My Attendance\" employee self-view",
      "status": "done",
      "est": "0.5-1d",
      "note": "DEPLOYED & verified 2026-07-14: /odoo/attendances/my + export + menu in xeno_attendance v1.1.0, self-only restrict_codes. Live on 192.168.60.33. Extended 2026-07-16: added an actual GPS check-in/out widget (live clock, Present/Absent status, button) above the history table, reusing core's own /hr_attendance/systray_check_in_out route rather than reimplementing it. Also fixed two access bugs found in the same pass: the whole Attendances app was invisible to every regular employee (Officer-only by default in stock Odoo), and the GPS geofence guard didn't distinguish kiosk terminal check-ins (which can never supply real GPS here) from personal-device ones."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "36",
      "task": "Version-control xeno_theme_slate + kill duplicate holiday model",
      "status": "done",
      "est": "0.5-1d",
      "note": "DONE 2026-07-14: pulled the live module into the repo (confirmed not in GitLab). Found and fixed a real live bug: My Profile widget's calendar read xeno.hr.holiday, a separate unmaintained model with only 2 fake test rows, while the real 44-row calendar lives in resource.calendar.leaves (#29). Repointed the widget's query, deleted the dead model + views + access rules; Odoo auto-cleaned all dependent records on upgrade, zero errors. Announcements model untouched (no known issue)."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "37",
      "task": "Foreigner details migration + permit/visa expiry alerts",
      "status": "done",
      "est": "1d",
      "note": "DONE 2026-07-14: module xeno_foreigner_details deployed; data imported for the 2 real foreigners in HRSystem (100202 Nyein Su Su, 200002 Wa Wa Lwin -- 9 other rows were empty is_foreigner=0 placeholders), HR Responsible set to the hr.manager@xenoptics.com role account. Known gap flagged not fixed: HRSystem's visa_issue_place column has no Odoo field (only 3 of 4 metadata columns are tracked)."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "38",
      "task": "Read XENHR AttendanceAutoRule + leave_approval_steps logic",
      "status": "done",
      "est": "0.5d",
      "note": "READ 2026-07-14: AttendanceAutoRule has no OT logic (the \"ignore\" rule is dead code) -- settles the OT boundary question for #13. Full approval-step chain read from source (ApproverConfigController::buildChain, LeaveApprovalStep::canBeActedOn, LeaveApplicationController) via GitLab -- confirms xeno_leave's current single-approver scope-down is NOT safe long-term: XENHR is a real N-step chain (dept/employee override + always-appended HR step), now being ported as a new task."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "39",
      "task": "Outgoing-email parity check",
      "status": "done",
      "est": "0.5d",
      "note": "DONE 2026-07-15: SMTP configured (Office365, erp@xenoptics.com, verified live); new xeno_mail_staging module redirects ALL outgoing mail to susu@xenoptics.com during testing (original recipient preserved in subject + X-Xeno-Original-To header, one system parameter to disable before cutover); root-caused and fixed the original queue failures (Office365 SendAsDenied on Odoo's odoobot@example.com placeholder -- added a proper mail.alias.domain for xenoptics.com); all ~47 queued emails now send. Then built the actual requested feature: leave approval emails matching susu's XENHR template exactly (bilingual Thai/English, balance table, base-type/dates/reason/recent-leave-date), sent to the current step's approver on submission and on each step advance, with real click-to-Approve/Reject links. Deliberately NOT XENHR's unauthenticated approveStepEmail (confirmed unauth'd, anyone with the link could act) -- used an HMAC-signed, single-use, expiring token bound to the exact leave/step/action/user instead, verified via the same action_approve/action_refuse permission checks as the in-app buttons. Verified end-to-end over real HTTP: 2-step chain fully approved via clicked links (state -> validate), tampered action/leave_id both correctly rejected, replay after processing correctly blocked. Extended 2026-07-15: added a final-outcome notification to the REQUESTOR when the leave is fully approved or rejected (same template, no action buttons, full per-step approval/rejection summary) -- verified end-to-end via the same real-HTTP click test. UPDATE 2026-07-15: xeno_mail_staging extended with an xeno_mail_staging.exempt_emails system parameter (comma-separated addresses) so specific real recipients can receive mail for real while everyone else stays redirected -- only applies when EVERY recipient of a message is on the list, so a mixed message still redirects in full. Deployed (v1.1.0, clean upgrade + restart). Set to thinzar@xenoptics.com so she actually receives her approval-step emails now that she has a working login and is configured as a real approver (#42's data migration)."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "40",
      "task": "Per-employee attendance export parity",
      "status": "done",
      "est": "0.5d",
      "note": "RESOLVED 2026-07-16: connected directly to HRSystem SQL Server and checked -- 'sp_employee_attendance' isn't a real stored procedure (an unverified gap-analysis guess); the only attendance routine that exists is sp_get_attendance_month_V3, taking just start_date/end_date. Per-employee filtering already happens client-side via the employee_code param already used by /odoo/attendances/my and bymonth?employee=. Full parity already exists, nothing left to build."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "41",
      "task": "Audit coverage extension",
      "status": "done",
      "est": "0.5d",
      "note": "SHIPPED 2026-07-16: b1_salary/b1_bank_account verified already audited (stored hr.employee fields); mixin added to code.map + viewer 2026-07-14 (audit v1.1.0). Last gap closed today -- xeno.hr.announcement added to the audit mixin now that #36 (its module's version control) is done, xeno_audit_log v1.3.0 deployed and upgraded cleanly. Verified: model correctly inherits xeno.audit.mixin and carries the write hook."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "42",
      "task": "Multi-step leave approval chain (port XENHR's Approver Setup)",
      "status": "done",
      "est": "2-3d",
      "note": "SHIPPED 2026-07-14: xeno.leave.approver is now an ordered per-department/per-employee step template (employee overrides fully replace the department chain when configured, both always end in a synthetic \"any HR/Admin\" final step, ported exactly from ApproverConfigController::buildChain). Submitting a leave snapshots the resolved chain onto new model xeno.leave.approval.step; action_approve()/action_refuse() now operate per-step (previous steps must already be approved; rejecting any step rejects the whole leave, matching XENHR). New preview wizard on the Employee form + reworked Leave Approvers list (drag-to-reorder, grouped by department). Deployed to 192.168.60.33, verified live via 8 odoo-shell scenarios: chain build, materialization on submit, non-approver blocked, step-1 approve keeps state at confirm, final step approve transitions to validate, reject-any-step refuses the whole leave, employee override fully replaces department chain, and a legacy leave with no chain still falls back to the old single-shot behavior. UPDATE 2026-07-15: real approver data migrated in. Almost none of HRSystem's named approvers had an Odoo login (only HR Manager/Jarin/Susu/test accounts existed) -- created 13 placeholder res.users (login=bare employee_code, unusable random password, no invite sent) linked to their existing hr.employee, per the login convention already decided in #34. Populated 23 xeno.leave.approver rows: HR/Admin/Account/R&D's real 2-step chains (43% of active headcount) plus Engineering/Software/Quality/Production (unambiguous 1:1 dept match, found while pulling the data -- HRSystem configures approvers for all 13 depts, not just the 4 flagged multi-step ones), the 5 real per-employee overrides, and Thin Zar Myo Lwin's own MD override. Verified via build_chain() for 6 employees spanning every path. Excluded 3 XENHR test/dev rows and 1 self-referential row. New gap found: Marketing/Sales/Logistics/Procurement/IT don't map 1:1 to Odoo's department list -- see #47."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "43",
      "task": "Fix holiday cell color in Month view of Time Off dashboard",
      "status": "done",
      "est": "0.5d",
      "note": "#29's green-vs-grey holiday patch only covered the Year calendar renderer -- Month/Week/Day view still fell back to plain grey. Added the same weekday-split patch for TimeOffCalendarCommonRenderer. Deploy also caught a real ops gotcha: Odoo had cached the old JS/CSS bundle in ir.attachment since 2026-07-03, so the code fix alone didn't show up until that stale bundle was deleted and the container restarted."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "44",
      "task": "Match monthly attendance report styling to xenhr_frontend",
      "status": "done",
      "est": "trivial",
      "note": "The bymonth/my/myteam attendance report's shared CSS had a @media (prefers-color-scheme:dark) block that repainted it navy for anyone with OS dark mode on -- XENHR's page never does this, it's always light. Removed the dark override so all three report pages always render the same light palette as xenhr_frontend."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "45",
      "task": "Add description field to holiday table",
      "status": "done",
      "est": "trivial",
      "note": "DONE 2026-07-14: the field already existed (xeno_remark, mirroring XENHR's Holiday.description) but was labeled \"Remark\" and hidden by default in the list. Relabeled to \"Description\" and made visible by default -- zero data risk, no column change."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "46",
      "task": "Attendance history calendar on My Profile (user-side visibility)",
      "status": "done",
      "est": "0.5d",
      "note": "DONE 2026-07-14: root cause of \"attendance not showing\" -- the My Profile widget queried Odoo's native hr.attendance table, which is essentially unused (real attendance lives in HRSystem via biometric/app punches, same source as /odoo/attendances/my). Added a JSON endpoint (/odoo/attendances/my/calendar) reusing that same HRSystem query, and extended the My Profile mini-calendar to mark each day's status (present/late/absent/holiday/weekend/leave) with a colored indicator + tooltip, replaced the dead \"Today's Attendance\" card with real data, and added a link through to the full Monthly Attendance page. Verified the day-classification logic against real HRSystem data (a real 'late' check-in correctly flagged) and that the new route is registered and auth-gated correctly."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "47",
      "task": "Resolve dept-taxonomy mismatch for Marketing/Sales/Logistics/Procurement/IT approvers",
      "status": "done",
      "est": "—",
      "note": "RESOLVED 2026-07-15: checked real HRSystem employee counts rather than guessing -- Sales, Logistics and IT have zero active employees, so only one real department per Odoo-side grouping actually needed a rule. Wrote the 2 real rows: Purchasing -> Vanchan Chittam, Sales & Marketing -> Thin Zar Myo Lwin (HR). Confirmed with susu before writing. New generic finding surfaced while verifying: a department's sole team-lead approver can never clear their OWN leave (self-approval guard blocks it) -- affects Vanchan/Purchasing and by the same pattern Engineering/Software/Quality/Production's leads. No workaround today besides HR Force Approve/Reject; the new Approval Chain Status page (below) is built to surface exactly this."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "48",
      "task": "Approval Chain Status page (company-wide pending-approvals monitor)",
      "status": "done",
      "est": "0.5d",
      "note": "Built 2026-07-15, confirmed live and working 2026-07-16: list view under Time Off > Management showing every leave company-wide currently sitting at a step -- employee, department, type, step X of N, approver, waiting-since, and a red-flagged 'Stuck?' column for the self-approval-deadlock case found while resolving #47. Model xeno.leave.approval.step extended with 3 new stored computed fields. Re-verified directly against the running instance: view compiles, action resolves correctly, 6 real pending-approval rows currently match, 0 stuck right now."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "49",
      "task": "System Access / Module Access management (per-employee + per-group self-service module gating)",
      "status": "in_progress",
      "est": "1-1.5d",
      "note": "Config UI deployed 2026-07-17: xeno_module_access module. xeno.hr.module (admin module list, seeded Attendance+Leave -- the only 2 XENHR actually enforces today, others in the screenshot like Overtime/Outside Request are unbuilt placeholder rows in XENHR itself). Employee Group = Odoo's native hr.employee.category (Tags), extended with xeno_module_ids -- reused rather than building a parallel model, per susu's question. xeno.employee.module.access covers the individual per-employee override. hr.employee._xeno_has_module_access() resolves group-OR-individual with HR/Admin bypass, matching XENHR's canAccessModule() exactly (verified live: group grant wins over individual, both toggle sources report correctly, tested and rolled back with no data left behind). NOT YET DONE: actually gating My Attendance menu + native Time Off self-service on this -- deliberately deferred to a second pass per susu's explicit build-order choice."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "50",
      "task": "Admin/HR overview dashboard (landing page for the HR app)",
      "status": "done",
      "est": "1-1.5d",
      "note": "Built + deployed 2026-07-17: xeno_hr_dashboard, new landing page of the custom HR app (menu_xeno_hr_root), sequenced before Leave List/Approval. Ports XENHR's AdminDashboardPage.tsx: 4 stat cards (total/present/absent/pending, present-absent sourced from the same HRSystem SP xeno_attendance's monthly report uses -- verified live, 0.63s for a 7-day pull), weekly present/absent bar chart, Employee Attendance Report (leave-by-date table w/ prev-next nav, reuses xeno_leave's flexible-duration fields for period formatting), Dashboard Calendar (xeno_holidays + passport/visa/work-permit expiry overlay -- confirmed those 3 fields are core hr.version fields delegated onto hr.employee, no new fields needed), Today's Announcements (existing basic xeno.hr.announcement model, date==today only -- richer XENHR parity (date range, audience, attachment) is gap #3, deliberately not built here). Backend stats method verified end-to-end with real data via odoo shell; action/menu wiring and sequencing confirmed; stale asset bundle cache cleared with susu's confirmation. NOT verified in an actual browser session (no credentials/browser tool available this pass) -- susu should sanity-check the live page."
    },
    {
      "phase": "Phase 2b — Gap re-check 2026-07-14",
      "id": "51",
      "task": "Announcements: date-range/audience/attachment/email parity + maternity return-to-work message",
      "status": "done",
      "est": "0.5-1d",
      "note": "Built + deployed 2026-07-17. xeno.hr.announcement (xeno_theme_slate) extended: start_date/end_date, is_all_day + start_time/end_time, target_audience_type (All/Department -- REAL enforcement per susu's choice, not a cosmetic label like XENHR), attachment, and a new send_email checkbox (not in XENHR at all). Every new announcement always in-app notifies its target audience (message_notify, real bell/inbox); send_email additionally fires one real outgoing email, once (email_sent_at guard). Employee's own landing page (app_dashboard.js, already the post-login home action) now filters by date range + real audience match; the HR admin dashboard (xeno_hr_dashboard, #50) shows everything valid today regardless of audience, matching XENHR's own admin-side behavior. New: xeno_leave daily cron -- when an approved Maternity Leave ends and the employee is back the next day, posts + emails a congratulations message once (xeno_welcome_back_sent guard). Paternity Leave intentionally NOT added (doesn't exist as a real type in XENHR either -- susu's call, Maternity only for now). CAUGHT AND FIXED a real migration bug during deploy: the post-migrate script's 'WHERE start_date IS NULL' guard never matched, since Odoo's own _auto_init already backfills a new required field's default before post-migrate runs -- the 1 existing prod announcement briefly showed today's date instead of its real 2026-07-03 origin. Fixed the live row (from my own earlier read, since the old Fri Jul 17 08:29:15 UTC 2026 column was later dropped by Odoo's own field-cleanup) and corrected the migration script for any future install. Verified end-to-end (notify+email+constraints+cron idempotency) via rolled-back shell tests, no data left in prod. NOT visually verified in a browser (no credentials/browser tool available) -- susu should sanity-check the announcement form, the employee landing page feed, and the admin dashboard card."
    }
  ]
}