This commit is contained in:
gsinghpal
2026-02-22 01:37:50 -05:00
parent 5200d5baf0
commit d6bac8e623
1550 changed files with 263540 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import fusion_fax
from . import fusion_fax_document
from . import dashboard
from . import res_config_settings
from . import res_partner
from . import sale_order
from . import account_move

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import api, fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
x_ff_fax_ids = fields.One2many(
'fusion.fax',
'account_move_id',
string='Faxes',
)
x_ff_fax_count = fields.Integer(
string='Fax Count',
compute='_compute_fax_count',
)
@api.depends('x_ff_fax_ids')
def _compute_fax_count(self):
for move in self:
move.x_ff_fax_count = len(move.x_ff_fax_ids)
def action_send_fax(self):
"""Open the Send Fax wizard pre-filled with this invoice."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Send Fax',
'res_model': 'fusion_faxes.send.fax.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'active_model': 'account.move',
'active_id': self.id,
},
}
def action_view_faxes(self):
"""Open fax history for this invoice."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Faxes',
'res_model': 'fusion.fax',
'view_mode': 'list,form',
'domain': [('account_move_id', '=', self.id)],
'context': {'default_account_move_id': self.id},
}

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import api, fields, models, _
import logging
_logger = logging.getLogger(__name__)
STATUS_DOMAINS = {
'all': [],
'sent': [('state', '=', 'sent')],
'failed': [('state', '=', 'failed')],
'draft': [('state', '=', 'draft')],
'received': [('state', '=', 'received')],
}
class FusionFaxDashboard(models.TransientModel):
_name = 'fusion.fax.dashboard'
_description = 'Fusion Fax Dashboard'
_rec_name = 'name'
name = fields.Char(default='Fax Dashboard', readonly=True)
# KPI stat fields
total_count = fields.Integer(compute='_compute_stats')
sent_count = fields.Integer(compute='_compute_stats')
received_count = fields.Integer(compute='_compute_stats')
failed_count = fields.Integer(compute='_compute_stats')
draft_count = fields.Integer(compute='_compute_stats')
# Recent faxes as a proper relational field for embedded list
recent_fax_ids = fields.Many2many(
'fusion.fax',
compute='_compute_recent_faxes',
)
def _compute_stats(self):
Fax = self.env['fusion.fax'].sudo()
for rec in self:
rec.total_count = Fax.search_count([])
rec.sent_count = Fax.search_count(STATUS_DOMAINS['sent'])
rec.received_count = Fax.search_count(STATUS_DOMAINS['received'])
rec.failed_count = Fax.search_count(STATUS_DOMAINS['failed'])
rec.draft_count = Fax.search_count(STATUS_DOMAINS['draft'])
def _compute_recent_faxes(self):
Fax = self.env['fusion.fax'].sudo()
for rec in self:
rec.recent_fax_ids = Fax.search([], order='create_date desc', limit=20)
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_open_all(self):
return self._open_fax_list('All Faxes', STATUS_DOMAINS['all'])
def action_open_sent(self):
return self._open_fax_list('Sent Faxes', STATUS_DOMAINS['sent'])
def action_open_received(self):
return self._open_fax_list('Received Faxes', STATUS_DOMAINS['received'])
def action_open_failed(self):
return self._open_fax_list('Failed Faxes', STATUS_DOMAINS['failed'])
def action_open_draft(self):
return self._open_fax_list('Draft Faxes', STATUS_DOMAINS['draft'])
def _open_fax_list(self, name, domain):
return {
'type': 'ir.actions.act_window',
'name': name,
'res_model': 'fusion.fax',
'view_mode': 'list,form',
'domain': domain,
}
def action_send_fax(self):
return {
'type': 'ir.actions.act_window',
'name': 'Send Fax',
'res_model': 'fusion_faxes.send.fax.wizard',
'view_mode': 'form',
'target': 'new',
}

View File

