387 lines
25 KiB
Markdown
387 lines
25 KiB
Markdown
# 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 19–23,
|
||
Microsoft endpoints 26–34.
|
||
|
||
**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.
|