This commit is contained in:
gsinghpal
2026-02-23 00:32:20 -05:00
parent d6bac8e623
commit e8e554de95
549 changed files with 1330 additions and 124935 deletions

View File

@@ -2,13 +2,18 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import base64
import re
import time
import logging
from datetime import datetime, timedelta
from odoo import api, fields, models, _
_logger = logging.getLogger(__name__)
RC_RATE_LIMIT_DELAY = 6
class FusionFaxRC(models.Model):
"""Extend fusion.fax with contact matching, smart buttons,
@@ -186,3 +191,201 @@ class FusionFaxRC(models.Model):
'target': 'new',
'context': ctx,
}
# ------------------------------------------------------------------
# Historical fax import (via rc.config OAuth)
# ------------------------------------------------------------------
@api.model
def _run_historical_fax_import(self):
"""Background job: import up to 12 months of faxes in monthly chunks."""
config = self.env['rc.config']._get_active_config()
if not config:
_logger.warning("RC Fax Historical Import: No connected config.")
return
try:
config._ensure_token()
except Exception:
_logger.exception("RC Fax Historical Import: Token invalid.")
return
ICP = self.env['ir.config_parameter'].sudo()
now = datetime.utcnow()
total_imported = 0
total_skipped = 0
for months_back in range(12, 0, -1):
chunk_start = now - timedelta(days=months_back * 30)
chunk_end = now - timedelta(days=(months_back - 1) * 30)
chunk_num = 13 - months_back
date_from = chunk_start.strftime('%Y-%m-%dT%H:%M:%S.000Z')
date_to = chunk_end.strftime('%Y-%m-%dT%H:%M:%S.000Z')
chunk_key = f'fusion_rc.fax_import_done_{chunk_start.strftime("%Y%m")}'
if ICP.get_param(chunk_key, ''):
_logger.info(
"RC Fax Import [%d/12]: %s to %s -- already done, skipping.",
chunk_num, date_from[:10], date_to[:10],
)
total_skipped += 1
continue
_logger.info(
"RC Fax Import [%d/12]: %s to %s ...",
chunk_num, date_from[:10], date_to[:10],
)
try:
chunk_count = self._sync_faxes_from_rc(config, date_from, date_to)
ICP.set_param(chunk_key, 'done')
total_imported += chunk_count
_logger.info(
"RC Fax Import [%d/12]: imported %d faxes.",
chunk_num, chunk_count,
)
except Exception:
_logger.exception(
"RC Fax Import [%d/12]: chunk failed, will retry next run.",
chunk_num,
)
_logger.info(
"RC Fax Historical Import complete: %d imported, %d chunks skipped.",
total_imported, total_skipped,
)
def _sync_faxes_from_rc(self, config, date_from, date_to):
"""Fetch faxes (inbound + outbound) from RC message-store via OAuth."""
imported = 0
for direction in ('Inbound', 'Outbound'):
params = {
'messageType': 'Fax',
'direction': direction,
'dateFrom': date_from,
'dateTo': date_to,
'perPage': 100,
}
endpoint = '/restapi/v1.0/account/~/extension/~/message-store'
while endpoint:
time.sleep(RC_RATE_LIMIT_DELAY)
resp = config._api_request('GET', endpoint, params=params)
data = resp.json() if hasattr(resp, 'json') else resp
records = data.get('records', []) if isinstance(data, dict) else []
for msg in records:
msg_id = str(msg.get('id', ''))
if not msg_id:
continue
if self.search_count([('ringcentral_message_id', '=', msg_id)]):
continue
self._import_fax_from_rc(msg, config, direction.lower())
imported += 1
params = None
nav = data.get('navigation', {}) if isinstance(data, dict) else {}
next_page = nav.get('nextPage', {}) if isinstance(nav, dict) else {}
next_uri = next_page.get('uri', '') if isinstance(next_page, dict) else ''
endpoint = next_uri or None
return imported
def _import_fax_from_rc(self, msg, config, direction):
"""Import a single fax message from RingCentral API response."""
try:
msg_id = str(msg.get('id', ''))
from_info = msg.get('from', {}) or {}
to_info = msg.get('to', [{}])
if isinstance(to_info, list) and to_info:
to_info = to_info[0]
elif not isinstance(to_info, dict):
to_info = {}
sender = from_info.get('phoneNumber', '')
recipient = to_info.get('phoneNumber', '')
creation_time = msg.get('creationTime', '')
read_status = msg.get('readStatus', '')
page_count = msg.get('faxPageCount', 0)
attachments = msg.get('attachments', [])
received_dt = False
if creation_time:
try:
clean = creation_time.replace('Z', '+00:00')
received_dt = datetime.fromisoformat(clean).strftime(
'%Y-%m-%d %H:%M:%S'
)
except (ValueError, AttributeError):
pass
fax_number = recipient if direction == 'outbound' else sender
search_number = sender if direction == 'inbound' else recipient
partner = self._match_fax_partner(
fax_number, search_number, direction
)
document_lines = []
for att in attachments:
att_uri = att.get('uri', '')
att_type = att.get('contentType', '')
if not att_uri or 'text' in (att_type or ''):
continue
try:
time.sleep(RC_RATE_LIMIT_DELAY)
resp = config._api_request('GET', att_uri)
pdf_content = resp.content if hasattr(resp, 'content') else resp
if not pdf_content:
continue
file_ext = 'pdf' if 'pdf' in (att_type or '') else 'bin'
file_name = (
f'FAX_{"IN" if direction == "inbound" else "OUT"}'
f'_{msg_id}.{file_ext}'
)
ir_att = self.env['ir.attachment'].sudo().create({
'name': file_name,
'type': 'binary',
'datas': base64.b64encode(
pdf_content
if isinstance(pdf_content, bytes)
else pdf_content.encode()
),
'mimetype': att_type or 'application/pdf',
'res_model': 'fusion.fax',
})
document_lines.append((0, 0, {
'sequence': 10,
'attachment_id': ir_att.id,
}))
except Exception:
_logger.exception(
"RC Fax Import: Failed to download attachment for %s",
msg_id,
)
state = 'received' if direction == 'inbound' else 'sent'
self.sudo().create({
'direction': direction,
'state': state,
'fax_number': fax_number or 'Unknown',
'sender_number': sender if direction == 'inbound' else False,
'partner_id': partner.id if partner else False,
'ringcentral_message_id': msg_id,
'received_date': received_dt if direction == 'inbound' else False,
'sent_date': received_dt if direction == 'outbound' else False,
'page_count': page_count or 0,
'rc_read_status': read_status,
'document_ids': document_lines,
})
except Exception:
_logger.exception(
"RC Fax Import: Failed to import fax %s", msg.get('id', '?')
)

View File

@@ -467,6 +467,40 @@ class RcConfig(models.Model):
'info',
)
def action_import_historical_faxes(self):
"""Trigger historical fax import in the background via cron."""
self.ensure_one()
self._ensure_token()
cron = self.env.ref(
'fusion_ringcentral.cron_rc_historical_fax_import',
raise_if_not_found=False,
)
if cron:
cron.sudo().write({
'active': True,
'nextcall': fields.Datetime.now(),
})
else:
self.env['ir.cron'].sudo().create({
'name': 'RingCentral: Historical Fax Import',
'cron_name': 'RingCentral: Historical Fax Import',
'model_id': self.env['ir.model']._get('fusion.fax').id,
'state': 'code',
'code': 'model._run_historical_fax_import()',
'interval_number': 9999,
'interval_type': 'days',
'nextcall': fields.Datetime.now(),
'active': True,
})
return self._notify(
'Fax Import Started',
'Historical fax import (12 months) is running in the background. '
'Check Fusion Connect > Faxes in a few minutes.',
'info',
)
def action_backfill_voicemail_media(self):
"""Re-download audio and transcriptions for voicemails missing them."""
self.ensure_one()