update
This commit is contained in:
7
fusion_schedule/models/__init__.py
Normal file
7
fusion_schedule/models/__init__.py
Normal 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
|
||||
101
fusion_schedule/models/calendar_event.py
Normal file
101
fusion_schedule/models/calendar_event.py
Normal 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
|
||||
1169
fusion_schedule/models/fusion_calendar_account.py
Normal file
1169
fusion_schedule/models/fusion_calendar_account.py
Normal file
File diff suppressed because it is too large
Load Diff
35
fusion_schedule/models/fusion_calendar_event_link.py
Normal file
35
fusion_schedule/models/fusion_calendar_event_link.py
Normal 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.',
|
||||
)
|
||||
83
fusion_schedule/models/res_config_settings.py
Normal file
83
fusion_schedule/models/res_config_settings.py
Normal 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)
|
||||
77
fusion_schedule/models/res_users.py
Normal file
77
fusion_schedule/models/res_users.py
Normal 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)
|
||||
Reference in New Issue
Block a user