docs(fusion_clock): implementation plan — remove Planning dependency
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,300 @@
|
|||||||
|
# Remove Odoo Planning Dependency — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use `- [ ]`.
|
||||||
|
|
||||||
|
**Goal:** Make `fusion_clock` fully Community-installable by re-fitting Odoo Planning's role + recurrence + send onto the native per-day `fusion.clock.schedule`, folding `fusion_planning` in, and retiring it — with full Planning feature parity.
|
||||||
|
|
||||||
|
**Architecture:** All new code lands in `fusion_clock` (deps stay `hr_attendance, hr, portal, mail, resource` — no `planning`). New native models `fusion.clock.role` and `fusion.clock.schedule.recurrence`; additive fields on `fusion.clock.schedule` / `fusion.clock.shift` / `hr.employee`. The attendance contract `_get_fclk_day_plan` keeps its one-window-per-day shape (multi/overnight/open resolve into it). A guarded, idempotent post-migration ports the live planning data, then `fusion_planning` is uninstalled.
|
||||||
|
|
||||||
|
**Tech Stack:** Odoo 19, Python, OWL, QWeb, xlsxwriter. Verify on Entech LXC 111 clone; deploy gated revert-on-failure.
|
||||||
|
|
||||||
|
**Reference (read before coding each piece):** Enterprise Planning source on Entech at `/mnt/extra-addons/_dependencies/planning/` (`models/planning_role.py`, `models/planning_recurrency.py`, `models/planning_planning.py`, `data/mail_template_data.xml`). Spec: `fusion_clock/docs/superpowers/specs/2026-06-04-remove-planning-dependency-design.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
**Create:**
|
||||||
|
- `fusion_clock/models/clock_role.py` — `fusion.clock.role`
|
||||||
|
- `fusion_clock/models/clock_recurrence.py` — `fusion.clock.schedule.recurrence` + generation engine
|
||||||
|
- `fusion_clock/views/clock_role_views.xml` — role list/form/action/menu + Employee Roles editor
|
||||||
|
- `fusion_clock/views/clock_recurrence_views.xml` — recurrence list/form/action/menu
|
||||||
|
- `fusion_clock/views/portal_schedule_templates.xml` — portal Schedule tab (folded from fusion_planning)
|
||||||
|
- `fusion_clock/controllers/portal_schedule.py` — `/my/clock/schedule` (+ self-assign endpoints)
|
||||||
|
- `fusion_clock/static/src/css/portal_schedule.css` — folded from fusion_planning
|
||||||
|
- `fusion_clock/data/clock_recurrence_cron.xml` — recurrence generation cron
|
||||||
|
- `fusion_clock/tests/test_role.py`, `test_recurrence.py`, `test_publish_range.py`, `test_open_shift.py`, `test_overnight.py`, `test_multishift_window.py`, `test_planning_migration.py`
|
||||||
|
- `fusion_clock/migrations/19.0.5.0.0/post-migrate.py` — planning→native data migration
|
||||||
|
|
||||||
|
**Modify:**
|
||||||
|
- `fusion_clock/models/__init__.py` — register `clock_role`, `clock_recurrence`
|
||||||
|
- `fusion_clock/models/hr_employee.py` — `x_fclk_default_role_id`, `x_fclk_role_ids`; `_get_fclk_day_plan` multi-window
|
||||||
|
- `fusion_clock/models/clock_shift.py` — `role_id`
|
||||||
|
- `fusion_clock/models/clock_schedule.py` — `role_id`, `recurrence_id`, `is_open`, `crosses_midnight`; overnight math; constraint relax; `fclk_email_posted_range`; recurrence helpers
|
||||||
|
- `fusion_clock/models/res_config_settings.py` + `res_company.py` — `fclk_planning_generation_months`, `fclk_self_unassign_days_before`
|
||||||
|
- `fusion_clock/controllers/shift_planner.py` — recurrence, publish-range, open-shift, bulk-apply endpoints
|
||||||
|
- `fusion_clock/static/src/js/fusion_clock_shift_planner.js` + `.xml` — role chip, Repeat dialog, Publish&Notify, open lane, bulk apply
|
||||||
|
- `fusion_clock/views/clock_shift_views.xml`, `clock_schedule_views.xml` — role fields; recurrence/open columns
|
||||||
|
- `fusion_clock/views/portal_*_templates.xml` (clock, timesheets, reports, payslip list+detail) — inline Schedule nav button
|
||||||
|
- `fusion_clock/data/mail_template_data.xml` — schedule publish email
|
||||||
|
- `fusion_clock/security/ir.model.access.csv` — role + recurrence ACLs
|
||||||
|
- `fusion_clock/views/clock_menus.xml` — Roles + Recurrences config menus
|
||||||
|
- `fusion_clock/__manifest__.py` — version `19.0.5.0.0`; new data/asset files
|
||||||
|
|
||||||
|
**Retire (on deploy):** uninstall `fusion_planning`; optional uninstall `planning`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE A — Core parity (roles, recurrence, send, portal, migration)
|
||||||
|
|
||||||
|
### Task A1: `fusion.clock.role` model
|
||||||
|
**Files:** Create `models/clock_role.py`; Modify `models/__init__.py`, `security/ir.model.access.csv`.
|
||||||
|
|
||||||
|
- [ ] Write `test_role.py`: create role, default color in 1..11, `_get_color_from_code(False)` returns `#`-hex.
|
||||||
|
- [ ] Implement (copied from `planning_role.py`, trimmed):
|
||||||
|
```python
|
||||||
|
from random import randint
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
class FusionClockRole(models.Model):
|
||||||
|
_name = 'fusion.clock.role'
|
||||||
|
_description = 'Clock Shift Role'
|
||||||
|
_order = 'sequence, name'
|
||||||
|
|
||||||
|
def _get_default_color(self):
|
||||||
|
return randint(1, 11)
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True)
|
||||||
|
color = fields.Integer(default=_get_default_color)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||||
|
|
||||||
|
_COLOR_HEX = {0:'#008784',1:'#EE4B39',2:'#F29648',3:'#F4C609',4:'#55B7EA',
|
||||||
|
5:'#71405B',6:'#E86869',7:'#008784',8:'#267283',9:'#BF1255',
|
||||||
|
10:'#2BAF73',11:'#8754B0'}
|
||||||
|
|
||||||
|
def _get_color_from_code(self, is_open_shift=False):
|
||||||
|
self.ensure_one()
|
||||||
|
hexv = self._COLOR_HEX.get(self.color, '#008784')
|
||||||
|
return hexv + ('80' if is_open_shift else '')
|
||||||
|
```
|
||||||
|
- [ ] Register in `__init__.py` (add `from . import clock_role` before `clock_shift`).
|
||||||
|
- [ ] ACL rows: `model_fusion_clock_role` → user read; manager RWCU; portal read.
|
||||||
|
- [ ] Run `test_role` on Entech clone; commit.
|
||||||
|
|
||||||
|
### Task A2: Employee role fields + Roles editor view/menu
|
||||||
|
**Files:** Modify `models/hr_employee.py`, `views/clock_role_views.xml` (create), `views/clock_menus.xml`, `security/ir.model.access.csv`.
|
||||||
|
|
||||||
|
- [ ] Add to `hr.employee`:
|
||||||
|
```python
|
||||||
|
x_fclk_default_role_id = fields.Many2one('fusion.clock.role', string='Default Shift Role')
|
||||||
|
x_fclk_role_ids = fields.Many2many('fusion.clock.role', 'fclk_employee_role_rel',
|
||||||
|
'employee_id', 'role_id', string='Allowed Shift Roles')
|
||||||
|
```
|
||||||
|
- [ ] `clock_role_views.xml`: role list/form/action + `action_fclk_employee_role_editor` (editable hr.employee list with `x_fclk_default_role_id` + `x_fclk_role_ids`, copied from `fusion_planning/views/hr_employee_role_views.xml`, native fields, `multi_edit="1"`).
|
||||||
|
- [ ] Menus under fusion_clock config: "Roles" (role action) + "Employee Roles" (editor), `groups="group_fusion_clock_manager"`.
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
### Task A3: `role_id` on shift template + schedule + default
|
||||||
|
**Files:** Modify `models/clock_shift.py`, `models/clock_schedule.py`, `views/clock_shift_views.xml`, `views/clock_schedule_views.xml`.
|
||||||
|
|
||||||
|
- [ ] `clock_shift.py`: `role_id = fields.Many2one('fusion.clock.role')`.
|
||||||
|
- [ ] `clock_schedule.py`: `role_id = fields.Many2one('fusion.clock.role')`; default in `fclk_apply_planner_cell` vals from `shift.role_id` or `employee.x_fclk_default_role_id`; include `role_id`+`role_color` in `fclk_cell_payload`.
|
||||||
|
- [ ] Add `role_id` to shift form/list + schedule list/form.
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
### Task A4: `fusion.clock.schedule.recurrence` model + engine + cron
|
||||||
|
**Files:** Create `models/clock_recurrence.py`, `data/clock_recurrence_cron.xml`; Modify `models/__init__.py`, `models/clock_schedule.py`, `models/res_company.py`, ACL csv, manifest.
|
||||||
|
|
||||||
|
- [ ] Write `test_recurrence.py`: weekly interval=1 forever generates rows on same weekday up to horizon; `until` caps at date; `x_times` caps at N; `_stop` deletes future drafts only, keeps posted; leave days skipped.
|
||||||
|
- [ ] `res_company.py`: `fclk_planning_generation_months = fields.Integer(default=6)`.
|
||||||
|
- [ ] Implement model (design copied from `planning_recurrency.py`, re-fit to per-day):
|
||||||
|
```python
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
class FusionClockScheduleRecurrence(models.Model):
|
||||||
|
_name = 'fusion.clock.schedule.recurrence'
|
||||||
|
_description = 'Clock Schedule Recurrence'
|
||||||
|
|
||||||
|
schedule_ids = fields.One2many('fusion.clock.schedule', 'recurrence_id')
|
||||||
|
repeat_interval = fields.Integer('Repeat Every', default=1, required=True)
|
||||||
|
repeat_unit = fields.Selection([('day','Days'),('week','Weeks'),('month','Months'),('year','Years')],
|
||||||
|
default='week', required=True)
|
||||||
|
repeat_type = fields.Selection([('forever','Forever'),('until','Until'),('x_times','Number of Repetitions')],
|
||||||
|
default='forever', required=True)
|
||||||
|
repeat_until = fields.Date('Repeat Until')
|
||||||
|
repeat_number = fields.Integer('Repetitions')
|
||||||
|
last_generated_date = fields.Date(readonly=True)
|
||||||
|
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
|
||||||
|
|
||||||
|
_check_interval_pos = models.Constraint('CHECK(repeat_interval >= 1)', 'Repeat every must be >= 1.')
|
||||||
|
|
||||||
|
@api.constrains('repeat_type', 'repeat_until')
|
||||||
|
def _check_until(self):
|
||||||
|
for r in self:
|
||||||
|
if r.repeat_type == 'until' and not r.repeat_until:
|
||||||
|
raise ValidationError(_('Set an end date for "Until" recurrences.'))
|
||||||
|
|
||||||
|
def _delta(self, n):
|
||||||
|
unit = {'day':'days','week':'weeks','month':'months','year':'years'}[self.repeat_unit]
|
||||||
|
return relativedelta(**{unit: self.repeat_interval * n})
|
||||||
|
|
||||||
|
def _horizon(self):
|
||||||
|
months = int(self.env['ir.config_parameter'].sudo().get_param('fusion_clock.generation_months')
|
||||||
|
or self.company_id.fclk_planning_generation_months or 6)
|
||||||
|
return fields.Date.today() + relativedelta(months=months)
|
||||||
|
|
||||||
|
def _generate(self, stop_date=False):
|
||||||
|
Schedule = self.env['fusion.clock.schedule'].sudo()
|
||||||
|
for r in self:
|
||||||
|
seed = Schedule.search([('recurrence_id','=',r.id)], order='schedule_date desc', limit=1)
|
||||||
|
if not seed:
|
||||||
|
continue
|
||||||
|
limit = min([d for d in [r.repeat_until,
|
||||||
|
stop_date or r._horizon()] if d])
|
||||||
|
existing = Schedule.search_count([('recurrence_id','=',r.id)])
|
||||||
|
vals_list, last = [], seed.schedule_date
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
nxt = seed.schedule_date + r._delta(i); i += 1
|
||||||
|
if nxt > limit:
|
||||||
|
break
|
||||||
|
if r.repeat_type == 'x_times' and existing + len(vals_list) >= r.repeat_number:
|
||||||
|
break
|
||||||
|
if Schedule.search_count([('recurrence_id','=',r.id),('schedule_date','=',nxt)]):
|
||||||
|
continue
|
||||||
|
if seed.employee_id and seed.employee_id._fclk_on_leave(nxt):
|
||||||
|
continue
|
||||||
|
vals_list.append({
|
||||||
|
'employee_id': seed.employee_id.id, 'schedule_date': nxt,
|
||||||
|
'shift_id': seed.shift_id.id or False, 'is_off': seed.is_off,
|
||||||
|
'start_time': seed.start_time, 'end_time': seed.end_time,
|
||||||
|
'break_minutes': seed.break_minutes, 'role_id': seed.role_id.id or False,
|
||||||
|
'recurrence_id': r.id, 'state': 'draft',
|
||||||
|
})
|
||||||
|
last = nxt
|
||||||
|
if vals_list:
|
||||||
|
Schedule.create(vals_list)
|
||||||
|
r.last_generated_date = last
|
||||||
|
|
||||||
|
def _stop(self, from_date):
|
||||||
|
self.env['fusion.clock.schedule'].sudo().search([
|
||||||
|
('recurrence_id','in', self.ids), ('schedule_date','>=', from_date),
|
||||||
|
('state','=','draft')]).unlink()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _cron_generate(self):
|
||||||
|
self.search([])._generate()
|
||||||
|
```
|
||||||
|
- [ ] `hr_employee.py`: add `_fclk_on_leave(date)` (True if an approved `fusion.clock.leave.request` covers date — read existing leave model first).
|
||||||
|
- [ ] Cron `data/clock_recurrence_cron.xml`: daily, `model._cron_generate()`, no `numbercall` (Odoo 19).
|
||||||
|
- [ ] ACL: recurrence manager RWCU, user read.
|
||||||
|
- [ ] Run `test_recurrence`; commit.
|
||||||
|
|
||||||
|
### Task A5: Recurrence on schedule + planner "Repeat…" wiring
|
||||||
|
**Files:** Modify `models/clock_schedule.py`, `controllers/shift_planner.py`, planner `.js`/`.xml`.
|
||||||
|
|
||||||
|
- [ ] `clock_schedule.py`: `recurrence_id = fields.Many2one('fusion.clock.schedule.recurrence', ondelete='set null')`; method `fclk_attach_recurrence(schedule, repeat_vals)` creating the rule, linking the seed, calling `_generate()`.
|
||||||
|
- [ ] Controller endpoint `/fusion_clock/shift_planner/set_recurrence` (manager-gated) → calls `fclk_attach_recurrence`; `/clear_recurrence` → `_stop(today)` + unlink rule.
|
||||||
|
- [ ] Planner cell editor: "Repeat…" button → small dialog (interval/unit/type/until/number) → POST; show a recurrence badge on recurring cells.
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
### Task A6: Publish & Notify over a range (generalise post_week)
|
||||||
|
**Files:** Modify `models/clock_schedule.py`, `controllers/shift_planner.py`, planner `.js`/`.xml`, `data/mail_template_data.xml`.
|
||||||
|
|
||||||
|
- [ ] `clock_schedule.py`: rename/extend `fclk_email_posted_week` → `fclk_email_posted_range(employee, start, end)` (keep a thin `_week` wrapper). Add `fclk_publish_range(employees, start, end, message=None)` posting drafts + emailing.
|
||||||
|
- [ ] Controller `/fusion_clock/shift_planner/publish_range` (range + optional employee_ids + message) → `fclk_publish_range`; keep `post_week` calling it for the visible week.
|
||||||
|
- [ ] Mail template `mail_template_fclk_schedule_published` (copy/reword from planning `data/mail_template_data.xml`; obey Odoo-19 mail rules: no `url_encode`).
|
||||||
|
- [ ] Planner: "Publish & Notify…" dialog (date range + message). Commit.
|
||||||
|
|
||||||
|
### Task A7: Portal Schedule tab folded into fusion_clock
|
||||||
|
**Files:** Create `controllers/portal_schedule.py`, `views/portal_schedule_templates.xml`, `static/src/css/portal_schedule.css`; Modify `controllers/__init__.py`, portal nav templates, manifest.
|
||||||
|
|
||||||
|
- [ ] Move controller from `fusion_planning/controllers/portal_schedule.py`; **delete the `planning.slot` branch**; read only `fusion.clock.schedule`; role colour from `role_id._get_color_from_code()`.
|
||||||
|
- [ ] Move template → `fusion_clock.portal_schedule_page`; move css.
|
||||||
|
- [ ] Inline a "Schedule" nav `<a href="/my/clock/schedule">` into each `.fclk-nav-bar` (clock, timesheets, reports, payslip list, payslip detail) between Timesheets and Reports. Keep `.fclk-nav-bar` structure stable.
|
||||||
|
- [ ] Manifest: add template + css asset. Commit.
|
||||||
|
|
||||||
|
### Task A8: planning → native data migration
|
||||||
|
**Files:** Create `migrations/19.0.5.0.0/post-migrate.py`.
|
||||||
|
|
||||||
|
- [ ] Guarded + idempotent (marker `fusion_clock.planning_migrated`):
|
||||||
|
- roles: `planning.role` → `fusion.clock.role` (name, color); build id map.
|
||||||
|
- employees: `default_planning_role_id`→`x_fclk_default_role_id`; `planning_role_ids`→`x_fclk_role_ids`.
|
||||||
|
- slots: `planning.slot` → `fusion.clock.schedule` (resource→employee, local date+float times, role via map, posted if published).
|
||||||
|
- log anything unusual (overnight/open/multi handled by Phase B rules).
|
||||||
|
- [ ] Write `test_planning_migration.py` (stub planning models or skip if absent — guard with `'planning.slot' in env`).
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE B — Multi-shift / overnight / open shifts / self-assign / bulk
|
||||||
|
|
||||||
|
### Task B1: schedule fields + constraint relax
|
||||||
|
**Files:** Modify `models/clock_schedule.py`.
|
||||||
|
- [ ] Add `is_open = fields.Boolean()`, `crosses_midnight = fields.Boolean(compute store)`.
|
||||||
|
- [ ] Make `employee_id` `required=False`; add `@api.constrains` requiring employee unless `is_open`.
|
||||||
|
- [ ] Replace `_employee_date_unique` Constraint with `models.UniqueIndex('(employee_id, schedule_date) WHERE employee_id IS NOT NULL AND recurrence_id IS NULL AND is_open = false')` — allow intentional multi via recurrence/open; finalise predicate so existing 144 single rows pass. Write `test_open_shift.py` first (open row needs no employee; two open rows same day allowed).
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
### Task B2: overnight math
|
||||||
|
**Files:** Modify `models/clock_schedule.py`, `models/hr_employee.py`.
|
||||||
|
- [ ] `test_overnight.py`: 22:00→06:00 with 30m break → 7.5h; scheduled out is next-day.
|
||||||
|
- [ ] `_compute_planned_hours`: if `end<=start` → `(24-start)+end-break/60`; set `crosses_midnight`.
|
||||||
|
- [ ] `_check_schedule_times`: allow `end<=start` (remove the overnight block) but keep break < shift length.
|
||||||
|
- [ ] `hr_employee._get_fclk_scheduled_times`: when crossing midnight, out datetime += 1 day.
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
### Task B3: multi-shift day-plan work-window
|
||||||
|
**Files:** Modify `models/hr_employee.py`.
|
||||||
|
- [ ] `test_multishift_window.py`: two posted shifts 08–12 and 13–17 → plan window 08–17, hours = sum worked; penalties unaffected (one window).
|
||||||
|
- [ ] `_get_fclk_day_plan`: search ALL posted assigned rows for the date; if >1, earliest start / latest end, summed breaks, summed hours; single row + none unchanged.
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
### Task B4: open shifts in planner
|
||||||
|
**Files:** Modify `controllers/shift_planner.py`, planner `.js`/`.xml`.
|
||||||
|
- [ ] `_load_week_data`: include an "Open Shifts" pseudo-row (is_open rows by day).
|
||||||
|
- [ ] Endpoints `/create_open_shift`, `/bulk_apply` (apply a cell to many employee_ids — replaces `x_fc_additional_resource_ids`).
|
||||||
|
- [ ] Planner UI: open-shift lane + "Apply to…" multi-select. Commit.
|
||||||
|
|
||||||
|
### Task B5: portal self-assign / unassign
|
||||||
|
**Files:** Modify `controllers/portal_schedule.py`, `views/portal_schedule_templates.xml`, `res_company.py`/`res_config_settings.py`.
|
||||||
|
- [ ] `res_company`: `fclk_self_unassign_days_before = fields.Integer(default=1)`.
|
||||||
|
- [ ] Portal endpoints `/my/clock/schedule/claim/<id>` (open→assign me) and `/unassign/<id>` (respect days-before).
|
||||||
|
- [ ] Template: show open shifts + Claim button; show Unassign on own upcoming shifts when allowed. Commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHASE C — Manifest, verify, deploy
|
||||||
|
|
||||||
|
### Task C1: manifest + config settings UI
|
||||||
|
- [ ] `__manifest__.py` → version `19.0.5.0.0`; add data files (`clock_role_views.xml`, `clock_recurrence_views.xml`, `clock_recurrence_cron.xml`, `portal_schedule_templates.xml`), asset `portal_schedule.css`; mail template already in `data/`.
|
||||||
|
- [ ] `res_config_settings.py` + view: expose `fclk_planning_generation_months`, `fclk_self_unassign_days_before` (Integer — config_parameter ok).
|
||||||
|
- [ ] Commit.
|
||||||
|
|
||||||
|
### Task C2: clone-verify on Entech
|
||||||
|
- [ ] Clone `admin` → `admin_fctest` (pg_dump|psql inside LXC 111).
|
||||||
|
- [ ] Stage branch `fusion_clock` into an isolated `_test` addons dir shadowing prod; `-u fusion_clock --stop-after-init --http-port=0 --gevent-port=0` on the clone; assert exit 0 + "Modules loaded".
|
||||||
|
- [ ] Run `--test-tags /fusion_clock` on the clone; assert green.
|
||||||
|
- [ ] `odoo shell` on clone: assert 144 schedule rows intact, 8 slots + 1 role migrated, portal `/my/clock/schedule` renders; `env.cr.rollback()`.
|
||||||
|
|
||||||
|
### Task C3: deploy to Entech prod (gated)
|
||||||
|
- [ ] Backup DB (`pg_dump -Fc`) + module dir copy OUTSIDE addons path.
|
||||||
|
- [ ] scp branch `fusion_clock` → pve-worker5 → `pct push` into `/mnt/extra-addons/custom/fusion_clock` (swap; keep backup).
|
||||||
|
- [ ] `systemctl stop odoo; runuser -u odoo -- odoo -c ... -d admin -u fusion_clock --stop-after-init --http-port=0 --gevent-port=0 --logfile=/tmp/up.log`. **Restart only if RC==0 + "Modules loaded"**, else restore backup, no restart.
|
||||||
|
- [ ] `DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';` then `systemctl start odoo`.
|
||||||
|
|
||||||
|
### Task C4: retire fusion_planning (+ optional planning)
|
||||||
|
- [ ] After prod `-u` healthy + migration verified: uninstall `fusion_planning` (Apps → uninstall, or `env['ir.module.module'].search([('name','=','fusion_planning')]).button_immediate_uninstall()` via shell with backup).
|
||||||
|
- [ ] Verify portal Schedule tab still present (now served by fusion_clock); attendance/penalty crons intact.
|
||||||
|
- [ ] Optional, last, gated: uninstall `planning`/`web_gantt` (destructive — only after migration confirmed). Leave if any doubt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review notes
|
||||||
|
- Spec coverage: roles (A1–A3), recurrence (A4–A5), send (A6), portal (A7), migration (A8), multi/overnight/open/self-assign/bulk (B1–B5), Community manifest + deploy (C). All §5–§7 items mapped.
|
||||||
|
- Risk: recurrence engine + multi-window are new — covered by `test_recurrence`, `test_multishift_window`, `test_overnight`.
|
||||||
|
- Verification is batch (Entech clone), not per-step (no local docker from Mac).
|
||||||
Reference in New Issue
Block a user