@@ -0,0 +1,581 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import base64
import json
import logging
from datetime import datetime, timedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from markupsafe import Markup
_logger = logging.getLogger(__name__)
class FusionFax(models.Model):
_name = 'fusion.fax'
_description = 'Fax Record'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc'
_rec_name = 'name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: _('New'),
)
direction = fields.Selection([
('outbound', 'Outbound'),
('inbound', 'Inbound'),
], string='Direction', default='outbound', required=True, tracking=True)
partner_id = fields.Many2one(
'res.partner',
string='Recipient',
tracking=True,
)
fax_number = fields.Char(
string='Fax Number',
required=True,
tracking=True,
)
state = fields.Selection([
('draft', 'Draft'),
('sending', 'Sending'),
('sent', 'Sent'),
('failed', 'Failed'),
('received', 'Received'),
], string='Status', default='draft', required=True, tracking=True)
# Inbound fax fields
sender_number = fields.Char(
string='Sender Number',
readonly=True,
)
received_date = fields.Datetime(
string='Received Date',
readonly=True,
)
rc_read_status = fields.Char(
string='Read Status',
readonly=True,
)
# Computed display fields for list views (work regardless of direction)
display_date = fields.Datetime(
string='Date',
compute='_compute_display_fields',
store=True,
)
display_number = fields.Char(
string='Fax Number',
compute='_compute_display_fields',
store=True,
)
cover_page_text = fields.Text(string='Cover Page Text')
document_ids = fields.One2many(
'fusion.fax.document',
'fax_id',
string='Documents',
)
document_count = fields.Integer(
compute='_compute_document_count',
)
# Keep for backwards compat with existing records
attachment_ids = fields.Many2many(
'ir.attachment',
'fusion_fax_attachment_rel',
'fax_id',
'attachment_id',
string='Attachments (Legacy)',
)
ringcentral_message_id = fields.Char(
string='RingCentral Message ID',
readonly=True,
copy=False,
)
sent_date = fields.Datetime(
string='Sent Date',
readonly=True,
copy=False,
)
sent_by_id = fields.Many2one(
'res.users',
string='Sent By',
readonly=True,
default=lambda self: self.env.user,
)
page_count = fields.Integer(
string='Pages',
readonly=True,
copy=False,
)
error_message = fields.Text(
string='Error Message',
readonly=True,
copy=False,
)
# Links to source documents
sale_order_id = fields.Many2one(
'sale.order',
string='Sale Order',
ondelete='set null',
tracking=True,
)
account_move_id = fields.Many2one(
'account.move',
string='Invoice',
ondelete='set null',
tracking=True,
)
def write(self, vals):
"""Post chatter message when a sale order or invoice is linked."""
old_so_ids = {rec.id: rec.sale_order_id.id for rec in self}
result = super().write(vals)
if 'sale_order_id' in vals:
for rec in self:
new_so = rec.sale_order_id
old_so_id = old_so_ids.get(rec.id)
if new_so and new_so.id != old_so_id:
rec._post_link_chatter_message(new_so)
# Also set the partner from the SO if not already matched
if not rec.partner_id and new_so.partner_id:
rec.partner_id = new_so.partner_id
return result
def _post_link_chatter_message(self, sale_order):
"""Post a message on the sale order when a fax is linked to it."""
self.ensure_one()
direction_label = 'Received' if self.direction == 'inbound' else 'Sent'
date_str = ''
if self.direction == 'inbound' and self.received_date:
date_str = self.received_date.strftime('%b %d, %Y %H:%M')
elif self.sent_date:
date_str = self.sent_date.strftime('%b %d, %Y %H:%M')
number = self.sender_number or self.fax_number or ''
body = Markup(
'<p><strong>Fax Linked</strong></p>'
'<p>%s fax <a href="/odoo/faxes/%s"><b>%s</b></a> has been linked to this order.</p>'
'<ul>'
'<li>Fax Number: %s</li>'
'<li>Date: %s</li>'
'<li>Pages: %s</li>'
'</ul>'
) % (direction_label, self.id, self.name, number,
date_str or '-', self.page_count or '-')
# Attach the fax documents to the chatter message
attachment_ids = self.document_ids.mapped('attachment_id').ids
sale_order.message_post(
body=body,
message_type='notification',
attachment_ids=attachment_ids,
)
@api.depends('document_ids')
def _compute_document_count(self):
for rec in self:
rec.document_count = len(rec.document_ids)
@api.depends('direction', 'sent_date', 'received_date', 'fax_number', 'sender_number')
def _compute_display_fields(self):
for rec in self:
if rec.direction == 'inbound':
rec.display_date = rec.received_date
rec.display_number = rec.sender_number or rec.fax_number
else:
rec.display_date = rec.sent_date
rec.display_number = rec.fax_number
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('fusion.fax') or _('New')
return super().create(vals_list)
# ------------------------------------------------------------------
# RingCentral SDK helpers
# ------------------------------------------------------------------
def _get_rc_sdk(self):
"""Initialize and authenticate the RingCentral SDK. Returns (sdk, platform) tuple."""
ICP = self.env['ir.config_parameter'].sudo()
enabled = ICP.get_param('fusion_faxes.ringcentral_enabled', 'False')
if enabled not in ('True', 'true', '1'):
raise UserError(_('RingCentral faxing is not enabled. Go to Settings > Fusion Faxes to enable it.'))
client_id = ICP.get_param('fusion_faxes.ringcentral_client_id', '')
client_secret = ICP.get_param('fusion_faxes.ringcentral_client_secret', '')
server_url = ICP.get_param('fusion_faxes.ringcentral_server_url', 'https://platform.ringcentral.com')
jwt_token = ICP.get_param('fusion_faxes.ringcentral_jwt_token', '')
if not all([client_id, client_secret, jwt_token]):
raise UserError(_(
'RingCentral credentials are not configured. '
'Go to Settings > Fusion Faxes and enter Client ID, Client Secret, and JWT Token.'
))
try:
from ringcentral import SDK
except ImportError:
raise UserError(_(
'The ringcentral Python package is not installed. '
'Run: pip install ringcentral'
))
sdk = SDK(client_id, client_secret, server_url)
platform = sdk.platform()
platform.login(jwt=jwt_token)
return sdk, platform
def _get_ordered_attachments(self):
"""Return attachments in the correct order: document_ids by sequence, or legacy attachment_ids."""
self.ensure_one()
if self.document_ids:
return self.document_ids.sorted('sequence').mapped('attachment_id')
return self.attachment_ids
def _send_fax(self):
"""Send this fax record via RingCentral API."""
self.ensure_one()
attachments = self._get_ordered_attachments()
if not attachments:
raise UserError(_('Please attach at least one document to send.'))
self.write({'state': 'sending', 'error_message': False})
try:
sdk, platform = self._get_rc_sdk()
# Use the SDK's multipart builder
builder = sdk.create_multipart_builder()
# Set the JSON body (metadata)
body = {
'to': [{'phoneNumber': self.fax_number}],
'faxResolution': 'High',
}
if self.cover_page_text:
body['coverPageText'] = self.cover_page_text
builder.set_body(body)
# Add document attachments in sequence order
for attachment in attachments:
file_content = base64.b64decode(attachment.datas)
builder.add((attachment.name, file_content))
# Build the request and send
request = builder.request('/restapi/v1.0/account/~/extension/~/fax')
response = platform.send_request(request)
result = response.json()
# Extract response fields
message_id = ''
page_count = 0
if hasattr(result, 'id'):
message_id = str(result.id)
elif isinstance(result, dict):
message_id = str(result.get('id', ''))
if hasattr(result, 'pageCount'):
page_count = result.pageCount
elif isinstance(result, dict):
page_count = result.get('pageCount', 0)
self.write({
'state': 'sent',
'ringcentral_message_id': message_id,
'sent_date': fields.Datetime.now(),
'sent_by_id': self.env.user.id,
'page_count': page_count,
})
# Post chatter message on linked documents
self._post_fax_chatter_message(success=True)
_logger.info("Fax %s sent successfully. RC Message ID: %s", self.name, message_id)
except UserError:
raise
except Exception as e:
error_msg = str(e)
self.write({
'state': 'failed',
'error_message': error_msg,
})
self._post_fax_chatter_message(success=False)
_logger.exception("Fax %s failed to send", self.name)
raise UserError(_('Fax sending failed: %s') % error_msg)
def _post_fax_chatter_message(self, success=True):
"""Post a chatter message on the linked sale order or invoice."""
self.ensure_one()
if success:
body = Markup(
'<p><strong>Fax Sent</strong></p>'
'<p>Fax <b>%s</b> sent successfully to <b>%s</b> (%s).</p>'
'<p>Pages: %s | RingCentral ID: %s</p>'
) % (self.name, self.partner_id.name, self.fax_number,
self.page_count or '-', self.ringcentral_message_id or '-')
else:
body = Markup(
'<p><strong>Fax Failed</strong></p>'
'<p>Fax <b>%s</b> to <b>%s</b> (%s) failed.</p>'
'<p>Error: %s</p>'
) % (self.name, self.partner_id.name, self.fax_number,
self.error_message or 'Unknown error')
if self.sale_order_id:
self.sale_order_id.message_post(body=body, message_type='notification')
if self.account_move_id:
self.account_move_id.message_post(body=body, message_type='notification')
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_send(self):
"""Button action to send this fax."""
self.ensure_one()
self._send_fax()
def action_retry(self):
"""Retry a failed fax."""
self.ensure_one()
if self.state != 'failed':
raise UserError(_('Only failed faxes can be retried.'))
self._send_fax()
def action_resend(self):
"""Resend a previously sent fax with all the same attachments."""
self.ensure_one()
if self.state != 'sent':
raise UserError(_('Only sent faxes can be resent.'))
self._send_fax()
def action_open_sale_order(self):
"""Open the linked sale order."""
self.ensure_one()
if not self.sale_order_id:
return
return {
'type': 'ir.actions.act_window',
'name': self.sale_order_id.name,
'res_model': 'sale.order',
'res_id': self.sale_order_id.id,
'view_mode': 'form',
}
def action_reset_to_draft(self):
"""Reset a failed fax back to draft."""
self.ensure_one()
if self.state not in ('failed',):
raise UserError(_('Only failed faxes can be reset to draft.'))
self.write({
'state': 'draft',
'error_message': False,
})
# ------------------------------------------------------------------
# Incoming fax polling
# ------------------------------------------------------------------
@api.model
def _cron_fetch_incoming_faxes(self):
"""Poll RingCentral for inbound faxes and create records."""
ICP = self.env['ir.config_parameter'].sudo()
enabled = ICP.get_param('fusion_faxes.ringcentral_enabled', 'False')
if enabled not in ('True', 'true', '1'):
return
client_id = ICP.get_param('fusion_faxes.ringcentral_client_id', '')
client_secret = ICP.get_param('fusion_faxes.ringcentral_client_secret', '')
server_url = ICP.get_param('fusion_faxes.ringcentral_server_url', 'https://platform.ringcentral.com')
jwt_token = ICP.get_param('fusion_faxes.ringcentral_jwt_token', '')
if not all([client_id, client_secret, jwt_token]):
_logger.warning("Fusion Faxes: RingCentral credentials not configured, skipping inbound poll.")
return
try:
from ringcentral import SDK
except ImportError:
_logger.error("Fusion Faxes: ringcentral package not installed.")
return
# Determine dateFrom: last poll or 1 year ago for first run
last_poll = ICP.get_param('fusion_faxes.last_inbound_poll', '')
if last_poll:
date_from = last_poll
else:
one_year_ago = datetime.utcnow() - timedelta(days=365)
date_from = one_year_ago.strftime('%Y-%m-%dT%H:%M:%S.000Z')
try:
sdk = SDK(client_id, client_secret, server_url)
platform = sdk.platform()
platform.login(jwt=jwt_token)
total_imported = 0
total_skipped = 0
# Fetch first page
endpoint = (
'/restapi/v1.0/account/~/extension/~/message-store'
f'?messageType=Fax&direction=Inbound&dateFrom={date_from}'
'&perPage=100'
)
while endpoint:
response = platform.get(endpoint)
data = response.json()
records = []
if hasattr(data, 'records'):
records = data.records
elif isinstance(data, dict):
records = data.get('records', [])
for msg in records:
msg_id = str(msg.get('id', '')) if isinstance(msg, dict) else str(getattr(msg, 'id', ''))
# Deduplicate
existing = self.search_count([('ringcentral_message_id', '=', msg_id)])
if existing:
total_skipped += 1
continue
imported = self._import_inbound_fax(msg, platform)
if imported:
total_imported += 1
# Handle pagination
endpoint = None
navigation = None
if isinstance(data, dict):
navigation = data.get('navigation', {})
elif hasattr(data, 'navigation'):
navigation = data.navigation
if navigation:
next_page = None
if isinstance(navigation, dict):
next_page = navigation.get('nextPage', {})
elif hasattr(navigation, 'nextPage'):
next_page = navigation.nextPage
if next_page:
next_uri = next_page.get('uri', '') if isinstance(next_page, dict) else getattr(next_page, 'uri', '')
if next_uri:
endpoint = next_uri
# Update last poll timestamp
ICP.set_param('fusion_faxes.last_inbound_poll', datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z'))
if total_imported:
_logger.info("Fusion Faxes: Imported %d inbound faxes, skipped %d duplicates.", total_imported, total_skipped)
except Exception:
_logger.exception("Fusion Faxes: Error fetching inbound faxes from RingCentral.")
def _import_inbound_fax(self, msg, platform):
"""Import a single inbound fax message from RingCentral."""
try:
# Extract fields (handle both dict and SDK JsonObject responses)
if isinstance(msg, dict):
msg_id = str(msg.get('id', ''))
from_info = msg.get('from', {})
sender = from_info.get('phoneNumber', '') if isinstance(from_info, dict) else ''
creation_time = msg.get('creationTime', '')
read_status = msg.get('readStatus', '')
page_count = msg.get('faxPageCount', 0)
attachments = msg.get('attachments', [])
else:
msg_id = str(getattr(msg, 'id', ''))
# SDK exposes 'from' as 'from_' since 'from' is a Python keyword
from_info = getattr(msg, 'from_', None) or getattr(msg, 'from', None)
sender = getattr(from_info, 'phoneNumber', '') if from_info else ''
creation_time = getattr(msg, 'creationTime', '')
read_status = getattr(msg, 'readStatus', '')
page_count = getattr(msg, 'faxPageCount', 0)
attachments = getattr(msg, 'attachments', [])
# Parse received datetime
received_dt = False
if creation_time:
try:
clean_time = creation_time.replace('Z', '+00:00')
received_dt = datetime.fromisoformat(clean_time).strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, AttributeError):
received_dt = False
# Try to match sender to a partner
partner = False
if sender:
partner = self.env['res.partner'].sudo().search(
[('x_ff_fax_number', '=', sender)], limit=1
)
# Download the PDF attachment
document_lines = []
for att in attachments:
att_uri = att.get('uri', '') if isinstance(att, dict) else getattr(att, 'uri', '')
att_type = att.get('contentType', '') if isinstance(att, dict) else getattr(att, 'contentType', '')
if not att_uri:
continue
try:
att_response = platform.get(att_uri)
pdf_content = att_response.body()
if not pdf_content:
continue
file_ext = 'pdf' if 'pdf' in (att_type or '') else 'bin'
file_name = f'FAX_IN_{msg_id}.{file_ext}'
ir_attachment = self.env['ir.attachment'].sudo().create({
'name': file_name,
'type': 'binary',
'datas': base64.b64encode(pdf_content),
'mimetype': att_type or 'application/pdf',
'res_model': 'fusion.fax',
})
document_lines.append((0, 0, {
'sequence': 10,
'attachment_id': ir_attachment.id,
}))
except Exception:
_logger.exception("Fusion Faxes: Failed to download attachment for message %s", msg_id)
# Create the fax record
self.sudo().create({
'direction': 'inbound',
'state': 'received',
'fax_number': sender or 'Unknown',
'sender_number': sender,
'partner_id': partner.id if partner else False,
'ringcentral_message_id': msg_id,
'received_date': received_dt,
'page_count': page_count or 0,
'rc_read_status': read_status,
'document_ids': document_lines,
})
return True
except Exception:
_logger.exception("Fusion Faxes: Failed to import inbound fax message.")
return False

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import api, fields, models
class FusionFaxDocument(models.Model):
_name = 'fusion.fax.document'
_description = 'Fax Document Line'
_order = 'sequence, id'
fax_id = fields.Many2one(
'fusion.fax',
string='Fax',
required=True,
ondelete='cascade',
)
sequence = fields.Integer(
string='Order',
default=10,
)
attachment_id = fields.Many2one(
'ir.attachment',
string='Document',
required=True,
ondelete='cascade',
)
file_name = fields.Char(
related='attachment_id.name',
string='File Name',
)
mimetype = fields.Char(
related='attachment_id.mimetype',
string='Type',
)
def action_preview(self):
"""Open the attachment in Odoo's built-in PDF viewer dialog."""
self.ensure_one()
return {
'type': 'ir.actions.client',
'tag': 'fusion_claims.preview_document',
'params': {
'attachment_id': self.attachment_id.id,
'title': self.file_name or 'Document Preview',
},
}

