Files
Odoo-Modules/fusion_clock/docs/superpowers/specs/2026-06-04-remove-planning-dependency-design.md
gsinghpal 9574fa0ae4 docs(fusion_clock): design spec — remove Odoo Planning dependency
Re-fit Planning's role + recurrence + send onto the native per-day
fusion.clock.schedule model; retire fusion_planning into fusion_clock;
make the family Community-installable. Full feature-parity matrix,
data migration, and gated Entech rollout included.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:29:27 -04:00

367 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Remove the Odoo Planning dependency from the Fusion Clock family
**Date:** 2026-06-04
**Module:** `fusion_clock` (absorbs `fusion_planning`, which is retired)
**Status:** Design — awaiting spec review
---
## 1. Goal
Make the Fusion Clock product family **fully Community-installable** (no Odoo
Enterprise `planning` dependency) **and** simplify the Entech deployment, while
**preserving every scheduling capability** currently delivered through Odoo
Planning. No feature is removed; the work is sequenced, not trimmed.
Driver (confirmed): **Both** — ship to clients without Enterprise *and* cut the
barely-used Planning Gantt out of Entech.
## 2. Current state (verified)
`fusion_clock` itself does **not** depend on `planning`. Its deps are clean:
`hr_attendance, hr, portal, mail, resource`. The entire Odoo-`planning` coupling
lives in **one bridge module, `fusion_planning`**:
| Coupling point | Where |
|---|---|
| `depends: ['planning']` | `fusion_planning/__manifest__.py` |
| `_inherit = 'planning.slot'` (+ `x_fc_additional_resource_ids`, auto-publish `create()`) | `fusion_planning/models/planning_slot.py` |
| inherits `planning.planning_view_form`, uses `planning.group_planning_manager` | `fusion_planning/views/planning_slot_views.xml` |
| uses `hr.employee.default_planning_role_id` / `planning_role_ids`, menu under `planning.planning_menu_settings` | `fusion_planning/views/hr_employee_role_views.xml` |
| reads `planning.slot` for the portal Schedule tab (already merges with native schedule) | `fusion_planning/controllers/portal_schedule.py` |
| the Planning **Gantt** backend UI (`web_gantt`, Enterprise) | Odoo Planning |
**Live Entech data (LXC 111, DB `admin`, Enterprise):**
| Table | Rows |
|---|---|
| `planning.slot` | **8** (7 published) |
| `planning.role` | **1** |
| `fusion.clock.schedule` (native per-day planner) | **144** |
| `fusion.clock.shift` (native templates) | 6 |
The **native** per-day planner (`fusion.clock.schedule` + the OWL shift planner)
is the real workhorse. Odoo Planning is essentially vestigial here.
No other module in the repo references `planning` or `fusion_planning` (the one
grep hit in `fusion_plating` is the English word "Planning" in a selection — noise).
## 3. Decision & rationale
**Chosen approach: re-fit Planning's *logic* onto the native per-day model
(`fusion.clock.schedule`), extended to full feature parity. Retire
`fusion_planning` by folding everything into `fusion_clock`.**
Rejected alternative: *vendor `planning.slot` + `planning.recurrency` +
`planning.planning` wholesale*. Why rejected:
1. **The Gantt can't come to Community anyway.** `web_gantt` is Enterprise.
Vendoring `planning.slot`'s datetime model still leaves us building a
non-Gantt UI — so wholesale vendoring buys the heavy data model but not the UI.
2. **Bugs live in the attendance pipeline, not the recurrence engine.**
fusion_clock's penalties (money), overtime, absence detection, reminders,
portal and dashboard all read one contract: `hr.employee._get_fclk_day_plan()`
off `fusion.clock.schedule`. Option 1 leaves that pipeline's data source
**untouched** and confines new code to isolated, testable features. Wholesale
vendoring forces either a dual schedule model or a rewire of that
money-critical pipeline onto Planning's datetime model **plus** a migration of
the 144 live rows — the highest-risk change possible, in exactly the code we
most need to keep correct.
3. **Reuse is still honoured.** We copy the parts that copy cleanly
(`planning.role` near-verbatim, the recurrence **field design + repeat
semantics**, the **mail templates**) and re-fit only the generation loop —
which is *less* code on the per-day model because it drops the
resource-interval/DST math.
**Two honest deltas vs Odoo Planning (only these):**
- **The Gantt drag-drop board** → replaced by the native weekly OWL planner.
Capability preserved, UX differs. (Accepted by owner.) Drag-drop is a possible
future enhancement, out of scope here.
- **Full resource-calendar-aware generation.** Planning's recurrence consults
resource work-intervals, flexible-resource flags and contract-end dates when
generating. The native re-fit uses the employee weekday pattern and skips
approved-leave days. This covers the real case; the heavy resource-calendar
engine is overkill at Entech's scale (8 slots). Documented simplification.
## 4. End-state architecture
- **`fusion_clock`** becomes self-contained and Community-installable. It owns:
native scheduling (existing), **roles**, **recurrence**, **publish/notify
(send)**, the **portal Schedule tab**, and **open / multi / overnight shift**
support. Manifest deps unchanged (`hr_attendance, hr, portal, mail, resource`)
— crucially **no `planning`**.
- **`fusion_planning`** is **retired** — its functionality is folded into
`fusion_clock`, then it is uninstalled on Entech.
- The attendance automation contract (`_get_fclk_day_plan`) is **unchanged** in
shape; new schedule capabilities resolve into the same single per-day
work-window it already returns (see §5.4).
## 5. Detailed design
### 5.1 New model: `fusion.clock.role` (copied from `planning.role`)
Near-verbatim copy of `planning/models/planning_role.py`:
- `name` — Char, required, translate
- `color` — Integer, default random 111
- `active` — Boolean, default True
- `sequence` — Integer
- `company_id` — Many2one `res.company` (added for fusion multi-company
consistency; Planning's role had none)
- Copy `_get_color_from_code(is_open_shift)` → returns the fullcalendar-compatible
hex used to colour shifts on the portal Schedule tab.
- Drop `resource_ids` m2m and `slot_properties_definition` (unused here).
### 5.2 `hr.employee` — native role fields
- `x_fclk_default_role_id` — Many2one `fusion.clock.role` (fills new shifts)
- `x_fclk_role_ids` — Many2many `fusion.clock.role` (allowed roles)
(Migrated from `default_planning_role_id` / `planning_role_ids`.)
**Employee Roles editor** — port `fusion_planning/views/hr_employee_role_views.xml`
to the native fields; reparent the menu from `planning.planning_menu_settings`
to a fusion_clock config menu; gate with `group_fusion_clock_manager`.
### 5.3 `fusion.clock.shift` (existing template) — additions
- `role_id` — Many2one `fusion.clock.role` (a template can carry a default role)
### 5.4 `fusion.clock.schedule` (existing per-day) — additions
**Part A additions** (ship with the core dependency removal; the
`UNIQUE(employee_id, schedule_date)` one-shift/day constraint is **kept**):
- `role_id` — Many2one `fusion.clock.role`; default from `shift_id.role_id` or
`employee_id.x_fclk_default_role_id`. Drives portal colour/label.
- `recurrence_id` — Many2one `fusion.clock.schedule.recurrence` (set when a rule
generated the row); `ondelete='set null'`.
In Part A the attendance contract `_get_fclk_day_plan` is **completely
unchanged** (one posted row per employee per day, exactly as today).
**Part B additions** (parity for currently-unused Planning capabilities; built
after A — see §10):
- `is_open` — Boolean; an **open / unassigned** shift available for self-assign.
- `crosses_midnight` — Boolean (overnight support).
- **Constraint changes:** replace the hard `UNIQUE(employee_id, schedule_date)`
with a **partial unique** that still forbids accidental duplicate assigned
rows while allowing intentional multiple shifts/day. Exact predicate finalised
in the plan; use `models.Constraint` / `models.UniqueIndex` per Odoo-19 rules.
`employee_id` becomes **not required** *only* when `is_open = True` (enforced by
a Python `@api.constrains`).
- **Overnight:** relax `_check_schedule_times` to permit `end_time <= start_time`
as crossing midnight (set `crosses_midnight`); update `_compute_planned_hours`
(`(24 - start) + end - break`) and `_get_fclk_scheduled_times` (out datetime is
next day).
**The attendance contract stays single-window in Part B too.**
`_get_fclk_day_plan(date)` still returns one plan per employee per day:
- 0 assigned rows → not scheduled (unchanged).
- 1 assigned row → that row (unchanged).
- N assigned rows for the day → resolve to one work-window = earliest start →
latest end across that day's assigned shifts; break = sum of breaks. This keeps
penalties/overtime/absence math **unchanged in shape** while letting managers
schedule split shifts and employees see each shift on the portal.
- `is_open` rows never feed any employee's plan until self-assigned.
This is the key safety property: **multi-shift / overnight / open-shift live in
the scheduling + UI + portal layers; the money-critical attendance layer keeps
its existing one-window contract.**
### 5.5 New model: `fusion.clock.schedule.recurrence` (design copied from `planning.recurrency`)
Fields (copied semantics):
- `repeat_interval` — Integer, default 1, `CHECK(repeat_interval >= 1)`
- `repeat_unit` — Selection day/week/month/year, default week
- `repeat_type` — Selection forever/until/x_times, default forever
- `repeat_until` — Date (required when `repeat_type='until'`)
- `repeat_number` — Integer (`>= 0`)
- `last_generated_date` — Date, readonly
- `company_id` — Many2one res.company
- `schedule_ids` — One2many `fusion.clock.schedule`
**Generation** (`_generate(stop_date=False)`), re-fit of `_repeat_slot` onto the
per-day model — much simpler (no resource-interval/DST math):
- Seed = the schedule entry the rule was created from (employee, weekday,
start/end/break/role).
- Emit per-day `fusion.clock.schedule` rows at the cadence (`repeat_interval` ×
`repeat_unit`) up to a horizon = `min(repeat_until, today + company.fclk_planning_generation_months, repeat_number cap)`.
- **Skip** dates the employee has an approved `fusion.clock.leave.request`
(the "resource-calendar-aware" simplification).
- Generated rows are created in **draft** (must be posted/published to drive
automation), carrying `recurrence_id`.
- Idempotent via `last_generated_date` (never regenerate past rows).
- `_stop(from_date)` deletes future **draft** rows of the rule (copy of
Planning's `_delete_slot`); posted rows are kept.
**Cron** `_cron_generate_recurring_schedules` (copy of Planning's
`_cron_schedule_next` shape) rolls the horizon forward. Odoo-19: no `numbercall`;
`active=True` recurring cron.
### 5.6 Manager UI — native OWL planner extensions
The existing weekly planner (`fusion_clock_shift_planner.js/xml` + controller
`shift_planner.py`) gains, alongside its current toolbar (Prev/This/Next week,
Copy Previous Week, Export XLSX, Save, Post Schedule):
- **Role** shown/edited per cell (colour chip from `role_id._get_color_from_code`).
- **Repeat…** control in the cell editor → creates a `fusion.clock.schedule.recurrence`
for that cell and generates rows; a backend list view manages/stops recurrences.
- **Publish & Notify** (generalises the existing `post_week`): pick a date range
(default current week) + optional employee subset + optional message → posts
matching draft rows and emails each affected employee their posted shifts for
the range (see §5.8).
- **Open shifts lane** + **bulk apply** ("Apply Also To" replacement): create an
open shift, or apply one cell's shift to several selected employees in one go.
All planner endpoints stay gated by `group_fusion_clock_manager` (unchanged).
### 5.7 Portal — fold the Schedule tab into `fusion_clock`
- Move the controller `/my/clock/schedule` into `fusion_clock`
(`controllers/portal_clock.py` or a new `portal_schedule.py`), reading **only**
`fusion.clock.schedule` (drop the `planning.slot` branch). Role colour/label
come from `role_id`.
- Move the template `fusion_planning.portal_schedule_page`
`fusion_clock.portal_schedule_page`.
- Add the **Schedule** nav button **inline** in each fusion_clock portal page's
`.fclk-nav-bar` (clock, timesheets, reports, payslips list, payslip detail),
replacing `fusion_planning`'s cross-module xpath inherits. Keep the
`.fclk-nav-bar` structure stable (no shared-template refactor — see the known
Odoo-19 xpath-inheritor gotcha).
- **Self-assign / unassign** of open shifts on the portal (respect a company
"days before shift" setting, mirroring Planning's `allow_self_unassign`).
### 5.8 Mail templates (copied, reworded)
Port Planning's "send schedule" templates (`planning/data/mail_template_data.xml`)
as the basis for fusion_clock's publish/notify email; reword for the Fusion
portal link. The native `fusion.clock.schedule.fclk_email_posted_week()` is
generalised to `fclk_email_posted_range(employee, start, end)`.
Follow Odoo-19 mail.template rules (no `url_encode` in QWeb; `ctx` is
`env.context`).
### 5.9 Security & menus
- `fusion.clock.role` + `fusion.clock.schedule.recurrence``ir.model.access.csv`
(manager write, user read as needed) + appropriate `ir.rule`s.
- Employee Roles editor menu + a Recurrences menu under the fusion_clock
configuration menu, gated `group_fusion_clock_manager`.
## 6. Data migration & retirement
### 6.1 Migration (in `fusion_clock`, guarded, idempotent)
A post-migration step (new module version) that runs only where Planning data
exists (`if 'planning.role' in env` / `'planning.slot' in env`):
1. **Roles:** each `planning.role` → find-or-create `fusion.clock.role`
(name + color). Build an id map.
2. **Employee roles:** `default_planning_role_id``x_fclk_default_role_id`;
`planning_role_ids``x_fclk_role_ids` (via the map).
3. **Slots:** each `planning.slot``fusion.clock.schedule`:
`resource_id`→employee, local date + local float start/end (employee tz),
break derived from span vs `allocated_hours`, `role_id` via map,
`state = posted` if published else draft. Unusual slots (overnight / multi /
open) handled by the §5.4 rules; anything unexpected is logged, not dropped.
Idempotent via a one-time `ir.config_parameter` marker.
Volume is tiny (8 slots, 1 role) — fast and low-risk.
### 6.2 Retire `fusion_planning`
After `fusion_clock` provides the Schedule tab + roles + migration, **uninstall
`fusion_planning`** (its portal templates, nav xpath-inherits and the
`x_fc_additional_resource_ids` m2m are removed). fusion_clock now owns the
Schedule tab inline. **Optionally** uninstall `planning` / `web_gantt` afterwards
(separate, gated cleanup — destructive, so done last and only on sign-off).
### 6.3 Community-install guarantee
After the change, `docker exec odoo-modsdev-app odoo -d fusion-dev -u fusion_clock`
must install on **Community** with no `planning` present (the migration is
guarded; no runtime code references `planning.*`). Add this as a smoke check.
## 7. Entech rollout (gated, revert-on-failure)
1. **Backup** DB + module dir (outside the addons path).
2. **Clone-verify**: clone `admin` → upgrade `fusion_clock` (+migration) on the
clone → assert: 144 native rows intact, 8 slots + 1 role migrated, roles +
recurrence + portal Schedule render, attendance/penalty tests green.
3. **Prod upgrade** `fusion_clock` (stop → `-u` → start **only if RC==0 +
"Modules loaded"**, else restore backup, no restart). Clear asset bundle
attachments; restart.
4. **Uninstall `fusion_planning`**.
5. **Optional**: uninstall `planning` / `web_gantt` (final, on sign-off).
## 8. Feature-parity matrix
| Planning feature | Preserved as |
|---|---|
| Assign shifts, weekly board | Native OWL planner (extended) |
| Gantt drag-drop timeline | ❗→ native weekly planner (Gantt can't be Community) |
| Shift templates | `fusion.clock.shift` (exists) + `role_id` |
| Roles + colour | `fusion.clock.role` (copied) + portal colour |
| Employee default/allowed roles | `x_fclk_default_role_id` / `x_fclk_role_ids` + editor |
| Recurrence (N day/week/month/year; forever/until/N-times) + cron | `fusion.clock.schedule.recurrence` (copied design) |
| Send / publish + email | Publish & Notify over a range (copied templates) |
| Multiple shifts/day | per-day model + single work-window contract (§5.4) |
| Overnight shifts | `crosses_midnight` (§5.4) |
| Open shifts + self-assign/unassign | `is_open` + portal self-assign |
| Auto-publish on create | native option (kept) |
| "Apply Also To" multi-employee | native bulk-apply |
| Allocated hours, portal My Schedule | `planned_hours`; Schedule tab folded in |
| Attendance/penalty/overtime/absence | **UNCHANGED** (per-day contract preserved) |
| Resource-calendar-aware generation | simplified: weekday pattern + skip leave |
## 9. Testing strategy
- **Unit:** role + colour; recurrence generation across each repeat_type/unit;
`_stop` deletes future drafts only; publish-range posts + emails; migration maps
roles/slots/employee-roles; overnight `planned_hours` + scheduled_times;
open-shift self-assign; multi-shift day-plan → correct single work-window.
- **Regression:** existing attendance / penalty / overtime / absence / dashboard
tests stay green (data source unchanged).
- **Community smoke:** install `fusion_clock` on Community `modsdev` (no planning).
- Odoo-19 test runner: `--http-port=0 --gevent-port=0`, `--test-tags /fusion_clock`.
## 10. Sequencing
**Decision: Part A and Part B ship together in one release** (full Planning
parity at once). The A/B labels below are **internal build phases** for the
implementation plan (so each gets its own review checkpoint), not separate
deployments.
- **Phase A:** drop the planning dep with parity for everything in real use —
roles, recurrence, publish/notify, portal fold-in, migration, retire
`fusion_planning`. (Per-day model keeps one-shift/day here.)
- **Phase B:** the remaining Planning capabilities — multi-shift/day, overnight,
open shifts + self-assign, "Apply Also To" bulk — using the safe single-window
attendance contract; isolated from the attendance engine.
Both phases are validated together before the single Entech rollout in §7.
## 11. Risks & open questions
- **Recurrence correctness** is new code — mitigated by isolation + unit tests
across every repeat_type/unit and the idempotent `last_generated_date` guard.
- **Multi-shift day-plan resolution** (§5.4) is the subtlest change; covered by a
dedicated test asserting the work-window and that penalties are unaffected.
- **Licensing:** the role model is generic, the recurrence loop is re-fit/original,
and mail templates are reworded — so near-verbatim Enterprise code is minimal.
Flag for the resale build; owner's call.
- **Resolved:** Parts A and B ship together in one release (§10).
## 12. Out of scope
- Drag-and-drop on the native planner (future enhancement).
- Full resource working-interval / flexible-resource / contract-end recurrence
math (deliberate simplification, §3).
- Uninstalling `planning` from Entech is optional and gated separately.