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>
18 KiB
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:
- The Gantt can't come to Community anyway.
web_ganttis Enterprise. Vendoringplanning.slot's datetime model still leaves us building a non-Gantt UI — so wholesale vendoring buys the heavy data model but not the UI. - 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()offfusion.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. - Reuse is still honoured. We copy the parts that copy cleanly
(
planning.rolenear-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_clockbecomes 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 noplanning.fusion_planningis retired — its functionality is folded intofusion_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, translatecolor— Integer, default random 1–11active— Boolean, default Truesequence— Integercompany_id— Many2oneres.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_idsm2m andslot_properties_definition(unused here).
5.2 hr.employee — native role fields
x_fclk_default_role_id— Many2onefusion.clock.role(fills new shifts)x_fclk_role_ids— Many2manyfusion.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— Many2onefusion.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— Many2onefusion.clock.role; default fromshift_id.role_idoremployee_id.x_fclk_default_role_id. Drives portal colour/label.recurrence_id— Many2onefusion.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; usemodels.Constraint/models.UniqueIndexper Odoo-19 rules.employee_idbecomes not required only whenis_open = True(enforced by a Python@api.constrains). - Overnight: relax
_check_schedule_timesto permitend_time <= start_timeas crossing midnight (setcrosses_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_openrows 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 weekrepeat_type— Selection forever/until/x_times, default foreverrepeat_until— Date (required whenrepeat_type='until')repeat_number— Integer (>= 0)last_generated_date— Date, readonlycompany_id— Many2one res.companyschedule_ids— One2manyfusion.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.schedulerows 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.recurrencefor 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/scheduleintofusion_clock(controllers/portal_clock.pyor a newportal_schedule.py), reading onlyfusion.clock.schedule(drop theplanning.slotbranch). Role colour/label come fromrole_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), replacingfusion_planning's cross-module xpath inherits. Keep the.fclk-nav-barstructure 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) + appropriateir.rules.- 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):
- Roles: each
planning.role→ find-or-createfusion.clock.role(name + color). Build an id map. - Employee roles:
default_planning_role_id→x_fclk_default_role_id;planning_role_ids→x_fclk_role_ids(via the map). - Slots: each
planning.slot→fusion.clock.schedule:resource_id→employee, local date + local float start/end (employee tz), break derived from span vsallocated_hours,role_idvia map,state = postedif published else draft. Unusual slots (overnight / multi / open) handled by the §5.4 rules; anything unexpected is logged, not dropped. Idempotent via a one-timeir.config_parametermarker.
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)
- Backup DB + module dir (outside the addons path).
- Clone-verify: clone
admin→ upgradefusion_clock(+migration) on the clone → assert: 144 native rows intact, 8 slots + 1 role migrated, roles + recurrence + portal Schedule render, attendance/penalty tests green. - Prod upgrade
fusion_clock(stop →-u→ start only if RC==0 + "Modules loaded", else restore backup, no restart). Clear asset bundle attachments; restart. - Uninstall
fusion_planning. - 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;
_stopdeletes future drafts only; publish-range posts + emails; migration maps roles/slots/employee-roles; overnightplanned_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_clockon Communitymodsdev(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_dateguard. - 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
planningfrom Entech is optional and gated separately.