Files
Odoo-Modules/fusion_schedule/CODE_MAP.md
gsinghpal ba7c028c30 CHANGES
2026-06-04 09:49:51 -04:00

387 lines
25 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.
# fusion_schedule — CODE MAP (where-is-what index)
> Precise symbol-level index for the whole module. Companion to `CLAUDE.md` (which is the
> narrative/guidance doc). **This file = "where is X".** Line numbers are exact at the time of
> writing (2026-06-03); re-grep `def `/`fields.`/`@http.route`/`<template id=` if they drift.
> Audit findings live in `CLAUDE.md §16` and in Supabase `fusionapps.issues`
> (project **Fusion Schedule** = `576de219-57e6-4596-8c8c-0c093e4cb54a`).
## 0. File tree (sizes approximate, by cat -n)
```
fusion_schedule/
├── __manifest__.py 61 deps, data load order, assets (v19.0.2.1.0)
├── __init__.py 3 → controllers, models
├── controllers/
│ ├── __init__.py 2 → portal_schedule
│ └── portal_schedule.py ~1607 PortalSchedule(CustomerPortal): 23 routes + helpers
├── models/
│ ├── __init__.py 6 load order (see below)
│ ├── fusion_calendar_account.py ~1191 sync engine + OAuth + cron (THE core)
│ ├── fusion_calendar_event_link.py 30 Odoo↔external join table
│ ├── calendar_event.py 89 inherit: sync fields + write/unlink push
│ ├── res_users.py 69 inherit: slug + work prefs + auto-slug create()
│ └── res_config_settings.py 74 inherit: OAuth creds + sync interval + defaults
├── data/
│ ├── ir_cron_data.xml 13 5-min sync cron
│ ├── mail_template_data.xml 155 booking confirmation email
│ └── appointment_invite_data.xml 10 default share invite (noupdate)
├── security/
│ ├── security.xml 17 2 record rules
│ └── ir.model.access.csv 5 4 ACL rows
├── views/
│ ├── fusion_calendar_account_views.xml 64 backend list/form/action/menu
│ ├── res_config_settings_views.xml 148 Settings app block
│ ├── portal_schedule_tile.xml 25 tile into fusion_portal home
│ ├── portal_schedule.xml 833 portal_schedule_page + portal_schedule_book
│ └── public_booking.xml 586 public_booking_page + public_manage_page (inline JS)
├── static/src/
│ ├── css/portal_schedule.css 48 responsive helpers only
│ ├── js/portal_schedule_booking.js ~575 booking form (authenticated)
│ ├── js/portal_schedule_accounts.js ~489 dashboard modals/toasts/optimize
│ └── views/fusion_calendar_controller.{js,xml} 68/44 backend AttendeeCalendarController patch
├── utils/__init__.py 1 empty placeholder
└── graphify-out/ — Cursor artifact, NOT loaded by Odoo
```
Model load order (`models/__init__.py`): `fusion_calendar_account → fusion_calendar_event_link
→ calendar_event → res_users → res_config_settings`.
---
## 1. Models
### 1.1 `fusion.calendar.account` — `models/fusion_calendar_account.py`
`_order = 'x_fc_provider, x_fc_email'`. Module constants (top of file): `TIMEOUT=20` (14),
`MAX_THROTTLE_RETRIES=3` (15), `DEFAULT_RETRY_SECONDS=10` (16); Google endpoints 1923,
Microsoft endpoints 2634.
**Fields**
| line | field | type / notes |
|---|---|---|
| 42 | `x_fc_user_id` | m2o res.users · required · cascade · default=current user · index |
| 46 | `x_fc_provider` | sel google/microsoft · required |
| 50 | `x_fc_email` | char |
| 51 | `x_fc_name` | char · compute `_compute_name` · store |
| 52 | `x_fc_active` | bool · default True |
| 55 | `x_fc_rtoken` | char · **groups=base.group_system** |
| 56 | `x_fc_token` | char · **group_system** |
| 57 | `x_fc_token_validity` | datetime · **group_system** |
| 60 | `x_fc_sync_token` | char · **group_system** (delta/sync token) |
| 61 | `x_fc_calendar_id` | char · default `'primary'` |
| 62 | `x_fc_last_sync` | datetime |
| 63 | `x_fc_sync_status` | sel active/error/paused · default active |
| 68 | `x_fc_error_message` | text |
| 71 | `x_fc_link_ids` | o2m → fusion.calendar.event.link |
**Methods**
| line | method | purpose |
|---|---|---|
| 76 | `_compute_name` | "Provider — email" label |
| 85/92/99/106 | `_get_{google,microsoft}_client_{id,secret}` | creds: `fusion_schedule_*` ICP → native `*_calendar_*` ICP fallback |
| 117 | `_get_valid_token` | return token, refresh if <1 min to expiry |
| 130 | `_refresh_token` | dispatch to provider refresh; on 400/401 mark error + clear |
| 149 / 170 | `_refresh_google_token` / `_refresh_microsoft_token` | OAuth refresh (MS may rotate rtoken) |
| 200 / 213 | `_exchange_{google,microsoft}_code` | code→tokens (called from callback) |
| 232 / 243 | `_fetch_{google,microsoft}_email` | `@api.model` · whoami email |
| 258 | `_sync_pull` | entry: dispatch pull per provider, catch+record errors |
| 293 | `_sync_pull_google` | events.list paging; 410→drop token+full resync; window 14/+30d |
| 362 | `_google_request_with_retry` | GET w/ 429/503 + connection retry |
| 389 | `_silent_ctx` | context flags that suppress mail + re-push (load-bearing) |
| 401 | `_find_existing_event` | dedup by name+start+stop (incl. archived) |
| 419 | `_upsert_event_link` | create/update the join row |
| 447 | `_process_google_event` | upsert one Google event (3-tier dedup) |
| 503 | `_google_event_to_odoo_vals` | ⚠ uses `astimezone(None)` — server-tz bug (CLAUDE §16.2) |
| 550 | `_sync_pull_microsoft` | Graph `calendarView/delta`; page cap 2000/5000 |
| 642 | `_microsoft_request_with_retry` | GET w/ retry |
| 671 | `_process_microsoft_event` | upsert one MS event; `@removed=changed``'skipped'` (don't archive) |
| 738 | `_microsoft_event_to_odoo_vals` | MS dict→Odoo vals |
| 798 | `_fetch_microsoft_event_subject` | fallback fetch when delta omits subject |
| 821 | `_sync_push_event` | push one Odoo event (insert/patch per provider) |
| 870/884/896 | `_google_{insert,patch,delete}_event` | Google write API |
| 908/924/938 | `_microsoft_{insert,patch,delete}_event` | Graph write API |
| 953 / 977 | `_odoo_event_to_{google,microsoft}` | Odoo→external format |
| 1022 | `_cross_calendar_push` | push **unlinked Odoo-native** events to **first** account (CLAUDE §16.4) |
| 1066 | `get_user_accounts_status` | `@api.model` **[backend RPC]** — account chips |
| 1081 | `sync_current_user` | `@api.model` **[backend RPC]** — "Sync now" (commits per account) |
| 1116 | `_cron_sync_all_accounts` | `@api.model` **[cron]** — sync all, then cross-push per multi-acct user |
| 1161 | `action_disconnect` | delete pushed external events, unlink, pause |
### 1.2 `fusion.calendar.event.link` — `models/fusion_calendar_event_link.py`
`_order = 'x_fc_last_synced desc'`. Fields: `x_fc_event_id` (11, m2o calendar.event, cascade),
`x_fc_account_id` (15, m2o account, cascade), `x_fc_external_id` (19, req, index),
`x_fc_universal_id` (22, iCalUID, index), `x_fc_last_synced` (25), `x_fc_sync_direction`
(26, pull/push/both). **Constraint** `_unique_account_external` = `UNIQUE(x_fc_account_id,
x_fc_external_id)` (32).
### 1.3 `calendar.event` (inherit) — `models/calendar_event.py`
**Sole extender of `calendar.event` in the whole repo.** Fields: `x_fc_source_account_id`
(14), `x_fc_is_external` (18, compute+store), `x_fc_link_ids` (21), `x_fc_manage_token`
(24, index, copy=False), `x_fc_client_email` (28), `x_fc_client_phone` (29),
`x_fc_address_lat` (30), `x_fc_address_lng` (31), `x_fc_travel_minutes_before` (32),
`x_fc_is_travel_block` (36). Methods: `_compute_is_external` (42), `_skip_fc_sync` (46),
`unlink` (51, deletes from external), `write` (76, pushes to external) — both gated by
`_skip_fc_sync()` + presence of links. ⚠ external HTTP is synchronous (CLAUDE §16.7).
### 1.4 `res.users` (inherit) — `models/res_users.py`
Fields: `x_fc_calendar_account_ids` (12), `x_fc_schedule_slug` (16), `x_fc_booking_enabled`
(21), `x_fc_work_start` (26), `x_fc_work_end` (30), `x_fc_break_start` (34),
`x_fc_break_duration` (38), `x_fc_travel_buffer` (42), `x_fc_home_address` (46),
`x_fc_home_lat` (50), `x_fc_home_lng` (51). **Constraint** `_unique_schedule_slug` =
`UNIQUE(x_fc_schedule_slug)` (53). Methods: `create` (59, `@api.model_create_multi`,
auto-slug — ⚠ collision risk CLAUDE §16.5), `_generate_schedule_slug` (66).
### 1.5 `res.config.settings` (inherit) — `models/res_config_settings.py`
Fields (12): `x_fc_google_client_id` (10), `_secret` (14), `_has_fallback` (18);
`x_fc_microsoft_client_id` (24), `_secret` (28), `_has_fallback` (32);
`x_fc_sync_interval_minutes` (38, **not wired to cron**); `x_fc_default_work_start` (45),
`_work_end` (50), `_break_start` (55), `_break_duration` (60), `_travel_buffer` (65).
Methods: `_compute_google_has_fallback` (72), `_compute_microsoft_has_fallback` (79).
---
## 2. Controller — `controllers/portal_schedule.py` (`PortalSchedule(CustomerPortal)`)
**Helper methods**
| line | method | purpose |
|---|---|---|
| 30 | `_get_schedule_values` | portal gradient (fusion_claims params) + maps key |
| 43 | `_get_user_timezone` | → `_resolve_timezone(env.user)` |
| 46 | `_resolve_timezone` | user.tz → `tz` cookie → company cal → UTC |
| 69 | `_get_appointment_types` | types where current user is staff |
| 75 | `_get_user_prefs` | per-user prefs w/ company-default fallback |
| 101 | `_get_maps_api_key` | `fusion.api.service``fusion_claims.google_maps_api_key` |
| 114 | `_call_ai` | `fusion.api.service.call_openai` → direct OpenAI HTTP |
| 147 | `_get_travel_time` | Google Distance Matrix (min) |
| 178 | `_geocode_address` | Google Geocoding (lat,lng) |
| 200 | `_create_travel_blocks` | insert "Travel to …" placeholder events |
| 425 | `_format_hour` | staticmethod · 13.5 → "1:30 PM" |
| 435 | `_generate_available_slots` | **shared slot core**; emits UTC `datetime` (line 520) |
| 825 | `_get_event_by_token` | manage-token lookup (len==32) |
| 932 | `_build_schedule_context` | AI prompt context builder |
| 1336 | `_find_recently_connected_account` | OAuth callback resilience |
**Routes** (23 total)
| line | verb | path | auth | handler |
|---|---|---|---|---|
| 288 | http | `/my/schedule` | user | `schedule_page` |
| 363 | jsonrpc | `/my/schedule/preferences` | user | `schedule_save_preferences` |
| 397 | http | `/my/schedule/book` | user | `schedule_book` |
| 530 | jsonrpc | `/my/schedule/available-slots` | user | `schedule_available_slots` |
| 560 | jsonrpc | `/my/schedule/week-events` | user | `schedule_week_events` |
| 630 | http POST | `/my/schedule/book/submit` | user | `schedule_book_submit` ✅ tz-correct |
| 777 | jsonrpc | `/my/schedule/event/cancel` | user | `schedule_event_cancel` |
| 792 | jsonrpc | `/my/schedule/event/reschedule` | user | `schedule_event_reschedule` ⚠ tz-bug |
| 834 | http | `/schedule/manage/<token>` | public | `public_manage_page` |
| 860 | http POST | `/schedule/manage/<token>/cancel` | public | `public_manage_cancel` |
| 876 | http POST | `/schedule/manage/<token>/reschedule` | public | `public_manage_reschedule` ⚠ tz-bug |
| 907 | jsonrpc | `/schedule/manage/<token>/available-slots` | public (csrf=False) | `public_manage_slots` |
| 982 | jsonrpc | `/my/schedule/ai/suggest` | user | `schedule_ai_suggest` |
| 1093 | jsonrpc | `/my/schedule/ai/optimize` | user | `schedule_ai_optimize` |
| 1155 | http | `/my/schedule/connect/google` | user | `connect_google` |
| 1192 | http | `/my/schedule/connect/microsoft` | user | `connect_microsoft` |
| 1230 | http | `/my/schedule/oauth/callback` | user | `oauth_callback` |
| 1356 | jsonrpc | `/my/schedule/disconnect` | user | `schedule_disconnect` |
| 1370 | jsonrpc | `/my/schedule/sync-now` | user | `schedule_sync_now` |
| 1398 | http | `/schedule/<slug>` | public | `public_booking_page` |
| 1431 | jsonrpc | `/schedule/<slug>/available-slots` | public (csrf=False) | `public_available_slots` |
| 1465 | http POST | `/schedule/<slug>/book` | public (csrf) | `public_book_submit` ⚠ tz-bug + no re-validate |
| 1602 | jsonrpc | `/my/schedule/toggle-booking` | user | `schedule_toggle_booking` |
---
## 3. Frontend JS
### 3.1 backend patch — `static/src/views/fusion_calendar_controller.js`
`patch(AttendeeCalendarController.prototype)`: `setup`, getters `fusionAccounts` /
`fusionSyncing`, `_loadFusionAccounts` (→ `get_user_accounts_status`), `onFusionSyncNow`
(→ `sync_current_user`). Template `.xml` hides `#header_synchronization_settings`, injects
account chips + sync button + cog→`/my/schedule`.
### 3.2 `static/src/js/portal_schedule_booking.js` (authenticated book page)
`setTzCookie` (4), `getAppointmentTypeId` (35), `truncate` (41), `formatDateStr` (46),
`addDays` (53), `getMonday` (59), `selectDay` (67), `fetchWeekEvents` (77) →
`/my/schedule/week-events`, `navigateWeek` (120), `renderWeekCalendar` (140), `fetchSlots`
(260) → `/my/schedule/available-slots`, `renderGroup` (319, nested), `fetchAiSuggestions`
(364) → `/my/schedule/ai/suggest`, `setupAddressAutocomplete` (516, Google Places).
### 3.3 `static/src/js/portal_schedule_accounts.js` (dashboard)
Utils: `localDateStr` (4), `setTzCookie` (12), `jsonRpc` (21), `fusionConfirm` (30),
`fusionToast` (87, builds `#fusionToastLive` — template `#fusionToast` is dead),
`closeRescheduleModal` (304), `closeOptimizeModal` (474). Event bindings: disconnect (112)
`/disconnect`, sync (141) → `/sync-now`, share (160), save-prefs (186) → `/preferences`,
cancel (231) → `/event/cancel`, reschedule open (274) + date-change (321) +
confirm (375) → `/event/reschedule`, optimize (413) → `/ai/optimize`.
> Public pages (`public_booking_page`, `public_manage_page`) carry their **own inline
> `<script>`** in `public_booking.xml` (a 2nd copy of slot-render + Places autocomplete +
> reschedule) — they do **not** use the files above. See CLAUDE §9.1.
### 3.4 DOM-id contract (templates ↔ JS)
Book page: `bookingDate`, `appointmentTypeSelect`, `slotsContainer/slotsGrid/slotsLoading/
noSlots`, `slotDatetime`, `slotDuration`, `weekCalendar{Container,Loading,Grid,Header,Body,
Empty,Nav}`, `btnPrevWeek/btnNextWeek/weekRangeLabel`, `aiSuggest{Section,Loading,Grid}`,
`btnAiSuggest`, `clientStreet/City/Province/Postal/Lat/Lng`, `btnSubmitBooking`.
Dashboard: `fusionConfirmModal`(+Title/Message/Ok), `rescheduleModal`(+Date/SlotsContainer/
SlotsGrid/EventId/SlotDatetime/EventDuration/EventName/AppTypeId/btnConfirmReschedule),
`optimizeModal`(+Loading/Result/CurrentTravel/NewTravel/Savings/ScheduleList/Error),
`schedulePrefsForm`/`btnSavePrefs`/`prefsSavedMsg`, `btnOptimizeSchedule`, `.js-*` classes.
Public: `publicBookingDate`, `publicSlots*`, `publicSlotDatetime/Duration`, `publicBtnSubmit`,
`publicAppointmentType`, `publicClient*`, `publicReschedule*`.
---
## 4. Templates / data / security / settings
**Templates**
| id | file:line | base layout |
|---|---|---|
| `portal_schedule_page` | portal_schedule.xml:6 | `portal.portal_layout` |
| `portal_schedule_book` | portal_schedule.xml:605 | `portal.portal_layout` |
| `public_booking_page` | public_booking.xml:6 | `website.layout` (+inline JS) |
| `public_manage_page` | public_booking.xml:345 | `website.layout` (+inline JS) |
| `portal_my_home_schedule` | portal_schedule_tile.xml:5 | inherit `fusion_portal.portal_my_home_authorizer` |
| `FusionCalendarController` | fusion_calendar_controller.xml | t-inherit `calendar.AttendeeCalendarController` |
| `res_config_settings_view_form_fusion_schedule` | res_config_settings_views.xml:4 | inherit `base.res_config_settings_view_form` |
**Backend views**`fusion_calendar_account_views.xml`: list (5), form (24),
`action_fusion_calendar_account` (56), `menu_fusion_calendar_account` (63, under
`base.menu_custom`).
**Data**`ir_cron_fusion_calendar_sync` (ir_cron_data.xml:4, 5 min, `_cron_sync_all_accounts`);
`fusion_schedule_booking_confirmation` (mail_template_data.xml:4, model `calendar.event`,
NOT noupdate); `default_appointment_invite` (appointment_invite_data.xml:8, noupdate,
short_code `book-appointment`, empty types).
**Security** — rules `fusion_calendar_account_user_rule` (security.xml:5),
`fusion_calendar_event_link_user_rule` (security.xml:13); ACL: 4 rows in
`ir.model.access.csv` (account: CRUD user / none public; link: CRU user / full system).
---
## 5. Config parameters (`ir.config_parameter`)
**Owned**`fusion_schedule_google_client_id`, `_google_client_secret`,
`fusion_schedule_microsoft_client_id`, `_microsoft_client_secret`,
`fusion_schedule_sync_interval`; `fusion_schedule.default_work_start` / `_work_end` /
`_break_start` / `_break_duration` / `_travel_buffer`.
**Fallback (not owned)**`google_calendar_client_id` / `_secret`,
`microsoft_calendar_client_id` / `_secret`, `web.base.url`; and the fusion_claims namespace
`fusion_claims.portal_gradient_start/_mid/_end`, `fusion_claims.google_maps_api_key`,
`fusion_claims.ai_api_key`.
---
## 6. External HTTP it talks to
- **Google** OAuth (`accounts.google.com/o/oauth2/auth`, `oauth2.googleapis.com/token`),
Calendar v3 (`googleapis.com/calendar/v3`), userinfo, Distance Matrix + Geocoding
(`maps.googleapis.com`). Scopes: `calendar` + `userinfo.email`.
- **Microsoft** OAuth (`login.microsoftonline.com/common/oauth2/v2.0/*`), Graph
(`graph.microsoft.com/v1.0``me/calendarView/delta`, `me/events`, `me`). Scopes:
`offline_access openid Calendars.ReadWrite User.Read`.
- **OpenAI** `api.openai.com/v1/chat/completions` (`gpt-4o-mini`) — fallback only.
---
## 7. Cross-module touchpoints (full detail in CLAUDE §4)
| direction | what | where |
|---|---|---|
| depends ↓ | `fusion_portal` (→ fusion_claims stack) | __manifest__.py:35 |
| inherit ↓ | `fusion_portal.portal_my_home_authorizer` | portal_schedule_tile.xml:6 |
| soft-call ↓ | `fusion.api.service` (fusion_api) | portal_schedule.py:104,117 |
| ICP read ↓ | `fusion_claims.{portal_gradient_*,google_maps_api_key,ai_api_key}` | portal_schedule.py:33-35,111,126 |
| cookie ← | `tz` set by `fusion_portal/.../timezone_detect.js` | portal_schedule.py:_resolve_timezone |
| shared table | `calendar.event` (also written by fusion_claims schedule wizard / appointment) | models/calendar_event.py |
| reverse | **none** (only fusion_repairs lists it as *deferred*) | — |
---
## 8. Audit cross-reference
**19 findings** logged → Supabase `fusionapps.issues`, project **Fusion Schedule**
(`576de219-57e6-4596-8c8c-0c093e4cb54a`), all `status='open'`. Detail + fixes in
`CLAUDE.md §16` (deep dives #1#6). Provenance: AI-generated (Cursor + Claude 4.5 Opus) —
Odoo-19 syntax clean, bugs are semantic. Headlines: (a) timezone double-conversion on `schedule_event_reschedule` /
`public_book_submit` / `public_manage_reschedule` (slot string is UTC but they re-localize);
(b) the **sync-dedup cluster**`_find_existing_event` (`:401`) and the iCalUID lookup
(`:482`/`:715`) are unscoped by user, so same-titled / shared-invite events **merge across
different users** and resurrect archived ones; (c) public booking mutates an existing
`res.partner` by attacker-supplied email.
---
## 9. Consumed contracts — the OTHER side of each cross-module link (integration boundary)
### 9.1 `fusion.api.service` broker (`fusion_api`, **not a manifest dep**)
`request.env['fusion.api.service']`**`KeyError` if `fusion_api` absent** (caught by
fusion_schedule's bare `except` → fallback). 7 models: `fusion.api.service` (AbstractModel,
broker), `fusion.api.{provider,key,consumer,access,usage,user.limit}` + `usage.daily`.
Public methods fusion_schedule uses: `get_api_key(provider_type, consumer, feature)`
`api_service.py:394`; `call_openai(consumer, feature, messages, model)``:278`. **Raises
`UserError` on 14 conditions** (no active provider `:62`; consumer disabled `:129`; access
disabled `:141`; monthly/daily budget `:157/167`; rpm/rpd `:185/194`; user blocked/budget/rpd
`:218/224/234`; no key `:81`; package missing `:280/335`; downstream API error `:319/381`) —
**any** of these (or KeyError) triggers fusion_schedule's ICP fallback. `provider_type` enum:
`openai, anthropic, google_maps, google_oauth, microsoft_oauth, twilio, custom`. Consumer
auto-registers when `fusion_api.auto_detect_consumers` (default True).
> ⚠ **`get_api_key` returns `key.api_key` (a `group_admin`-gated field) on a *non-sudo*
> recordset (`api_service.py:407`).** For a portal/public request (non-admin/public user) this
> likely raises `AccessError` → fusion_schedule's fallback fires **every time** → in practice
> the maps key for portal/public callers comes from `fusion_claims.google_maps_api_key`, not the
> broker. The broker path may effectively never succeed for raw-key access from the portal.
### 9.2 `portal_gradient` / `fc_gradient` / the tile target (`fusion_portal`)
- `portal_gradient` computed in `fusion_portal/controllers/portal_main.py:81-87` from
`fusion_claims.portal_gradient_{start,mid,end}` (defaults `#5ba848/#3a8fb7/#2e7aad`) — **only
set for portal personas** (`is_authorizer/is_sales_rep_portal/is_client_portal/is_technician_portal`).
fusion_schedule computes its **own identical copy** in `portal_schedule.py:33-36`, so its pages
don't need the controller — only the **tile** does (via `fc_gradient`, set at
`portal_templates.xml:10` = `portal_gradient or <default>`).
- **Tile xpath fragility:** the tiles grid is `<div class="row g-3 mb-4">` (`portal_templates.xml:52-295`);
the anchor is the `/my/funding-claims` card (`:277-294`). fusion_schedule's tile xpath
(`portal_schedule_tile.xml:8`) needs **both** the `/my/funding-claims` `<a>` and the exact
`row g-3 mb-4` class triple — change either and the tile **ParseErrors at install** of
fusion_schedule.
- **`tz` cookie** set by `fusion_portal/static/src/js/timezone_detect.js:25`: name `tz`, value
raw IANA (`America/Toronto`), `path=/ max-age=31536000 SameSite=Lax`. Read at
`portal_schedule.py:_resolve_timezone` (2nd priority after `user.tz`).
### 9.3 The borrowed `fusion_claims.*` params — ownership (defaults all match)
| ICP key | owning field | file:line | default |
|---|---|---|---|
| `fusion_claims.portal_gradient_start/_mid/_end` | `fc_portal_gradient_*` | `fusion_claims/.../res_config_settings.py:461-474` | `#5ba848/#3a8fb7/#2e7aad` |
| `fusion_claims.ai_api_key` | `fc_ai_api_key` | `fusion_claims/.../res_config_settings.py:355` | empty |
| `fusion_claims.google_maps_api_key` | `fc_google_maps_api_key` | **`fusion_tasks`/.../res_config_settings.py:12-16** | empty |
> ⚠ The maps key is **owned by `fusion_tasks`, not `fusion_claims`** (the `fusion_claims.*`
> prefix is kept for data continuity). Grepping `fusion_claims/` for it finds nothing. Both
> owners are transitive deps via fusion_portal, so the params are always present.
---
## 10. Sibling scheduling surfaces & how they interact with this module
**Baseline:** fusion_schedule is the **only** `calendar.event` extender; its `write/unlink`
push to external is gated by `_skip_fc_sync()` (context `no_calendar_sync`/`dont_notify`) +
presence of links, and `_cross_calendar_push` (cron) mirrors **unlinked Odoo-native** events
(1d…+90d, on the user's partner) to the **first** account **only if the user has >1 account**.
| Writer | what it creates | interaction with fusion_schedule |
|---|---|---|
| `fusion_claims` `schedule_assessment_wizard.py:186` | 1 `calendar.event`/manual schedule (assessor partner, optional email alarm), **plain create** | eligible for cron mirror; **later edits fire the synchronous `write()` push** |
| `fusion_portal` `portal_assessment.py:1194` | 1 `calendar.event`/public booking (sales-rep partner; sets `accessibility.assessment.calendar_event_id`), **plain sudo create** | same as above |
| `fusion_tasks` `technician_task.py:1572` (`_sync_calendar_event`) | **HIGH volume** — 1 event/task, re-synced on every schedule-field write/create; **writes with `silent_ctx` (`dont_notify=True`)** | synchronous push **suppressed**; external mirror deferred to the 5-min cron. **Protection hinges on `dont_notify` staying in `silent_ctx`** — drop it and every task edit becomes an inline Google/Outlook round-trip |
- **Reverse coupling:** `fusion_tasks` slot scheduler reads `calendar.event` for busy intervals
(`technician_task.py:495-540`) and **excludes its own task-linked events**, so
externally-synced calendar entries (pulled by fusion_schedule) correctly block technician
availability.
- **Repo sweep:** only these **4** modules touch `calendar.event`/appointments; **only
fusion_schedule uses Enterprise `appointment.*`** (the others create raw `calendar.event`).
`fusion_repairs` maintenance booking is still *planned*. Stale vendored copies of the task
engine exist under `Entech Plating/` and `fusion_plating/`**not** the canonical install
path; flag for cleanup.
- **Maps key consumers:** `fusion_tasks` travel-time (`_calculate_travel_time`) and
`fusion_claims` `google_address_autocomplete.js` both read `fusion_claims.google_maps_api_key`
(owned by fusion_tasks) — same key fusion_schedule falls back to.