View File

@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import api, fields, models
import logging
_logger = logging.getLogger(__name__)
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
ff_ringcentral_enabled = fields.Boolean(
string='Enable RingCentral Faxing',
config_parameter='fusion_faxes.ringcentral_enabled',
)
ff_ringcentral_server_url = fields.Char(
string='RingCentral Server URL',
config_parameter='fusion_faxes.ringcentral_server_url',
default='https://platform.ringcentral.com',
)
ff_ringcentral_client_id = fields.Char(
string='Client ID',
config_parameter='fusion_faxes.ringcentral_client_id',
groups='fusion_faxes.group_fax_manager',
)
ff_ringcentral_client_secret = fields.Char(
string='Client Secret',
config_parameter='fusion_faxes.ringcentral_client_secret',
groups='fusion_faxes.group_fax_manager',
)
ff_ringcentral_jwt_token = fields.Char(
string='JWT Token',
config_parameter='fusion_faxes.ringcentral_jwt_token',
groups='fusion_faxes.group_fax_manager',
)
def action_test_ringcentral_connection(self):
"""Test connection to RingCentral using stored credentials."""
self.ensure_one()
ICP = self.env['ir.config_parameter'].sudo()
client_id = ICP.get_param('fusion_faxes.ringcentral_client_id', '')
client_secret = ICP.get_param('fusion_faxes.ringcentral_client_secret', '')
server_url = ICP.get_param('fusion_faxes.ringcentral_server_url', 'https://platform.ringcentral.com')
jwt_token = ICP.get_param('fusion_faxes.ringcentral_jwt_token', '')
if not all([client_id, client_secret, jwt_token]):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Configuration Missing',
'message': 'Please fill in Client ID, Client Secret, and JWT Token before testing.',
'type': 'warning',
'sticky': False,
},
}
try:
from ringcentral import SDK
sdk = SDK(client_id, client_secret, server_url)
platform = sdk.platform()
platform.login(jwt=jwt_token)
# Fetch account info to verify
res = platform.get('/restapi/v1.0/account/~/extension/~')
ext_info = res.json()
ext_name = ext_info.name if hasattr(ext_info, 'name') else str(ext_info.get('name', 'Unknown'))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Connection Successful',
'message': f'Connected to RingCentral as: {ext_name}',
'type': 'success',
'sticky': False,
},
}
except ImportError:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'SDK Not Installed',
'message': 'The ringcentral Python package is not installed. Run: pip install ringcentral',
'type': 'danger',
'sticky': True,
},
}
except Exception as e:
_logger.exception("RingCentral connection test failed")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Connection Failed',
'message': str(e),
'type': 'danger',
'sticky': True,
},
}
def set_values(self):
"""Protect credential fields from being blanked accidentally."""
protected_keys = [
'fusion_faxes.ringcentral_client_id',
'fusion_faxes.ringcentral_client_secret',
'fusion_faxes.ringcentral_jwt_token',
]
ICP = self.env['ir.config_parameter'].sudo()
for key in protected_keys:
field_map = {
'fusion_faxes.ringcentral_client_id': 'ff_ringcentral_client_id',
'fusion_faxes.ringcentral_client_secret': 'ff_ringcentral_client_secret',
'fusion_faxes.ringcentral_jwt_token': 'ff_ringcentral_jwt_token',
}
field_name = field_map[key]
new_val = getattr(self, field_name, None)
if new_val in (None, False, ''):
existing = ICP.get_param(key, '')
if existing:
ICP.set_param(key, existing)
return super().set_values()

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
x_ff_fax_number = fields.Char(string='Fax Number')
x_ff_fax_ids = fields.One2many(
'fusion.fax',
'partner_id',
string='Fax History',
)
x_ff_fax_count = fields.Integer(
string='Fax Count',
compute='_compute_fax_count',
)
@api.depends('x_ff_fax_ids')
def _compute_fax_count(self):
for partner in self:
partner.x_ff_fax_count = len(partner.x_ff_fax_ids)
def action_view_faxes(self):
"""Open fax history for this contact."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Fax History',
'res_model': 'fusion.fax',
'view_mode': 'list,form',
'domain': [('partner_id', '=', self.id)],
'context': {'default_partner_id': self.id},
}

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
x_ff_fax_ids = fields.One2many(
'fusion.fax',
'sale_order_id',
string='Faxes',
)
x_ff_fax_count = fields.Integer(
string='Fax Count',
compute='_compute_fax_count',
)
@api.depends('x_ff_fax_ids')
def _compute_fax_count(self):
for order in self:
order.x_ff_fax_count = len(order.x_ff_fax_ids)
def action_send_fax(self):
"""Open the Send Fax wizard pre-filled with this sale order."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Send Fax',
'res_model': 'fusion_faxes.send.fax.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'active_model': 'sale.order',
'active_id': self.id,
},
}
def action_view_faxes(self):
"""Open fax history for this sale order."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Faxes',
'res_model': 'fusion.fax',
'view_mode': 'list,form',
'domain': [('sale_order_id', '=', self.id)],
'context': {'default_sale_order_id': self.id},
}