changes
This commit is contained in:
@@ -52,6 +52,11 @@
|
||||
'views/account_move_views.xml',
|
||||
'wizard/send_fax_wizard_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_faxes/static/src/css/partner_mobile.css',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -1,63 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
{
|
||||
'name': 'Fusion Faxes',
|
||||
'version': '19.0.2.0.0',
|
||||
'category': 'Productivity',
|
||||
'summary': 'Send and receive faxes via RingCentral API from Sale Orders, Invoices, and Contacts.',
|
||||
'description': """
|
||||
Fusion Faxes
|
||||
============
|
||||
|
||||
Send faxes directly from Odoo using the RingCentral REST API.
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Send faxes from Sale Orders and Invoices
|
||||
* Fax history tracked per contact
|
||||
* Fax log with status tracking (draft/sending/sent/failed)
|
||||
* Cover page text support
|
||||
* Multiple document attachments per fax
|
||||
* Chatter integration for fax events
|
||||
* RingCentral JWT authentication (server-to-server)
|
||||
|
||||
Copyright 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'depends': [
|
||||
'base',
|
||||
'mail',
|
||||
'sale',
|
||||
'sale_management',
|
||||
'account',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['ringcentral'],
|
||||
},
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_config_parameter_data.xml',
|
||||
'data/ir_sequence_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'views/fusion_fax_views.xml',
|
||||
'views/dashboard_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/account_move_views.xml',
|
||||
'wizard/send_fax_wizard_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_faxes/static/src/css/partner_mobile.css',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="config_ringcentral_enabled" model="ir.config_parameter">
|
||||
<field name="key">fusion_faxes.ringcentral_enabled</field>
|
||||
<field name="value">False</field>
|
||||
</record>
|
||||
|
||||
<record id="config_ringcentral_server_url" model="ir.config_parameter">
|
||||
<field name="key">fusion_faxes.ringcentral_server_url</field>
|
||||
<field name="value">https://platform.ringcentral.com</field>
|
||||
</record>
|
||||
|
||||
<record id="config_ringcentral_client_id" model="ir.config_parameter">
|
||||
<field name="key">fusion_faxes.ringcentral_client_id</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
|
||||
<record id="config_ringcentral_client_secret" model="ir.config_parameter">
|
||||
<field name="key">fusion_faxes.ringcentral_client_secret</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
|
||||
<record id="config_ringcentral_jwt_token" model="ir.config_parameter">
|
||||
<field name="key">fusion_faxes.ringcentral_jwt_token</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
|
||||
<record id="config_last_inbound_poll" model="ir.config_parameter">
|
||||
<field name="key">fusion_faxes.last_inbound_poll</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="ir_cron_fetch_incoming_faxes" model="ir.cron">
|
||||
<field name="name">Fusion Faxes: Fetch Incoming Faxes</field>
|
||||
<field name="model_id" ref="model_fusion_fax"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_fetch_incoming_faxes()</field>
|
||||
<field name="interval_number">5</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="seq_fusion_fax" model="ir.sequence">
|
||||
<field name="name">Fusion Fax</field>
|
||||
<field name="code">fusion.fax</field>
|
||||
<field name="prefix">FAX/</field>
|
||||
<field name="padding">4</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,11 +0,0 @@
|
||||
# -*- 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
|
||||
@@ -1,51 +0,0 @@
|
||||
# -*- 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},
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
# -*- 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',
|
||||
}
|
||||
@@ -1,581 +0,0 @@
|
||||
# -*- 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
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- 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',
|
||||
},
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
# -*- 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()
|
||||
@@ -1,37 +0,0 @@
|
||||
# -*- 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},
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
# -*- 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},
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fusion_fax_user,fusion.fax.user,model_fusion_fax,group_fax_user,1,1,1,0
|
||||
access_fusion_fax_manager,fusion.fax.manager,model_fusion_fax,group_fax_manager,1,1,1,1
|
||||
access_fusion_send_fax_wizard_user,fusion.send.fax.wizard.user,model_fusion_faxes_send_fax_wizard,group_fax_user,1,1,1,1
|
||||
access_fusion_send_fax_wizard_line_user,fusion.send.fax.wizard.line.user,model_fusion_faxes_send_fax_wizard_line,group_fax_user,1,1,1,1
|
||||
access_fusion_fax_document_user,fusion.fax.document.user,model_fusion_fax_document,group_fax_user,1,1,1,0
|
||||
access_fusion_fax_document_manager,fusion.fax.document.manager,model_fusion_fax_document,group_fax_manager,1,1,1,1
|
||||
access_fusion_fax_dashboard_user,fusion.fax.dashboard.user,model_fusion_fax_dashboard,group_fax_user,1,1,1,1
|
||||
|
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- User group: can send faxes and view own fax history -->
|
||||
<record id="group_fax_user" model="res.groups">
|
||||
<field name="name">Fusion Faxes / User</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Manager group: can view all faxes and configure settings -->
|
||||
<record id="group_fax_manager" model="res.groups">
|
||||
<field name="name">Fusion Faxes / Manager</field>
|
||||
<field name="implied_ids" eval="[(4, ref('group_fax_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Record rules -->
|
||||
|
||||
<!-- Users see only their own faxes -->
|
||||
<record id="rule_fax_user_own" model="ir.rule">
|
||||
<field name="name">Fax: user sees own faxes</field>
|
||||
<field name="model_id" ref="model_fusion_fax"/>
|
||||
<field name="domain_force">[('sent_by_id', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_fax_user'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Managers see all faxes -->
|
||||
<record id="rule_fax_manager_all" model="ir.rule">
|
||||
<field name="name">Fax: manager sees all faxes</field>
|
||||
<field name="model_id" ref="model_fusion_fax"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_fax_manager'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Add Send Fax button + smart button to invoice form -->
|
||||
<record id="view_account_move_form_inherit_fusion_faxes" model="ir.ui.view">
|
||||
<field name="name">account.move.form.inherit.fusion_faxes</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Smart button for fax count -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_faxes" type="object"
|
||||
class="oe_stat_button" icon="fa-fax"
|
||||
invisible="x_ff_fax_count == 0">
|
||||
<field name="x_ff_fax_count" widget="statinfo" string="Faxes"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<!-- Send Fax header button -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_send_fax" string="Send Fax"
|
||||
type="object" class="btn-secondary"
|
||||
icon="fa-fax"
|
||||
groups="fusion_faxes.group_fax_user"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,156 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Dashboard Form View -->
|
||||
<record id="view_fusion_fax_dashboard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.fax.dashboard.form</field>
|
||||
<field name="model">fusion.fax.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Fax Dashboard" create="0" delete="0" edit="0">
|
||||
<sheet>
|
||||
<field name="name" invisible="1"/>
|
||||
<!-- Title -->
|
||||
<div class="mb-4">
|
||||
<span class="text-muted">Overview of fax activity and quick actions</span>
|
||||
</div>
|
||||
|
||||
<!-- KPI Stat Cards -->
|
||||
<div class="d-flex flex-nowrap gap-3 mb-4">
|
||||
|
||||
<!-- Total Faxes -->
|
||||
<div class="flex-fill">
|
||||
<button name="action_open_all" type="object"
|
||||
class="btn btn-primary w-100 p-0 border-0 rounded-3">
|
||||
<div class="text-center py-3 px-2">
|
||||
<div class="fw-bold" style="font-size: 2rem;">
|
||||
<field name="total_count"/>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem;">Total Faxes</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sent -->
|
||||
<div class="flex-fill">
|
||||
<button name="action_open_sent" type="object"
|
||||
class="btn btn-success w-100 p-0 border-0 rounded-3">
|
||||
<div class="text-center py-3 px-2">
|
||||
<div class="fw-bold" style="font-size: 2rem;">
|
||||
<field name="sent_count"/>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem;">Sent</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Received -->
|
||||
<div class="flex-fill">
|
||||
<button name="action_open_received" type="object"
|
||||
class="btn btn-info w-100 p-0 border-0 rounded-3">
|
||||
<div class="text-center py-3 px-2">
|
||||
<div class="fw-bold" style="font-size: 2rem;">
|
||||
<field name="received_count"/>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem;">Received</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Failed -->
|
||||
<div class="flex-fill" invisible="failed_count == 0">
|
||||
<button name="action_open_failed" type="object"
|
||||
class="btn btn-danger w-100 p-0 border-0 rounded-3">
|
||||
<div class="text-center py-3 px-2">
|
||||
<div class="fw-bold" style="font-size: 2rem;">
|
||||
<field name="failed_count"/>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem;">Failed</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Draft -->
|
||||
<div class="flex-fill" invisible="draft_count == 0">
|
||||
<button name="action_open_draft" type="object"
|
||||
class="btn btn-warning w-100 p-0 border-0 rounded-3">
|
||||
<div class="text-center py-3 px-2">
|
||||
<div class="fw-bold" style="font-size: 2rem;">
|
||||
<field name="draft_count"/>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem;">Draft</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Quick Action Buttons -->
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button name="action_send_fax" type="object"
|
||||
class="btn btn-primary"
|
||||
icon="fa-fax">
|
||||
Send New Fax
|
||||
</button>
|
||||
<button name="action_open_all" type="object"
|
||||
class="btn btn-secondary"
|
||||
icon="fa-list">
|
||||
View All Faxes
|
||||
</button>
|
||||
<button name="action_open_received" type="object"
|
||||
class="btn btn-secondary"
|
||||
icon="fa-download">
|
||||
View Received
|
||||
</button>
|
||||
<button name="action_open_failed" type="object"
|
||||
class="btn btn-secondary"
|
||||
icon="fa-exclamation-triangle"
|
||||
invisible="failed_count == 0">
|
||||
View Failed
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Recent Fax History -->
|
||||
<separator string="Recent Fax History"/>
|
||||
<field name="recent_fax_ids" nolabel="1" readonly="1">
|
||||
<list decoration-danger="state == 'failed'"
|
||||
decoration-success="state in ('sent', 'received')"
|
||||
decoration-info="state == 'sending'">
|
||||
<field name="name"/>
|
||||
<field name="direction" widget="badge"
|
||||
decoration-info="direction == 'outbound'"
|
||||
decoration-success="direction == 'inbound'"/>
|
||||
<field name="display_date" string="Date"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="display_number" string="Fax Number"/>
|
||||
<field name="page_count"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state in ('sent', 'received')"
|
||||
decoration-danger="state == 'failed'"
|
||||
decoration-info="state == 'sending'"
|
||||
decoration-warning="state == 'draft'"/>
|
||||
<field name="sent_by_id" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Action -->
|
||||
<record id="action_fusion_fax_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Dashboard</field>
|
||||
<field name="res_model">fusion.fax.dashboard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_fusion_fax_dashboard_form"/>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Menu (first item under Faxes root) -->
|
||||
<menuitem id="menu_fusion_fax_dashboard"
|
||||
name="Dashboard"
|
||||
parent="menu_fusion_faxes_root"
|
||||
action="action_fusion_fax_dashboard"
|
||||
sequence="1"/>
|
||||
|
||||
</odoo>
|
||||
@@ -1,259 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Fax Form View -->
|
||||
<record id="view_fusion_fax_form" model="ir.ui.view">
|
||||
<field name="name">fusion.fax.form</field>
|
||||
<field name="model">fusion.fax</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Fax">
|
||||
<header>
|
||||
<!-- Outbound buttons only -->
|
||||
<button name="action_send" string="Send Fax"
|
||||
type="object" class="btn-primary"
|
||||
icon="fa-fax"
|
||||
invisible="state != 'draft' or direction == 'inbound'"/>
|
||||
<button name="action_resend" string="Resend Fax"
|
||||
type="object" class="btn-primary"
|
||||
icon="fa-repeat"
|
||||
invisible="state != 'sent' or direction == 'inbound'"
|
||||
confirm="This will resend the fax with all attachments. Continue?"/>
|
||||
<button name="action_retry" string="Retry"
|
||||
type="object" class="btn-primary"
|
||||
icon="fa-refresh"
|
||||
invisible="state != 'failed' or direction == 'inbound'"/>
|
||||
<button name="action_reset_to_draft" string="Reset to Draft"
|
||||
type="object"
|
||||
invisible="state != 'failed' or direction == 'inbound'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,sending,sent,received"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_open_sale_order" type="object"
|
||||
class="oe_stat_button" icon="fa-shopping-cart"
|
||||
invisible="not sale_order_id">
|
||||
<field name="sale_order_id" widget="statinfo" string="Sale Order"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Direction badge -->
|
||||
<div class="mb-3">
|
||||
<field name="direction" widget="badge"
|
||||
decoration-info="direction == 'outbound'"
|
||||
decoration-success="direction == 'inbound'"
|
||||
readonly="1"/>
|
||||
</div>
|
||||
|
||||
<!-- Outbound fields -->
|
||||
<group invisible="direction == 'inbound'">
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="fax_number"/>
|
||||
<field name="sent_by_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="sent_date"/>
|
||||
<field name="page_count"/>
|
||||
<field name="ringcentral_message_id"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Inbound fields -->
|
||||
<group invisible="direction == 'outbound'">
|
||||
<group>
|
||||
<field name="sender_number"/>
|
||||
<field name="partner_id" string="Matched Contact"/>
|
||||
<field name="sale_order_id" string="Link to Sale Order"
|
||||
options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="received_date"/>
|
||||
<field name="page_count"/>
|
||||
<field name="rc_read_status"/>
|
||||
<field name="ringcentral_message_id"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group invisible="direction == 'inbound'">
|
||||
<field name="cover_page_text" placeholder="Optional cover page text..."/>
|
||||
</group>
|
||||
|
||||
<!-- Ordered Documents with preview -->
|
||||
<separator string="Documents"/>
|
||||
<field name="document_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="attachment_id"/>
|
||||
<field name="file_name" readonly="1"/>
|
||||
<field name="mimetype" readonly="1"/>
|
||||
<button name="action_preview" type="object"
|
||||
string="Preview" icon="fa-eye"
|
||||
class="btn-link"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
<group string="Linked Records"
|
||||
invisible="direction == 'inbound'">
|
||||
<group>
|
||||
<field name="sale_order_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="account_move_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Error Details" invisible="state != 'failed'">
|
||||
<field name="error_message" readonly="1" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Fax List View -->
|
||||
<record id="view_fusion_fax_list" model="ir.ui.view">
|
||||
<field name="name">fusion.fax.list</field>
|
||||
<field name="model">fusion.fax</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Faxes" decoration-danger="state == 'failed'"
|
||||
decoration-success="state in ('sent', 'received')"
|
||||
decoration-info="state == 'sending'">
|
||||
<field name="name"/>
|
||||
<field name="direction" widget="badge"
|
||||
decoration-info="direction == 'outbound'"
|
||||
decoration-success="direction == 'inbound'"/>
|
||||
<field name="display_date" string="Date"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="display_number" string="Fax Number"/>
|
||||
<field name="page_count"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state in ('sent', 'received')"
|
||||
decoration-danger="state == 'failed'"
|
||||
decoration-info="state == 'sending'"
|
||||
decoration-warning="state == 'draft'"/>
|
||||
<field name="sent_by_id" optional="show"/>
|
||||
<field name="sale_order_id" optional="hide"/>
|
||||
<field name="account_move_id" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Fax Search View -->
|
||||
<record id="view_fusion_fax_search" model="ir.ui.view">
|
||||
<field name="name">fusion.fax.search</field>
|
||||
<field name="model">fusion.fax</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Faxes">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="fax_number"/>
|
||||
<field name="sender_number"/>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="account_move_id"/>
|
||||
<separator/>
|
||||
<filter string="Outbound" name="filter_outbound"
|
||||
domain="[('direction', '=', 'outbound')]"/>
|
||||
<filter string="Inbound" name="filter_inbound"
|
||||
domain="[('direction', '=', 'inbound')]"/>
|
||||
<separator/>
|
||||
<filter string="Draft" name="filter_draft"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter string="Sent" name="filter_sent"
|
||||
domain="[('state', '=', 'sent')]"/>
|
||||
<filter string="Received" name="filter_received"
|
||||
domain="[('state', '=', 'received')]"/>
|
||||
<filter string="Failed" name="filter_failed"
|
||||
domain="[('state', '=', 'failed')]"/>
|
||||
<separator/>
|
||||
<filter string="My Faxes" name="filter_my_faxes"
|
||||
domain="[('sent_by_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Direction" name="group_direction"
|
||||
context="{'group_by': 'direction'}"/>
|
||||
<filter string="Status" name="group_state"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter string="Recipient" name="group_partner"
|
||||
context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Sent Date" name="group_date"
|
||||
context="{'group_by': 'sent_date:month'}"/>
|
||||
<filter string="Sent By" name="group_user"
|
||||
context="{'group_by': 'sent_by_id'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu and Actions -->
|
||||
<record id="action_fusion_fax" model="ir.actions.act_window">
|
||||
<field name="name">All Faxes</field>
|
||||
<field name="res_model">fusion.fax</field>
|
||||
<field name="path">faxes</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fusion_fax_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No faxes yet
|
||||
</p>
|
||||
<p>
|
||||
Send faxes from Sale Orders or Invoices using the "Send Fax" button,
|
||||
or create one manually from here.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Received Faxes Action -->
|
||||
<record id="action_fusion_fax_received" model="ir.actions.act_window">
|
||||
<field name="name">Received Faxes</field>
|
||||
<field name="res_model">fusion.fax</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('direction', '=', 'inbound')]</field>
|
||||
<field name="search_view_id" ref="view_fusion_fax_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No received faxes yet
|
||||
</p>
|
||||
<p>
|
||||
Incoming faxes are automatically synced from RingCentral every 5 minutes.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Sent Faxes Action -->
|
||||
<record id="action_fusion_fax_sent" model="ir.actions.act_window">
|
||||
<field name="name">Sent Faxes</field>
|
||||
<field name="res_model">fusion.fax</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('direction', '=', 'outbound')]</field>
|
||||
<field name="search_view_id" ref="view_fusion_fax_search"/>
|
||||
</record>
|
||||
|
||||
<!-- Top-level Faxes menu -->
|
||||
<menuitem id="menu_fusion_faxes_root"
|
||||
name="Faxes"
|
||||
web_icon="fusion_faxes,static/description/icon.png"
|
||||
sequence="45"/>
|
||||
|
||||
<menuitem id="menu_fusion_fax_list"
|
||||
name="All Faxes"
|
||||
parent="menu_fusion_faxes_root"
|
||||
action="action_fusion_fax"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fusion_fax_received"
|
||||
name="Received Faxes"
|
||||
parent="menu_fusion_faxes_root"
|
||||
action="action_fusion_fax_received"
|
||||
sequence="15"/>
|
||||
|
||||
<menuitem id="menu_fusion_fax_sent"
|
||||
name="Sent Faxes"
|
||||
parent="menu_fusion_faxes_root"
|
||||
action="action_fusion_fax_sent"
|
||||
sequence="20"/>
|
||||
|
||||
</odoo>
|
||||
@@ -1,115 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form_fusion_faxes" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.fusion_faxes</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="Fusion Faxes" string="Fusion Faxes" name="fusion_faxes"
|
||||
groups="fusion_faxes.group_fax_manager">
|
||||
|
||||
<h2>RingCentral Configuration</h2>
|
||||
<div class="row mt-4 o_settings_container">
|
||||
|
||||
<!-- Enable toggle -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="ff_ringcentral_enabled"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="ff_ringcentral_enabled"/>
|
||||
<div class="text-muted">
|
||||
Enable sending faxes via RingCentral API.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server URL -->
|
||||
<div class="col-12 col-lg-6 o_setting_box"
|
||||
invisible="not ff_ringcentral_enabled">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Server URL</span>
|
||||
<div class="text-muted">
|
||||
RingCentral API server URL. Use https://platform.devtest.ringcentral.com for sandbox.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="ff_ringcentral_server_url"
|
||||
placeholder="https://platform.ringcentral.com"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client ID -->
|
||||
<div class="col-12 col-lg-6 o_setting_box"
|
||||
invisible="not ff_ringcentral_enabled">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Client ID</span>
|
||||
<div class="text-muted">
|
||||
From your RingCentral Developer Console app.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="ff_ringcentral_client_id"
|
||||
placeholder="Enter Client ID..."/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Secret -->
|
||||
<div class="col-12 col-lg-6 o_setting_box"
|
||||
invisible="not ff_ringcentral_enabled">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Client Secret</span>
|
||||
<div class="text-muted">
|
||||
From your RingCentral Developer Console app.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="ff_ringcentral_client_secret"
|
||||
password="True"
|
||||
placeholder="Enter Client Secret..."/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JWT Token -->
|
||||
<div class="col-12 col-lg-6 o_setting_box"
|
||||
invisible="not ff_ringcentral_enabled">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">JWT Token</span>
|
||||
<div class="text-muted">
|
||||
Generated from RingCentral Developer Console > Credentials > JWT.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="ff_ringcentral_jwt_token"
|
||||
password="True"
|
||||
placeholder="Enter JWT Token..."/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Connection button -->
|
||||
<div class="col-12 col-lg-6 o_setting_box"
|
||||
invisible="not ff_ringcentral_enabled">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Test Connection</span>
|
||||
<div class="text-muted">
|
||||
Verify your RingCentral credentials are working.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button name="action_test_ringcentral_connection"
|
||||
string="Test Connection"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
icon="fa-plug"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Add fax number field + fax history tab to contact form -->
|
||||
<record id="view_partner_form_inherit_fusion_faxes" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit.fusion_faxes</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Add fax number on its own row below phone -->
|
||||
<xpath expr="//field[@name='phone']/.." position="after">
|
||||
<div class="d-flex align-items-baseline w-md-50">
|
||||
<i class="fa fa-fw me-1 fa-fax text-primary" title="Fax"/>
|
||||
<field name="x_ff_fax_number" class="w-100" widget="phone"
|
||||
placeholder="Fax number..."/>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<!-- Add smart button for fax count -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_faxes" type="object"
|
||||
class="oe_stat_button" icon="fa-fax"
|
||||
invisible="x_ff_fax_count == 0">
|
||||
<field name="x_ff_fax_count" widget="statinfo" string="Faxes"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<!-- Add Fax History tab -->
|
||||
<xpath expr="//page[@name='internal_notes']" position="after">
|
||||
<page string="Fax History" name="fax_history"
|
||||
invisible="x_ff_fax_count == 0">
|
||||
<field name="x_ff_fax_ids" readonly="1">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="sent_date"/>
|
||||
<field name="fax_number"/>
|
||||
<field name="page_count"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'sent'"
|
||||
decoration-danger="state == 'failed'"/>
|
||||
<field name="sent_by_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Add Send Fax button + smart button to sale order form -->
|
||||
<record id="view_sale_order_form_inherit_fusion_faxes" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.inherit.fusion_faxes</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Smart button for fax count -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_faxes" type="object"
|
||||
class="oe_stat_button" icon="fa-fax"
|
||||
invisible="x_ff_fax_count == 0">
|
||||
<field name="x_ff_fax_count" widget="statinfo" string="Faxes"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<!-- Send Fax header button -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_send_fax" string="Send Fax"
|
||||
type="object" class="btn-secondary"
|
||||
icon="fa-fax"
|
||||
groups="fusion_faxes.group_fax_user"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,6 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import send_fax_wizard
|
||||
from . import send_fax_wizard_line
|
||||
@@ -1,162 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SendFaxWizard(models.TransientModel):
|
||||
_name = 'fusion_faxes.send.fax.wizard'
|
||||
_description = 'Send Fax Wizard'
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Recipient',
|
||||
required=True,
|
||||
)
|
||||
fax_number = fields.Char(
|
||||
string='Fax Number',
|
||||
required=True,
|
||||
)
|
||||
cover_page_text = fields.Text(
|
||||
string='Cover Page Text',
|
||||
)
|
||||
document_line_ids = fields.One2many(
|
||||
'fusion_faxes.send.fax.wizard.line',
|
||||
'wizard_id',
|
||||
string='Documents',
|
||||
)
|
||||
generate_pdf = fields.Boolean(
|
||||
string='Attach PDF of Source Document',
|
||||
default=True,
|
||||
help='Automatically generate and attach a PDF of the sale order or invoice.',
|
||||
)
|
||||
|
||||
# Context links
|
||||
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
|
||||
account_move_id = fields.Many2one('account.move', string='Invoice')
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
if self.partner_id and self.partner_id.x_ff_fax_number:
|
||||
self.fax_number = self.partner_id.x_ff_fax_number
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
context = self.env.context
|
||||
|
||||
# Auto-fill from sale order context
|
||||
if context.get('active_model') == 'sale.order' and context.get('active_id'):
|
||||
order = self.env['sale.order'].browse(context['active_id'])
|
||||
res['sale_order_id'] = order.id
|
||||
res['partner_id'] = order.partner_id.id
|
||||
if order.partner_id.x_ff_fax_number:
|
||||
res['fax_number'] = order.partner_id.x_ff_fax_number
|
||||
|
||||
# Auto-fill from invoice context
|
||||
elif context.get('active_model') == 'account.move' and context.get('active_id'):
|
||||
move = self.env['account.move'].browse(context['active_id'])
|
||||
res['account_move_id'] = move.id
|
||||
res['partner_id'] = move.partner_id.id
|
||||
if move.partner_id.x_ff_fax_number:
|
||||
res['fax_number'] = move.partner_id.x_ff_fax_number
|
||||
|
||||
# Pre-attach documents when forwarding a fax
|
||||
forward_ids = context.get('forward_attachment_ids')
|
||||
if forward_ids:
|
||||
lines = []
|
||||
for seq, att_id in enumerate(forward_ids, start=1):
|
||||
lines.append((0, 0, {
|
||||
'sequence': seq * 10,
|
||||
'attachment_id': att_id,
|
||||
'file_name': self.env['ir.attachment'].browse(att_id).name,
|
||||
}))
|
||||
res['document_line_ids'] = lines
|
||||
res['generate_pdf'] = False
|
||||
|
||||
return res
|
||||
|
||||
def _generate_source_pdf(self):
|
||||
"""Generate a PDF of the linked sale order or invoice."""
|
||||
self.ensure_one()
|
||||
if self.sale_order_id:
|
||||
report = self.env.ref('sale.action_report_saleorder')
|
||||
pdf_content, _ = report._render_qweb_pdf(report.id, [self.sale_order_id.id])
|
||||
filename = f'{self.sale_order_id.name}.pdf'
|
||||
return filename, pdf_content
|
||||
elif self.account_move_id:
|
||||
report = self.env.ref('account.account_invoices')
|
||||
pdf_content, _ = report._render_qweb_pdf(report.id, [self.account_move_id.id])
|
||||
filename = f'{self.account_move_id.name}.pdf'
|
||||
return filename, pdf_content
|
||||
return None, None
|
||||
|
||||
def action_send(self):
|
||||
"""Create a fusion.fax record and send it."""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.fax_number:
|
||||
raise UserError(_('Please enter a fax number.'))
|
||||
|
||||
# Collect attachment IDs from ordered wizard lines
|
||||
ordered_attachment_ids = list(
|
||||
self.document_line_ids.sorted('sequence').mapped('attachment_id').ids
|
||||
)
|
||||
|
||||
# Generate PDF of source document if requested
|
||||
if self.generate_pdf:
|
||||
filename, pdf_content = self._generate_source_pdf()
|
||||
if pdf_content:
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': filename,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(pdf_content),
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': 'fusion.fax',
|
||||
})
|
||||
# Generated PDF goes first (sequence 1)
|
||||
ordered_attachment_ids.insert(0, attachment.id)
|
||||
|
||||
if not ordered_attachment_ids:
|
||||
raise UserError(_('Please attach at least one document or enable PDF generation.'))
|
||||
|
||||
# Build document lines preserving the wizard ordering
|
||||
document_lines = []
|
||||
for seq, att_id in enumerate(ordered_attachment_ids, start=1):
|
||||
document_lines.append((0, 0, {
|
||||
'sequence': seq * 10,
|
||||
'attachment_id': att_id,
|
||||
}))
|
||||
|
||||
# Create the fax record
|
||||
fax = self.env['fusion.fax'].create({
|
||||
'partner_id': self.partner_id.id,
|
||||
'fax_number': self.fax_number,
|
||||
'cover_page_text': self.cover_page_text,
|
||||
'document_ids': document_lines,
|
||||
'sale_order_id': self.sale_order_id.id if self.sale_order_id else False,
|
||||
'account_move_id': self.account_move_id.id if self.account_move_id else False,
|
||||
'sent_by_id': self.env.user.id,
|
||||
})
|
||||
|
||||
# Send immediately
|
||||
fax._send_fax()
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Fax Sent'),
|
||||
'message': _('Fax %s sent successfully to %s.') % (fax.name, self.fax_number),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
},
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SendFaxWizardLine(models.TransientModel):
|
||||
_name = 'fusion_faxes.send.fax.wizard.line'
|
||||
_description = 'Send Fax Wizard Document Line'
|
||||
_order = 'sequence, id'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'fusion_faxes.send.fax.wizard',
|
||||
string='Wizard',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Order',
|
||||
default=10,
|
||||
)
|
||||
file_upload = fields.Binary(
|
||||
string='Upload File',
|
||||
)
|
||||
file_name = fields.Char(
|
||||
string='File Name',
|
||||
)
|
||||
attachment_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Attachment',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.onchange('file_upload')
|
||||
def _onchange_file_upload(self):
|
||||
"""Create an ir.attachment when a file is uploaded."""
|
||||
if self.file_upload and self.file_name:
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': self.file_name,
|
||||
'type': 'binary',
|
||||
'datas': self.file_upload,
|
||||
'res_model': 'fusion.fax',
|
||||
})
|
||||
self.attachment_id = attachment.id
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Send Fax Wizard Form -->
|
||||
<record id="view_send_fax_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion_faxes.send.fax.wizard.form</field>
|
||||
<field name="model">fusion_faxes.send.fax.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Send Fax">
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="fax_number"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="generate_pdf"/>
|
||||
<field name="sale_order_id" readonly="1"
|
||||
invisible="not sale_order_id"/>
|
||||
<field name="account_move_id" readonly="1"
|
||||
invisible="not account_move_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="cover_page_text"
|
||||
placeholder="Optional cover page message..."/>
|
||||
</group>
|
||||
|
||||
<!-- Ordered document list with drag handle and file upload -->
|
||||
<separator string="Documents (drag to reorder)"/>
|
||||
<field name="document_line_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="file_upload" filename="file_name"/>
|
||||
<field name="file_name"/>
|
||||
<field name="attachment_id" column_invisible="1"/>
|
||||
<widget name="preview_button"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
<footer>
|
||||
<button name="action_send" string="Send Fax"
|
||||
type="object" class="btn-primary"
|
||||
icon="fa-fax"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Wizard action (for standalone use from menu) -->
|
||||
<record id="action_send_fax_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Send Fax</field>
|
||||
<field name="res_model">fusion_faxes.send.fax.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_send_fax"
|
||||
name="Send Fax"
|
||||
parent="menu_fusion_faxes_root"
|
||||
action="action_send_fax_wizard"
|
||||
sequence="5"/>
|
||||
|
||||
</odoo>
|
||||
@@ -8,9 +8,13 @@
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Add fax number after phone field -->
|
||||
<xpath expr="//field[@name='phone']" position="after">
|
||||
<field name="x_ff_fax_number" placeholder="Fax number..."/>
|
||||
<!-- Add fax number on its own row below phone -->
|
||||
<xpath expr="//field[@name='phone']/.." position="after">
|
||||
<div class="d-flex align-items-baseline w-md-50">
|
||||
<i class="fa fa-fw me-1 fa-fax text-primary" title="Fax"/>
|
||||
<field name="x_ff_fax_number" class="w-100" widget="phone"
|
||||
placeholder="Fax number..."/>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<!-- Add smart button for fax count -->
|
||||
|
||||
Reference in New Issue
Block a user