changes
This commit is contained in:
@@ -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', '?')
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user