This commit is contained in:
gsinghpal
2026-03-16 08:14:56 -04:00
parent fdca9518ab
commit e56974d46f
196 changed files with 19739 additions and 3471 deletions

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import fusion_calendar_account
from . import fusion_calendar_event_link
from . import calendar_event
from . import res_users
from . import res_config_settings

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
import logging
import secrets
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class CalendarEvent(models.Model):
_inherit = 'calendar.event'
x_fc_source_account_id = fields.Many2one(
'fusion.calendar.account', string='Source Calendar Account',
help='The external calendar account that originally synced this event.',
)
x_fc_is_external = fields.Boolean(
string='External Event', compute='_compute_is_external', store=True,
)
x_fc_link_ids = fields.One2many(
'fusion.calendar.event.link', 'x_fc_event_id', string='External Links',
)
x_fc_manage_token = fields.Char(
string='Manage Token', index=True, copy=False,
help='Random token allowing public visitors to manage their booking.',
)
x_fc_client_email = fields.Char(string='Client Email')
x_fc_client_phone = fields.Char(string='Client Phone')
x_fc_address_lat = fields.Float(string='Location Latitude', digits=(10, 7))
x_fc_address_lng = fields.Float(string='Location Longitude', digits=(10, 7))
x_fc_travel_minutes_before = fields.Integer(
string='Travel Time Before (min)',
help='Estimated travel time to reach this appointment from the previous one.',
)
x_fc_is_travel_block = fields.Boolean(
string='Travel Block',
help='Auto-generated travel time placeholder event.',
)
@api.depends('x_fc_source_account_id')
def _compute_is_external(self):
for event in self:
event.x_fc_is_external = bool(event.x_fc_source_account_id)
def _skip_fc_sync(self):
"""Check if Fusion Schedule should skip syncing (native Odoo sync active)."""
ctx = self.env.context
return ctx.get('no_calendar_sync') or ctx.get('dont_notify')
def unlink(self):
"""On delete, also remove from all linked external calendars."""
if not self._skip_fc_sync():
for event in self:
if not event.x_fc_link_ids:
continue
for link in event.x_fc_link_ids:
account = link.x_fc_account_id
if not account.x_fc_active or not account.sudo().x_fc_rtoken:
continue
try:
token = account._get_valid_token()
if not token:
continue
if account.x_fc_provider == 'google':
account._google_delete_event(link.x_fc_external_id, token)
elif account.x_fc_provider == 'microsoft':
account._microsoft_delete_event(link.x_fc_external_id, token)
except Exception as e:
_logger.warning(
"Failed to delete external event %s from account %s: %s",
link.x_fc_external_id, account.id, e,
)
return super().unlink()
def write(self, vals):
"""On update, push changes to all linked external calendars."""
res = super().write(vals)
if self._skip_fc_sync():
return res
sync_fields = {'name', 'description', 'location', 'start', 'stop', 'allday',
'start_date', 'stop_date', 'privacy', 'show_as', 'active'}
if not sync_fields.intersection(vals.keys()):
return res
for event in self:
if not event.x_fc_link_ids:
continue
for link in event.x_fc_link_ids:
account = link.x_fc_account_id
if not account.x_fc_active or account.x_fc_sync_status != 'active':
continue
try:
account._sync_push_event(event)
except Exception as e:
_logger.warning(
"Failed to push event update %s to account %s: %s",
event.id, account.id, e,
)
return res

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class FusionCalendarEventLink(models.Model):
_name = 'fusion.calendar.event.link'
_description = 'Calendar Event to External Account Link'
_order = 'x_fc_last_synced desc'
x_fc_event_id = fields.Many2one(
'calendar.event', string='Calendar Event',
required=True, ondelete='cascade', index=True,
)
x_fc_account_id = fields.Many2one(
'fusion.calendar.account', string='Calendar Account',
required=True, ondelete='cascade', index=True,
)
x_fc_external_id = fields.Char(
string='External Event ID', required=True, index=True,
)
x_fc_universal_id = fields.Char(
string='Universal Event ID (iCalUID)', index=True,
)
x_fc_last_synced = fields.Datetime(string='Last Synced')
x_fc_sync_direction = fields.Selection([
('pull', 'Pulled from External'),
('push', 'Pushed to External'),
('both', 'Bidirectional'),
], string='Sync Direction', default='pull')
_unique_account_external = models.Constraint(
'UNIQUE(x_fc_account_id, x_fc_external_id)',
'An external event can only be linked once per account.',
)

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
# Google OAuth — dedicated keys with fallback to Odoo defaults
x_fc_google_client_id = fields.Char(
string='Google Client ID',
config_parameter='fusion_schedule_google_client_id',
)
x_fc_google_client_secret = fields.Char(
string='Google Client Secret',
config_parameter='fusion_schedule_google_client_secret',
)
x_fc_google_has_fallback = fields.Boolean(
string='Google Using Odoo Default',
compute='_compute_google_has_fallback',
)
# Microsoft OAuth — dedicated keys with fallback to Odoo defaults
x_fc_microsoft_client_id = fields.Char(
string='Microsoft Client ID',
config_parameter='fusion_schedule_microsoft_client_id',
)
x_fc_microsoft_client_secret = fields.Char(
string='Microsoft Client Secret',
config_parameter='fusion_schedule_microsoft_client_secret',
)
x_fc_microsoft_has_fallback = fields.Boolean(
string='Microsoft Using Odoo Default',
compute='_compute_microsoft_has_fallback',
)
# Sync settings
x_fc_sync_interval_minutes = fields.Integer(
string='Sync Interval (minutes)',
config_parameter='fusion_schedule_sync_interval',
default=5,
)
# Schedule defaults
x_fc_default_work_start = fields.Float(
string='Default Work Start',
config_parameter='fusion_schedule.default_work_start',
default=9.0,
)
x_fc_default_work_end = fields.Float(
string='Default Work End',
config_parameter='fusion_schedule.default_work_end',
default=17.0,
)
x_fc_default_break_start = fields.Float(
string='Default Break Start',
config_parameter='fusion_schedule.default_break_start',
default=12.0,
)
x_fc_default_break_duration = fields.Float(
string='Default Break Duration',
config_parameter='fusion_schedule.default_break_duration',
default=0.5,
)
x_fc_default_travel_buffer = fields.Integer(
string='Default Travel Buffer (min)',
config_parameter='fusion_schedule.default_travel_buffer',
default=30,
)
@api.depends('x_fc_google_client_id')
def _compute_google_has_fallback(self):
ICP = self.env['ir.config_parameter'].sudo()
odoo_default = ICP.get_param('google_calendar_client_id', '')
for rec in self:
rec.x_fc_google_has_fallback = bool(not rec.x_fc_google_client_id and odoo_default)
@api.depends('x_fc_microsoft_client_id')
def _compute_microsoft_has_fallback(self):
ICP = self.env['ir.config_parameter'].sudo()
odoo_default = ICP.get_param('microsoft_calendar_client_id', '')
for rec in self:
rec.x_fc_microsoft_has_fallback = bool(not rec.x_fc_microsoft_client_id and odoo_default)

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
import re
import uuid
from odoo import api, fields, models
class ResUsers(models.Model):
_inherit = 'res.users'
x_fc_calendar_account_ids = fields.One2many(
'fusion.calendar.account', 'x_fc_user_id',
string='Connected Calendar Accounts',
)
x_fc_schedule_slug = fields.Char(
string='Booking URL Slug',
help='Unique slug for your public booking page, e.g. /schedule/john-doe',
copy=False,
)
x_fc_booking_enabled = fields.Boolean(
string='Public Booking Enabled',
default=False,
help='Allow external visitors to book your time via a public link.',
)
x_fc_work_start = fields.Float(
string='Work Day Start', default=9.0,
help='Start of your work day as decimal hours (e.g. 9.0 = 9:00 AM)',
)
x_fc_work_end = fields.Float(
string='Work Day End', default=17.0,
help='End of your work day as decimal hours (e.g. 17.0 = 5:00 PM)',
)
x_fc_break_start = fields.Float(
string='Break Start', default=12.0,
help='Fixed break/lunch start time (e.g. 12.0 = 12:00 PM)',
)
x_fc_break_duration = fields.Float(
string='Break Duration (hours)', default=0.5,
help='Break duration in hours (e.g. 0.5 = 30 minutes)',
)
x_fc_travel_buffer = fields.Integer(
string='Travel Buffer (minutes)', default=30,
help='Minimum travel buffer between appointments in minutes',
)
x_fc_home_address = fields.Char(
string='Base/Office Address',
help='Starting address for first appointment travel calculation',
)
x_fc_home_lat = fields.Float(string='Base Latitude', digits=(10, 7))
x_fc_home_lng = fields.Float(string='Base Longitude', digits=(10, 7))
_unique_schedule_slug = models.Constraint(
'UNIQUE(x_fc_schedule_slug)',
'This booking URL slug is already taken. Please choose a different one.',
)
@api.model_create_multi
def create(self, vals_list):
users = super().create(vals_list)
for user in users:
if not user.x_fc_schedule_slug:
user.x_fc_schedule_slug = user._generate_schedule_slug()
return users
def _generate_schedule_slug(self):
"""Generate a URL-safe slug from the user's name."""
self.ensure_one()
name = self.name or 'user'
# Normalize: lowercase, replace spaces with hyphens, remove special chars
slug = re.sub(r'[^a-z0-9\-]', '', name.lower().replace(' ', '-'))
slug = re.sub(r'-+', '-', slug).strip('-')
if not slug:
slug = 'user'
# Add short unique suffix
suffix = uuid.uuid4().hex[:4]
return '%s-%s' % (slug, suffix)