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

25 KiB
Raw Blame History

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.accountmodels/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

_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.servicefusion_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 viewsfusion_calendar_account_views.xml: list (5), form (24), action_fusion_calendar_account (56), menu_fusion_calendar_account (63, under base.menu_custom).

Datair_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)

Ownedfusion_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.0me/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.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.