changes
@@ -1,58 +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',
|
||||
],
|
||||
'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>
|
||||
|
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,47 +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 after phone field -->
|
||||
<xpath expr="//field[@name='phone']" position="after">
|
||||
<field name="x_ff_fax_number" placeholder="Fax number..."/>
|
||||
</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>
|
||||
@@ -1,7 +0,0 @@
|
||||
# Synodica Solutions
|
||||
|
||||
## Changelog
|
||||
|
||||
### 16.0.0.0.0
|
||||
|
||||
- Initial version
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import models
|
||||
from . import canada_post_api
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
# App information
|
||||
"name": "Canada Post Shipping",
|
||||
"version": "19.0.1.0.0",
|
||||
"category": "Inventory/Delivery",
|
||||
"summary": "Integration of Canada Post delivery services in Odoo "
|
||||
"to handle shipment operations, including get "
|
||||
"live rate, generating shipping labels, "
|
||||
"retrieving tracking numbers",
|
||||
"license": "OPL-1",
|
||||
"depends": ["stock_delivery", "mail"],
|
||||
# Views
|
||||
"data": [
|
||||
"data/delivery_canada_post_data.xml",
|
||||
"views/delivery_carrier_view.xml",
|
||||
],
|
||||
"images": ["static/description/canada_post_banner.gif"],
|
||||
# Author
|
||||
"author": "Synodica Solutions Pvt. Ltd.",
|
||||
"website": "https://synodica.com",
|
||||
"maintainer": "Synodica Solutions Pvt. Ltd.",
|
||||
"support": "support@synodica.com",
|
||||
# Technical
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"application": True,
|
||||
"price": "149.00",
|
||||
"currency": "USD",
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import canada_post_response
|
||||
from . import utils
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="0">
|
||||
<!-- Canada post Package -->
|
||||
<record id="canadapost_packaging_canadapost_BOX" model="stock.package.type">
|
||||
<field name="name">canadapost BOX</field>
|
||||
<field name="package_carrier_type">canada_post</field>
|
||||
<field name="packaging_length">100</field>
|
||||
<field name="width">100</field>
|
||||
<field name="height">100</field>
|
||||
<field name="max_weight">20.00</field>
|
||||
</record>
|
||||
|
||||
<!-- Canada post Delivery Carriers -->
|
||||
<record id="product_product_delivery_canadapost" model="product.product">
|
||||
<field name="name">Canada Post</field>
|
||||
<field name="default_code">Delivery</field>
|
||||
<field name="type">service</field>
|
||||
<field name="categ_id" ref="delivery.product_category_deliveries" />
|
||||
<field name="sale_ok" eval="False" />
|
||||
<field name="purchase_ok" eval="False" />
|
||||
<field name="list_price">0.0</field>
|
||||
</record>
|
||||
|
||||
<record id="delivery_carrier_canadapost" model="delivery.carrier">
|
||||
<field name="name">Canada Post</field>
|
||||
<field
|
||||
name="product_id"
|
||||
ref="delivery_canadapost.product_product_delivery_canadapost"
|
||||
/>
|
||||
<field name="delivery_type">canada_post</field>
|
||||
<field name="canadapost_type">commercial</field>
|
||||
<field name="option_code">SO</field>
|
||||
<field name="service_type">DOM.RP</field>
|
||||
<field
|
||||
name="product_packaging_id"
|
||||
ref="delivery_canadapost.canadapost_packaging_canadapost_BOX"
|
||||
/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,3 +0,0 @@
|
||||
from . import delivery_carrier
|
||||
from . import product_packaging
|
||||
from . import res_company
|
||||
@@ -1,509 +0,0 @@
|
||||
import string
|
||||
import random
|
||||
from requests import request
|
||||
import xml.etree.ElementTree as etree
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import RedirectWarning,ValidationError
|
||||
from odoo.addons.delivery_canadapost.canada_post_api.canada_post_response import Response
|
||||
|
||||
|
||||
class DeliveryCarrier(models.Model):
|
||||
_inherit = 'delivery.carrier'
|
||||
|
||||
delivery_type = fields.Selection(selection_add=[('canada_post', 'Canada Post')], ondelete={
|
||||
'canada_post': lambda recs: recs.write({'delivery_type': 'fixed', 'fixed_price': 0})})
|
||||
option_code = fields.Selection([('SO','SO - Signature'),
|
||||
('COV','COV - Coverage'),
|
||||
('COD','COD - Collect on delivery'),
|
||||
('PA18','PA18 - Proof of Age Required - 18'),
|
||||
('PA19','PA19 - Proof of Age Required - 19'),
|
||||
('HFP','HFP - Card for pickup'),
|
||||
('DNS','DNS - Do not safe drop'),
|
||||
('LAD','LAD - Leave at door - do not card'),
|
||||
],string="Option Code",
|
||||
help="Required if the corresponding parent XML element option exists. This is the option code indicating which option applies to this shipment.\n Note: The D2PO option indicates that the parcel will be delivered directly to a nearby Post Office. For the D2PO option, the following XML elements are required: \n name (under destination) \n client-voice-number (under destination) \n notification \n option-qualifier-2 \n Note: If you select Collect on Delivery (COD), specify Card for Pickup (HFP) or Deliver to Post Office (D2PO). This is to facilitate the collection of COD funds at a post office. If not specified, the system will default to HFP. \n Non-delivery handling codes (required for some U.S.A. and international shipments)\n RASE - Return at Sender’s Expense \n RTS - Return to Sender \n ABAN - Abandon")
|
||||
service_type = fields.Selection([('DOM.RP','DOM.RP - Regular Parcel'),
|
||||
('DOM.EP','DOM.EP - Expedited Parcel'),
|
||||
('DOM.XP','DOM.XP - Xpresspost'),
|
||||
('DOM.PC','DOM.PC - Priority'),
|
||||
('USA.XP','USA.XP - Xpresspost USA'),
|
||||
('USA.EP','USA.EP - Expedited Parcel USA'),
|
||||
('INT.IP.SURF','INT.IP.SURF - International Parcel Surface'),
|
||||
('INT.PW.PARCEL','INT.PW.PARCEL - Priority Worldwide parcel Int’l'),
|
||||
('INT.XP','INT.XP - Xpresspost International'),
|
||||
], string="Service Type",
|
||||
help="Canada Post delivery service used for shipping the item")
|
||||
product_packaging_id = fields.Many2one('stock.package.type', string="Default Package Type",help="Selected packaging type, used in the request parameter")
|
||||
|
||||
reason_for_export = fields.Selection([('DOC', 'DOC = document'),
|
||||
('SAM', 'SAM = commercial sample'),
|
||||
('REP', 'REP = repair or warranty'),
|
||||
('SOG', 'SOG = sale of goods'),
|
||||
('OTH', 'OTH = other')], string="Reason For Export",default="SOG",
|
||||
help="This is a code that represents the reason for export, which assists with border crossing.")
|
||||
|
||||
username = fields.Char("Username", copy=False, help="UserName provided by canada post.")
|
||||
password = fields.Char("Password", copy=False, help="Password provided by canada post.")
|
||||
customer_number = fields.Char("Customer Number", copy=False, help="The mailed by customer, Customer number provided by canada post.")
|
||||
tracking_link = fields.Char(string="Tracking Link",help="Tracking link(URL) useful to track the shipment or package from this URL.",size=256)
|
||||
|
||||
canadapost_type = fields.Selection(selection=[('commercial', 'Contract Shipping'), ('counter', 'Non-Contract Shipping')], string="Customer Type", required=False)
|
||||
canadapost_contract_id = fields.Char(string="Contract ID")
|
||||
canadapost_payment_method = fields.Selection([('CreditCard', 'Credit Card'), ('Account', 'Account')],
|
||||
string="Payment Method", default='CreditCard',
|
||||
help="This is the method of payment for the shipment. The default value is CreditCard.")
|
||||
|
||||
#set default weight_uom_id
|
||||
def _default_uom_in_delive(self):
|
||||
weight_uom_id = self.env.ref('uom.product_uom_kgm', raise_if_not_found=False)
|
||||
if not weight_uom_id:
|
||||
uom_categ_id = self.env.ref('uom.product_uom_categ_kgm').id
|
||||
weight_uom_id = self.env['uom.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1)
|
||||
return weight_uom_id
|
||||
|
||||
weight_uom_id = fields.Many2one('uom.uom', string='Shipping UoM according to API UoM',help="Set equivalent unit of measurement according to provider unit of measurement. For Example, if the provider unit of measurement is KG then you have to select KG unit of measurement in the Shipping Unit of Measurement field.",default=_default_uom_in_delive)
|
||||
|
||||
#Get Canada post URL
|
||||
@api.model
|
||||
def get_canadapost_url(self):
|
||||
if self.prod_environment:
|
||||
return "https://soa-gw.canadapost.ca/rs/"
|
||||
else:
|
||||
return "https://ct.soa-gw.canadapost.ca/rs/"
|
||||
|
||||
def _compute_can_generate_return(self):
|
||||
super(DeliveryCarrier, self)._compute_can_generate_return()
|
||||
for carrier in self:
|
||||
if carrier.delivery_type == 'canada_post':
|
||||
carrier.can_generate_return = True
|
||||
|
||||
#Check Address filed is validating or not and return boolean value
|
||||
@api.model
|
||||
def validating_address(self, partner, additional_fields=[]):
|
||||
missing_value = []
|
||||
mandatory_fields = ['country_id', 'city', 'zip']
|
||||
mandatory_fields.extend(additional_fields)
|
||||
if not partner.street and not partner.street2 :
|
||||
mandatory_fields.append('street')
|
||||
for field in mandatory_fields :
|
||||
if not getattr(partner, field) :
|
||||
missing_value.append(field)
|
||||
return missing_value
|
||||
|
||||
#Check required value proper or not for Shipping
|
||||
def check_required_value_to_ship(self, orders):
|
||||
for order in orders :
|
||||
if not order.order_line:
|
||||
return _("You have not any item to ship. Please provide item first")
|
||||
else :
|
||||
order_lines_without_weight = order.order_line.filtered(lambda line_item: not line_item.product_id.type in ['service', 'digital'] and not line_item.product_id.weight and not line_item.is_delivery)
|
||||
for order_line in order_lines_without_weight :
|
||||
return _("Please define weight in product : \n %s") % order_line.product_id.name
|
||||
|
||||
# validating customer address
|
||||
missing_value = self.validating_address(order.partner_shipping_id)
|
||||
if missing_value :
|
||||
fields = ", ".join(missing_value)
|
||||
return (_("Missing the values of the Customer address. \n Missing field(s) : %s ") % fields)
|
||||
|
||||
# validation shipper address
|
||||
missing_value = self.validating_address(order.warehouse_id.partner_id)
|
||||
if missing_value :
|
||||
fields = ", ".join(missing_value)
|
||||
return (_("Missing the values of the Warehouse address. \n Missing field(s) : %s ") % fields)
|
||||
return False
|
||||
|
||||
# Return Weight
|
||||
def convert_weight(self,from_uom_unit ,to_uom_unit, weight):
|
||||
if not from_uom_unit:
|
||||
from_uom_unit = self.env[
|
||||
"product.template"
|
||||
]._get_weight_uom_id_from_ir_config_parameter()
|
||||
return from_uom_unit._compute_quantity(weight, to_uom_unit)
|
||||
|
||||
#Check Validate weight or not
|
||||
def check_max_weight(self, order, shipment_weight):
|
||||
for order_line in order.order_line:
|
||||
if order_line.product_id and order_line.product_id.weight > shipment_weight:
|
||||
return (_("Product weight is more than maximum weight."))
|
||||
return False
|
||||
|
||||
#Get Rate from API than set Rate
|
||||
def canada_post_rate_shipment(self, order):
|
||||
# check the address validation
|
||||
check_value = self.check_required_value_to_ship(order)
|
||||
# check the product weight is appropriate to maximum weight.
|
||||
if check_value:
|
||||
return {'success': False, 'price': 0.0, 'error_message': check_value, 'warning_message': False}
|
||||
# check the product weight is appropriate to maximum weight.
|
||||
shipment_weight = self.product_packaging_id.max_weight
|
||||
check_weight = {}
|
||||
if shipment_weight!=0.0:
|
||||
check_weight = self.check_max_weight(order, shipment_weight)
|
||||
if check_weight:
|
||||
return {'success': False, 'price': 0.0, 'error_message': check_weight, 'warning_message': False}
|
||||
|
||||
shipper_address = order.warehouse_id.partner_id or order.company_id.partner_id
|
||||
recipient_address = order.partner_shipping_id or order.partner_id
|
||||
# Convert weight in to the delivery method's weight UOM
|
||||
carrier_ctx = self.env.context
|
||||
weight = carrier_ctx.get("order_weight", 0.0) or order._get_estimated_weight() or 0.0
|
||||
total_weight = self.convert_weight(order.company_id and order.company_id.weight_unit_of_measurement_id,
|
||||
self.weight_uom_id,
|
||||
weight)
|
||||
declared_value = round(order.amount_untaxed, 2)
|
||||
declared_currency = order.currency_id.name
|
||||
price=0.0
|
||||
rate_dict = self.canada_post_get_shipping_rate(shipper_address, recipient_address, total_weight,
|
||||
picking_bulk_weight=False, packages=False,
|
||||
declared_value=declared_value, declared_currency=declared_currency,
|
||||
company_id=order.company_id)
|
||||
_logger.info("Rate Response Data : %s" % (rate_dict))
|
||||
if rate_dict.get('messages', False):
|
||||
return {'success': False, 'price':price, 'error_message': rate_dict['messages']['message']['description'],
|
||||
'warning_message': False}
|
||||
if rate_dict.get('price-quotes',False) and rate_dict.get('price-quotes').get('price-quote',False) :
|
||||
cnt = 0;
|
||||
quotes = rate_dict['price-quotes']['price-quote']
|
||||
if isinstance(quotes, dict):
|
||||
quotes = [quotes]
|
||||
for quote in quotes:
|
||||
if quote['service-code']==self.service_type:
|
||||
price = quote['price-details']['due']
|
||||
cnt+=1
|
||||
if cnt==0:
|
||||
return {'success': False, 'price': price, 'error_message': "Rate API dosen't provide this service type price",
|
||||
'warning_message': False}
|
||||
|
||||
if self.canadapost_type == 'counter' and self.service_type in ['DOM.RP','DOM.EP','DOM.XP','DOM.PC'] and self.option_code == 'COD':
|
||||
if order.amount_total + float(price)>= 999.99:
|
||||
raise ValidationError(
|
||||
_("Get Rate Request Fail : \n The COD amount cannot exceed 1000.00 in Non-Contract Shipping.")
|
||||
)
|
||||
if self.canadapost_type == 'commercial' and self.service_type in ['DOM.RP','DOM.EP','DOM.XP','DOM.PC'] and self.option_code == 'COD':
|
||||
if order.amount_total + float(price) >= 5000.00:
|
||||
raise ValidationError(
|
||||
_("Get Rate Request Fail : \n The COD amount cannot exceed 5000.00 in Contract Shipping.")
|
||||
)
|
||||
return {'success': True, 'price': float(price), 'error_message': False, 'warning_message': False}
|
||||
|
||||
#Send required XML data and than response from Canada Post API
|
||||
def canada_post_get_shipping_rate(self, shipper_address, recipient_address, total_weight, picking_bulk_weight,
|
||||
packages=False, declared_value=False, declared_currency=False, company_id=False):
|
||||
result = {}
|
||||
# built request data
|
||||
service_root = etree.Element("mailing-scenario")
|
||||
|
||||
if self.canadapost_type == 'commercial':
|
||||
service_root.attrib["xmlns"] = "http://www.canadapost.ca/ws/ship/rate-v4"
|
||||
else:
|
||||
service_root.attrib["xmlns"] = "http://www.canadapost.ca/ws/ship/rate-v3"
|
||||
etree.SubElement(service_root, "customer-number").text = self.customer_number
|
||||
parcel = etree.SubElement(service_root, "parcel-characteristics")
|
||||
etree.SubElement(parcel, "weight").text = str(total_weight)
|
||||
|
||||
etree.SubElement(service_root, "origin-postal-code").text = "%s" % (shipper_address.zip.replace(" ","").upper())
|
||||
|
||||
destination = etree.SubElement(service_root, "destination")
|
||||
if str(self.service_type[:3])=='DOM':
|
||||
domestic = etree.SubElement(destination, "domestic")
|
||||
etree.SubElement(domestic, "postal-code").text = "%s" % (recipient_address.zip.replace(" ","").upper())
|
||||
elif str(self.service_type[:3])=='USA':
|
||||
united_states = etree.SubElement(destination, "united-states")
|
||||
etree.SubElement(united_states, "zip-code").text = "%s" % (recipient_address.zip.upper())
|
||||
|
||||
elif str(self.service_type[:3])=='INT':
|
||||
international = etree.SubElement(destination, "international")
|
||||
etree.SubElement(international, "country-code").text = "%s" % (recipient_address.country_id and recipient_address.country_id.code)
|
||||
|
||||
url='%sship/price'%(self.get_canadapost_url())
|
||||
|
||||
base_data = etree.tostring(service_root).decode('utf-8')
|
||||
|
||||
if self.canadapost_type == 'commercial':
|
||||
headers = {"Accept": "application/vnd.cpc.ship.rate-v4+xml","Content-Type":"application/vnd.cpc.ship.rate-v4+xml"}
|
||||
else:
|
||||
headers = {"Accept": "application/vnd.cpc.ship.rate-v3+xml","Content-Type":"application/vnd.cpc.ship.rate-v3+xml"}
|
||||
_logger.info("Rate Request Data : %s" % (base_data))
|
||||
try:
|
||||
response_body = request(method='POST', url=url, data=base_data, headers=headers, auth=(self.username,self.password))
|
||||
api = Response(response_body)
|
||||
result = api.dict()
|
||||
_logger.info("Rate Response Data : %s" % (result))
|
||||
except Exception as e:
|
||||
result['error_message'] = e.message
|
||||
return result
|
||||
return result
|
||||
|
||||
# Random generate string and return
|
||||
def get_group_id(self):
|
||||
size = 15
|
||||
chars = string.ascii_uppercase
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
# Create Shipment and return generate label and receipt from Canada post API
|
||||
def canada_post_send_shipping(self, pickings):
|
||||
response = []
|
||||
for picking in pickings:
|
||||
weight_get = picking.shipping_weight or picking.weight
|
||||
from_unit = picking.company_id and picking.company_id.weight_unit_of_measurement_id or ""
|
||||
total_weight = round(self.convert_weight(from_unit,
|
||||
self.weight_uom_id,
|
||||
weight_get), 2)
|
||||
# get package type
|
||||
package_type = self.product_packaging_id
|
||||
for stock_quant in picking.move_line_ids.result_package_id:
|
||||
if not stock_quant.package_type_id:
|
||||
package_type = self.product_packaging_id
|
||||
else:
|
||||
package_type = stock_quant.package_type_id
|
||||
break
|
||||
package_info = self.get_canadapost_parcel(package_type)
|
||||
# Get the address of the sender and recipient
|
||||
destination_address = picking.partner_id
|
||||
sender_address = picking.picking_type_id and picking.picking_type_id.warehouse_id and picking.picking_type_id.warehouse_id.partner_id
|
||||
if self.canadapost_type == 'commercial':
|
||||
root_node = etree.Element("shipment")
|
||||
root_node.attrib["xmlns"] = "http://www.canadapost.ca/ws/shipment-v8"
|
||||
|
||||
etree.SubElement(root_node, "transmit-shipment").text ="true"
|
||||
etree.SubElement(root_node, "provide-receipt-info").text ="true"
|
||||
else:
|
||||
root_node = etree.Element("non-contract-shipment")
|
||||
root_node.attrib["xmlns"] = "http://www.canadapost.ca/ws/ncshipment-v4"
|
||||
|
||||
etree.SubElement(root_node, "requested-shipping-point").text="%s"%(sender_address.zip.replace(" ","").upper() or "")
|
||||
|
||||
delivery_spec_node=etree.SubElement(root_node, "delivery-spec")
|
||||
etree.SubElement(delivery_spec_node, "service-code").text="%s"%(self.service_type)
|
||||
|
||||
sender_node = etree.SubElement(delivery_spec_node, "sender")
|
||||
etree.SubElement(sender_node, "company").text=sender_address.name
|
||||
etree.SubElement(sender_node, "contact-phone").text ="%s"%(sender_address.phone or "")
|
||||
address_details=etree.SubElement(sender_node, "address-details")
|
||||
etree.SubElement(address_details, "address-line-1").text =sender_address.street or ""
|
||||
etree.SubElement(address_details, "city").text =sender_address.city or ""
|
||||
etree.SubElement(address_details, "prov-state").text ="%s"%(sender_address.state_id and sender_address.state_id.code or "")
|
||||
if self.canadapost_type == 'commercial':
|
||||
etree.SubElement(address_details , "country-code").text = "%s" % (sender_address.country_id and sender_address.country_id.code or "")
|
||||
etree.SubElement(address_details, "postal-zip-code").text ="%s"%(sender_address.zip.replace(" ","").upper() or "")
|
||||
|
||||
destination_node = etree.SubElement(delivery_spec_node, "destination")
|
||||
etree.SubElement(destination_node, "name").text =destination_address.name
|
||||
etree.SubElement(destination_node, "company").text =destination_address.name
|
||||
etree.SubElement(destination_node, "client-voice-number").text = destination_address.phone
|
||||
destination_address_details = etree.SubElement(destination_node, "address-details")
|
||||
etree.SubElement(destination_address_details , "address-line-1").text =destination_address.street or ""
|
||||
etree.SubElement(destination_address_details , "city").text =destination_address.city or ""
|
||||
etree.SubElement(destination_address_details , "prov-state").text ="%s"%(destination_address.state_id and destination_address.state_id.code or "")
|
||||
etree.SubElement(destination_address_details , "country-code").text = "%s" % (destination_address.country_id and destination_address.country_id.code or "")
|
||||
etree.SubElement(destination_address_details , "postal-zip-code").text ="%s"%(destination_address.zip.replace(" ","").upper() or "")
|
||||
|
||||
if self.option_code and not self.service_type in ['USA.XP','USA.EP','INT.IP.SURF','INT.PW.PARCEL','INT.XP']:
|
||||
options = etree.SubElement(delivery_spec_node, "options")
|
||||
option = etree.SubElement(options, "option")
|
||||
etree.SubElement(option , "option-code").text = str(self.option_code or "")
|
||||
|
||||
|
||||
if self.option_code in ['PA18','PA19']:
|
||||
option_pa = etree.SubElement(options, "option")
|
||||
etree.SubElement(option_pa , "option-code").text = 'SO'
|
||||
|
||||
if self.option_code == 'COD' or self.option_code == 'COV':
|
||||
etree.SubElement(option , "option-amount").text = str(picking.sale_id.amount_total or "")
|
||||
etree.SubElement(option , "option-qualifier-1").text ="true"
|
||||
else:
|
||||
options = etree.SubElement(delivery_spec_node, "options")
|
||||
option_usa = etree.SubElement(options, "option")
|
||||
etree.SubElement(option_usa , "option-code").text = 'RASE'
|
||||
|
||||
|
||||
parcel_characteristics= etree.SubElement(delivery_spec_node, "parcel-characteristics")
|
||||
etree.SubElement(parcel_characteristics, "weight").text ="%s"%(total_weight)
|
||||
|
||||
dimensions= etree.SubElement(parcel_characteristics, "dimensions ")
|
||||
etree.SubElement(dimensions, "length").text ="%s"%(package_info.get('length', 1))
|
||||
etree.SubElement(dimensions, "width").text ="%s"%(package_info.get('width', 1))
|
||||
etree.SubElement(dimensions, "height").text ="%s"%(package_info.get('height', 1))
|
||||
|
||||
preferences= etree.SubElement(delivery_spec_node, "preferences")
|
||||
etree.SubElement(preferences, "show-packing-instructions").text ="true"
|
||||
|
||||
customs = etree.SubElement(delivery_spec_node, "customs")
|
||||
etree.SubElement(customs, "currency").text = str(picking.sale_id.currency_id.name)
|
||||
if picking.sale_id.currency_id.rate:
|
||||
rate=picking.sale_id.currency_id.rate
|
||||
rate=round(rate,2)
|
||||
etree.SubElement(customs, "conversion-from-cad").text = str(rate or '')
|
||||
etree.SubElement(customs, "reason-for-export").text = "%s"%(self.reason_for_export)
|
||||
|
||||
sku_list = etree.SubElement(customs, "sku-list")
|
||||
for move_line in picking.move_ids:
|
||||
item = etree.SubElement(sku_list,"item")
|
||||
etree.SubElement(item, "customs-description").text = str(move_line.product_id.name)
|
||||
etree.SubElement(item, "unit-weight").text = str(move_line.weight)
|
||||
etree.SubElement(item, "customs-value-per-unit").text = str(move_line.product_id.lst_price)
|
||||
etree.SubElement(item, "customs-number-of-units").text = str(int(move_line.product_uom_qty))
|
||||
|
||||
if self.canadapost_type == 'commercial':
|
||||
settlement_info = etree.SubElement(delivery_spec_node, "settlement-info")
|
||||
etree.SubElement(settlement_info, "contract-id").text = self.canadapost_contract_id
|
||||
etree.SubElement(settlement_info, "intended-method-of-payment").text = self.canadapost_payment_method
|
||||
api_url=self.get_canadapost_url()
|
||||
if self.canadapost_type == 'commercial':
|
||||
url="%s%s/%s/shipment"%(api_url,self.customer_number,self.customer_number)
|
||||
else:
|
||||
url="%s%s/ncshipment"%(api_url,self.customer_number)
|
||||
|
||||
base_data= etree.tostring(root_node).decode('utf-8')
|
||||
|
||||
if self.canadapost_type == 'commercial':
|
||||
headers = {"Accept": "application/vnd.cpc.shipment-v8+xml","Content-Type":"application/vnd.cpc.shipment-v8+xml","Accept-language":"en-CA"}
|
||||
else:
|
||||
headers = {"Accept": "application/vnd.cpc.ncshipment-v4+xml","Content-Type":"application/vnd.cpc.ncshipment-v4+xml","Accept-language":"en-CA"}
|
||||
#try:
|
||||
_logger.info("Create Shipment Request Data : %s" % (base_data))
|
||||
response_body = request(method='POST', url=url, data=base_data, headers=headers, auth=(self.username,self.password))
|
||||
if response_body.status_code == 200:
|
||||
api = Response(response_body)
|
||||
result = api.dict()
|
||||
_logger.info("Create Shipment Response Data : %s" % (result))
|
||||
else:
|
||||
error_code = "%s" % (response_body.status_code)
|
||||
error_message = response_body.reason
|
||||
message = error_code + " " + error_message
|
||||
|
||||
api = Response(response_body)
|
||||
result = api.dict()
|
||||
|
||||
if result['messages']['message']['description']:
|
||||
raise ValidationError(_("ShipmentRequest Fail : \n %s" % (result['messages']['message']['description'])))
|
||||
else:
|
||||
raise ValidationError(_("ShipmentRequest Fail : %s \n More Information \n %s" % (message, response_body.text)))
|
||||
if self.canadapost_type == 'commercial':
|
||||
if not result['shipment-info']['shipment-id'] or not result['shipment-info']['links']['link']:
|
||||
raise RedirectWarning("ShipmentRequest Fail \n More Information \n %s" % (result))
|
||||
|
||||
shipment_id=str(result['shipment-info']['shipment-id'])
|
||||
else:
|
||||
if not result['non-contract-shipment-info']['shipment-id'] or not result['non-contract-shipment-info']['links']['link']:
|
||||
raise RedirectWarning("ShipmentRequest Fail \n More Information \n %s" % (result))
|
||||
|
||||
shipment_id=str(result['non-contract-shipment-info']['shipment-id'])
|
||||
commercial_invoice_url_attchment=""
|
||||
commercial_invoice = False
|
||||
url_attchment=""
|
||||
|
||||
if self.canadapost_type == 'commercial':
|
||||
for link in result['shipment-info']['links']['link']:
|
||||
if link['_rel'] == 'label':
|
||||
url_attchment = link['_href']
|
||||
if link['_rel'] =='commercialInvoice':
|
||||
commercial_invoice_url_attchment = link['_href']
|
||||
commercial_invoice=True
|
||||
else:
|
||||
for link in result['non-contract-shipment-info']['links']['link']:
|
||||
if link['_rel'] == 'label':
|
||||
url_attchment = link['_href']
|
||||
if link['_rel'] =='commercialInvoice':
|
||||
commercial_invoice_url_attchment = link['_href']
|
||||
commercial_invoice=True
|
||||
headers_attchment = {'Accept': 'application/pdf'}
|
||||
try:
|
||||
attachment_response = request(method='GET', url=url_attchment, headers=headers_attchment, auth=(
|
||||
self.username, self.password))
|
||||
_logger.info("Label Response Data : %s" % (attachment_response))
|
||||
picking.message_post(attachments=[
|
||||
('Shipment Label - %s.PDF' % (shipment_id),
|
||||
attachment_response.content)])
|
||||
|
||||
if commercial_invoice:
|
||||
commercial_invoice_attachment_response = request(method='GET', url=commercial_invoice_url_attchment, headers=headers_attchment, auth=(
|
||||
self.username, self.password))
|
||||
picking.message_post(attachments=[
|
||||
('Shipment Commercial Invoice - %s.PDF' % (shipment_id),
|
||||
commercial_invoice_attachment_response.content)])
|
||||
except Exception as e:
|
||||
raise RedirectWarning(e)
|
||||
if self.canadapost_type == 'commercial':
|
||||
headers = {"Accept": "application/vnd.cpc.shipment-v8+xml","Accept-language":"en-CA"}
|
||||
url_receipt = "%s"%(self.get_canadapost_url())+ str(self.customer_number)+"/"+str(self.customer_number)+"/shipment/" + str(shipment_id) + "/receipt"
|
||||
else:
|
||||
url_receipt = "%s"%(self.get_canadapost_url())+ str(self.customer_number) +"/ncshipment/" + str(shipment_id) + "/receipt"
|
||||
try:
|
||||
receipt_response= request(method='GET', url=url_receipt, headers=headers, auth=(self.username,self.password))
|
||||
if receipt_response.status_code == 200:
|
||||
api_receipt = Response(receipt_response)
|
||||
result_receipt = api_receipt.dict()
|
||||
_logger.info("Get shipment Detail Response Data : %s" % (result_receipt))
|
||||
else:
|
||||
result_receipt = {}
|
||||
error_code = "%s" % (receipt_response.status_code)
|
||||
error_message = response_body.reason
|
||||
message = error_code + " " + error_message
|
||||
mesage="ShipmentAcceptRequest Fail : %s \n More Information \n %s" % (message, response_body.text)
|
||||
picking.message_post(body=mesage)
|
||||
except Exception as e:
|
||||
picking.message_post(body=e)
|
||||
if self.canadapost_type == 'commercial':
|
||||
if result_receipt:
|
||||
extra_price = result_receipt['shipment-receipt'] and result_receipt['shipment-receipt']['cc-receipt-details'] and result_receipt['shipment-receipt']['cc-receipt-details']['charge-amount'] or 0.0
|
||||
else:
|
||||
extra_price = 0.0
|
||||
tracking_pin= result['shipment-info'].get('tracking-pin',False)
|
||||
else:
|
||||
if result_receipt:
|
||||
extra_price = result_receipt['non-contract-shipment-receipt'] and result_receipt['non-contract-shipment-receipt']['cc-receipt-details'] and result_receipt['non-contract-shipment-receipt']['cc-receipt-details']['charge-amount'] or 0.0
|
||||
else:
|
||||
extra_price = 0.0
|
||||
tracking_pin= result['non-contract-shipment-info'].get('tracking-pin',False)
|
||||
if not tracking_pin:
|
||||
_logger.info("This Service not provide the tracking no.Service is : %s"%self.service_type)
|
||||
shipping_data = {
|
||||
'exact_price': float(extra_price) or 0.0,
|
||||
'tracking_number': tracking_pin}
|
||||
response += [shipping_data]
|
||||
return response
|
||||
|
||||
# Tracking link return
|
||||
def canada_post_get_tracking_link(self, picking):
|
||||
link = picking.carrier_id.tracking_link or 'https://www.canadapost.ca/trackweb/en#/resultList?searchFor='
|
||||
res = '%s %s' % (link, picking.carrier_tracking_ref)
|
||||
return res
|
||||
|
||||
def canada_post_cancel_shipment(self, picking):
|
||||
raise ValidationError(_("Canada Post does not provide Shipment Cancel API!"))
|
||||
|
||||
def get_canadapost_parcel(self, package):
|
||||
packaging_length = (
|
||||
self.sudo()._canadapost_convert_dimension_to_uom(
|
||||
package.packaging_length, package.length_uom_name
|
||||
)
|
||||
)
|
||||
width = (
|
||||
self.sudo()._canadapost_convert_dimension_to_uom(
|
||||
package.width, package.length_uom_name
|
||||
)
|
||||
)
|
||||
height = (
|
||||
self.sudo()._canadapost_convert_dimension_to_uom(
|
||||
package.height, package.length_uom_name
|
||||
)
|
||||
)
|
||||
return {
|
||||
"length": packaging_length,
|
||||
"width": width,
|
||||
"height": height
|
||||
}
|
||||
|
||||
def _canadapost_convert_dimension_to_uom(self, dimension, length_uom_name):
|
||||
target_uom = self.env.ref("uom.product_uom_cm")
|
||||
from_uom = self.env["uom.uom"].sudo().search([("name", "=", length_uom_name)])
|
||||
if not from_uom:
|
||||
from_uom = self.env[
|
||||
"product.template"
|
||||
]._get_length_uom_id_from_ir_config_parameter()
|
||||
# Convert dimensions
|
||||
return from_uom._compute_quantity(dimension, target_uom)
|
||||
@@ -1,7 +0,0 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PackageType(models.Model):
|
||||
_inherit = "stock.package.type"
|
||||
|
||||
package_carrier_type = fields.Selection([('canada_post', 'Canada Post')])
|
||||
|
Before Width: | Height: | Size: 830 KiB |
|
Before Width: | Height: | Size: 968 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 983 KiB |
|
Before Width: | Height: | Size: 917 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -1,321 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="custom.css" rel="stylesheet"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="google-site-verification" content="NGpd7kJ1PumTrCwdhQdBLlYQDa7HzofXBtuQ4cJCtHw"/>
|
||||
<title>Canada Post Shipping Integration with Odoo</title>
|
||||
<meta name="description" content="Odoo Canada Post shipping Shipping gatway. Canada Post Shipping connector Odoo"/>
|
||||
<meta name="keywords" content="shipping connector,Odoo Canada Post gatway, Delivery Canada Post, Odoo Canada Post shipping, Canada Post Shipping located across Global, Canada Post Shipping connect,
|
||||
integrate Canada Post ,Canada Post odoo, Canada Post store, shipping solution to your site, accept shipment odoo, accept shipping odoo, sync shipment odoo, sync order odoo, Canada Post gateway settings,Canada Post Shipping gateway integration, Shipping Gateway Providers In Global,
|
||||
Canada Post: The New Digital Shipping Solution for Retailer"/>
|
||||
<meta name="robots" content="index, follow"/>
|
||||
<link type="text/css" rel="stylesheet" href="/assets/assets.css"/>
|
||||
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"/>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.js"
|
||||
integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
|
||||
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Laila:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
|
||||
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="top"></div>
|
||||
<section class="oe_container bg-transparent fixed-top position-relative">
|
||||
<div class="o_equal_col_sm">
|
||||
<div class="shadow bg-white pb32 pt32 px-2"
|
||||
style="border-radius: 15px">
|
||||
|
||||
<h3 class="oe_slogan"
|
||||
style="color: #051f5a; font-family: Raleway; font-weight: 700; text-align: center; opacity: 1; font-size: 35px; margin-bottom: 25px; margin-top: 45px;">
|
||||
Odoo Canada Post Shipping Integration</h3>
|
||||
<img src="dash.png" style="width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container overflow-hidden"
|
||||
style="position-relative">
|
||||
<div class="o_equal_col_sm">
|
||||
<div class="px-2" style="border-radius:15px">
|
||||
<h3 class="oe_slogan"
|
||||
style="color:#0f1e40; font-family:Montserrat; font-weight:700; text-align:center; text-transform:uppercase; opacity:1; font-size:28px; margin-bottom:20px; margin-top:20px">
|
||||
<span style="color:#2971FD">Why</span> Canada Post Odoo Shipping Integration App From Synodica?
|
||||
</h3>
|
||||
<p class="mb16 text-center text-black-dark px-3"
|
||||
style="font-family:; font-weight:normal; color:#11335b; font-size:18px">
|
||||
Integrate Canada Post streamline shipping processes by obtaining <b>real-time courier quotes for
|
||||
various services, generating shipping labels based on order details</b>, and providing <b>tracking
|
||||
numbers with
|
||||
links for shipment updates</b>
|
||||
ensuring a seamless and efficient logistics workflow
|
||||
</p>
|
||||
<div class="pb32 pt32" style="display:block; margin:0 auto; text-align:center">
|
||||
<div class="d-inline-block mx-3 my-3" style="max-width:348px">
|
||||
<div class="s_panel_video position-relative text-center" data-video-id="w17dnsMTt8M">
|
||||
<h3 class="oe_slogan"
|
||||
style="color:#091E42; font-family:Montserrat; font-weight:600; text-align:center; font-size:18px; opacity:1; margin:8px 0 16px">
|
||||
WATCH QUICK DEMO HERE
|
||||
</h3>
|
||||
|
||||
<!-- Play Button Centered -->
|
||||
<a target="_new" href="https://youtu.be/MIt4t8h71aQ"
|
||||
class="position-absolute top-50 start-50 translate-middle" style="padding-top: 50px;">
|
||||
<img class="img img-fluid" src="play_button.png" alt="Play Button">
|
||||
</a>
|
||||
|
||||
<!-- Thumbnail -->
|
||||
<a target="_new" href="https://youtu.be/MIt4t8h71aQ">
|
||||
<img class="img img-fluid shadow" src="canadapost_thumbnail.png"
|
||||
style="border-radius:10px; width:100%;">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container pb-5">
|
||||
<div class="mt64 mb64">
|
||||
<h2 style="color:#091E42; font-family:'Raleway'; text-align:center; margin:25px auto; text-transform:uppercase"
|
||||
class="oe_slogan">
|
||||
<b>Features</b>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 mt32">
|
||||
<div class="container shadow" style="border-radius:10px; padding:10px 0px">
|
||||
<div class="col-md-3 shadow" style="float:left; margin: -10px 0px;">
|
||||
<img class="img img-responsive"
|
||||
src="images/icons_live_price.png"
|
||||
style="width: 100%;height: auto;"></div>
|
||||
<div class="col-md-9" style="padding-left:0; float:left; width:70%;margin-left: 20px;">
|
||||
<h3 class="mt16 mb0" style="font-family:Roboto; font-weight:500; font-size:22px">Live Shipping
|
||||
Price</h3>
|
||||
<p class=" mt8" style="font-family:Roboto;">Get live price to ensure the best price for
|
||||
your shipment</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 mt32">
|
||||
<div class="container shadow" style="border-radius:10px; padding:10px 0px">
|
||||
<div class="col-md-3 shadow" style="float:left; margin: -10px 0px;">
|
||||
<img class="img img-responsive"
|
||||
src="images/icons_shipping_label.png"
|
||||
style="width: 100%;height: auto;"></div>
|
||||
<div class="col-md-9" style="padding-left:0; float:left; width:70%;margin-left: 20px;">
|
||||
<h3 class="mt16 mb0" style="font-family:Roboto; font-weight:500; font-size:22px">Shipping
|
||||
Label</h3>
|
||||
<p class="mt8" style="font-family:Roboto;">Generate shipping label using order information</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 mt32" style="margin: 0px auto;padding-top: 22px;">
|
||||
<div class="container shadow" style="border-radius:10px; padding:10px 0px">
|
||||
<div class="col-md-3 shadow" style="float:left; margin: -10px 0px;"><img class="img img-responsive"
|
||||
src="images/icons_track_and_trace.png"
|
||||
style="width: 100%;height: auto;">
|
||||
</div>
|
||||
<div class="col-md-9" style="padding-left:0; float:left; width:70%;margin-left: 20px;">
|
||||
<h3 class="mt16 mb0" style="font-family:Roboto; font-weight:500; font-size:22px">Track &
|
||||
Trace</h3>
|
||||
<p class="mt8 mid_p_1200" style="font-family:Roboto;">Live track your shipment using the Canada
|
||||
Post tracking number</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="container">
|
||||
<div class="oe_row oe_spaced mt8 mb8" style="width: auto;">
|
||||
<h3 class="oe_slogan">Canada Post Shipping In Configuration</h3>
|
||||
<div class="mt16 mb16" style="margin-left: 10px">
|
||||
<img src="S1.png"
|
||||
style="width: 100%; margin-top: 10px; border-radius: 5px; border: solid #E3E4E4; border-radius: 10px; border-width: 30px 7px 7px 7px;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<div class="oe_row oe_spaced mt8 mb8" style="width: auto;">
|
||||
<h3 class="oe_slogan">Create Sale Order With Canada Post Delivery Method</h3>
|
||||
<div class="mt16 mb16" style="margin-left: 10px">
|
||||
<img src="S2.png"
|
||||
style="width: 100%; margin-top: 10px; border-radius: 5px; border: solid #E3E4E4; border-radius: 10px; border-width: 30px 7px 7px 7px;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<div class="oe_row oe_spaced mt8 mb8" style="width: auto;">
|
||||
<h3 class="oe_slogan">Generate Shipment With Tracking Number And Shipping Label</h3>
|
||||
<div class="mt16 mb16" style="margin-left: 10px">
|
||||
<img src="S3.png"
|
||||
style="width: 100%; margin-top: 10px; border-radius: 5px; border: solid #E3E4E4; border-radius: 10px; border-width: 30px 7px 7px 7px;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<div class="oe_row oe_spaced mt8 mb8" style="width: auto;">
|
||||
<h3 class="oe_slogan">Generated Shipment Label</h3>
|
||||
<div class="mt16 mb16" style="margin: 0px auto;text-align: center;">
|
||||
<img src="label.png"
|
||||
style="margin-top: 10px; border-radius: 5px; border: solid #E3E4E4; border-radius: 10px; border-width: 7px 7px 7px 7px;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="container">
|
||||
<div class="oe_row oe_spaced mt8 mb8" style="width: auto;">
|
||||
<div class="mt16 mb16" style="margin-left: 10px">
|
||||
<img src="synodica_services.png" style="width: 100%; height: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Carousel -->
|
||||
<div class="row" style="padding: 0px 60px;">
|
||||
<section class="container">
|
||||
<h2 style="color:#00008b; text-align:center; margin:25px auto; text-transform:uppercase;font-size: 50px;font-weight: 600;"
|
||||
class="oe_slogan">
|
||||
Suggested Products
|
||||
</h2>
|
||||
<div id="suggested_products" class="row carousel slide mt64 mb32" data-bs-ride="carousel">
|
||||
<!-- The slideshow -->
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active" style="min-height: 292.4px;">
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left">
|
||||
<a href="https://apps.odoo.com/apps/modules/17.0/odoo_advance_search/">
|
||||
<div style="border-radius:10px" class="shadow-sm">
|
||||
<img class="img img-responsive center-block"
|
||||
style="border-top-left-radius:10px; border-top-right-radius:10px;height: 200px;width: -webkit-fill-available;width: -moz-available;width: -webkit-fill-available;"
|
||||
src="advanced_search.gif">
|
||||
<h4 class="mt0 text-truncate"
|
||||
style="padding:6% 4%; text-align:center; width:100%">
|
||||
Odoo Advanced Search
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left;padding-left: 20px;">
|
||||
<a href="https://apps.odoo.com/apps/modules/17.0/delivery_dpd_shipping/">
|
||||
<div style="border-radius:10px" class="shadow-sm">
|
||||
<img class="img img-responsive center-block"
|
||||
style="border-top-left-radius:10px; border-top-right-radius:10px;height: 200px;width: -webkit-fill-available;width: -moz-available;width: -webkit-fill-available;"
|
||||
src="dpd.gif">
|
||||
<h4 class="mt0 text-truncate"
|
||||
style="padding:6% 4%; text-align:center; width:100%;">
|
||||
DPD Shipping
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left;padding-left: 20px;">
|
||||
<a href="https://apps.odoo.com/apps/modules/17.0/delivery_shippo_ss/">
|
||||
<div style="border-radius:10px" class="shadow-sm">
|
||||
<img class="img img-responsive center-block"
|
||||
style="border-top-left-radius:10px; border-top-right-radius:10px;height: 200px;width: -webkit-fill-available;width: -moz-available;width: -webkit-fill-available;"
|
||||
src="GoShippo.gif">
|
||||
<h4 class="mt0 text-truncate"
|
||||
style="padding:6% 4%; text-align:center; width:100%">
|
||||
GoShippo Shipping
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item" style="min-height: 292.4px;">
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left">
|
||||
<a href="https://apps.odoo.com/apps/modules/17.0/delivery_dsv_express/">
|
||||
<div style="border-radius:10px" class="shadow-sm">
|
||||
<img class="img img-responsive center-block"
|
||||
style="border-top-left-radius:10px; border-top-right-radius:10px;height: 200px;width: -webkit-fill-available;width: -moz-available;width: -webkit-fill-available;"
|
||||
src="dsv_banner.gif">
|
||||
<h4 class="mt0 text-truncate"
|
||||
style="padding:6% 4%; text-align:center; width:100%">
|
||||
DSV Express Shipping
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left;padding-left: 20px;">
|
||||
<a href="https://apps.odoo.com/apps/modules/17.0/delivery_chitchats/">
|
||||
<div style="border-radius:10px" class="shadow-sm">
|
||||
<img class="img img-responsive center-block"
|
||||
style="border-top-left-radius:10px; border-top-right-radius:10px;height: 200px;width: -webkit-fill-available;width: -moz-available;width: -webkit-fill-available;"
|
||||
src="chitchat_shipping.gif">
|
||||
<h4 class="mt0 text-truncate"
|
||||
style="padding:6% 4%; text-align:center; width:100%">
|
||||
ChitChats Shipping
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left;padding-left: 20px;">
|
||||
<a href="https://apps.odoo.com/apps/modules/17.0/delivery_shippit_ss/">
|
||||
<div style="border-radius:10px" class="shadow-sm">
|
||||
<img class="img img-responsive center-block"
|
||||
style="border-top-left-radius:10px; border-top-right-radius:10px;height: 200px;width: -webkit-fill-available;width: -moz-available;width: -webkit-fill-available;"
|
||||
src="Shippit.gif">
|
||||
<h4 class="mt0 text-truncate"
|
||||
style="padding:6% 4%; text-align:center; width:100%">
|
||||
Shippit Shipping
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Left and right controls -->
|
||||
<a class="carousel-control-prev" href="#suggested_products" data-bs-slide="prev"
|
||||
style="width:35px; color:#000; margin-left:-30px">
|
||||
<span class="carousel-control-prev-icon"><i class="fa fa-chevron-left"
|
||||
style="font-size:24px"></i></span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#suggested_products" data-bs-slide="next"
|
||||
style="width:35px; color:#000; margin-right:-30px">
|
||||
<span class="carousel-control-next-icon"><i class="fa fa-chevron-right"
|
||||
style="font-size:24px"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- End Carousel -->
|
||||
|
||||
<div class="container">
|
||||
<section class="container">
|
||||
<h4 style="color:#00008b; text-align:center; margin:25px auto; text-transform:uppercase;font-weight: 600;"
|
||||
class="oe_slogan">
|
||||
Contact Us
|
||||
</h4>
|
||||
<div style="text-align:center">
|
||||
<span>Suggestions & Feedback to:</span> <a href="mailto:support@synodica.com">support@synodica.com </a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="container" align="center">
|
||||
<section class="container">
|
||||
<a href="#top">
|
||||
<img src="up_arrow.png" alt="up-arrow" style="height: 125px;">
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.0 MiB |
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="delivery_carrier_form_view_canada_post" model="ir.ui.view">
|
||||
<field name="name">delivery.carrier.form.view.canada.post</field>
|
||||
<field name="model">delivery.carrier</field>
|
||||
<field name="inherit_id" ref="delivery.view_delivery_carrier_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='destination']" position='before'>
|
||||
<page string="Configuration" name="configuration" invisible="delivery_type != 'canada_post'">
|
||||
<p class="alert alert-danger" role="alert" invisible="delivery_type != 'canada_post'">
|
||||
Note : Weight UOM must be select KG in Canada Post provider.
|
||||
</p>
|
||||
<group>
|
||||
<group>
|
||||
<field name="canadapost_type" required="delivery_type == 'canada_post'"/>
|
||||
<field name="canadapost_contract_id" required="canadapost_type == 'commercial'"
|
||||
invisible="canadapost_type == 'counter'"/>
|
||||
<field name="service_type" required="delivery_type == 'canada_post'"/>
|
||||
<field name="option_code"/>
|
||||
<field name="reason_for_export"/>
|
||||
<field name="product_packaging_id"
|
||||
required="delivery_type == 'canada_post'"
|
||||
/>
|
||||
</group>
|
||||
<group string="Shipping Option">
|
||||
<field
|
||||
name="canadapost_payment_method"
|
||||
string="Payment Method"
|
||||
required="delivery_type == 'canada_post'"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group invisible="delivery_type != 'canada_post'">
|
||||
<field name="username" required="delivery_type == 'canada_post'"/>
|
||||
<field name="password" password="True"
|
||||
required="delivery_type == 'canada_post'"/>
|
||||
<field name="customer_number" required="delivery_type == 'canada_post'"/>
|
||||
<field name="tracking_link"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
localhost FALSE / FALSE 1804532345 frontend_lang en_CA
|
||||
#HttpOnly_localhost FALSE / FALSE 1773601137 session_id 70Wf5yZpnwpU0Izf5wBSbiWNk7UjsoGB1737H73bWDK16z05MJP0SJnNN-NhOfw8GbW6a-d_-y0opbJwcgbq
|
||||
@@ -1,230 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Internal Server Error</title>
|
||||
|
||||
<link rel="stylesheet" href="/web/static/lib/bootstrap/dist/css/bootstrap.css"/>
|
||||
<script src="/web/static/lib/jquery/jquery.js" type="text/javascript"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/index.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/data.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/event-handler.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/manipulator.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/selector-engine.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/config.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/base-component.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/component-functions.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/backdrop.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/focustrap.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/scrollbar.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/modal.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/collapse.js"></script>
|
||||
<style>
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var button = $('.reset_templates_button');
|
||||
button.click(function() {
|
||||
$('#reset_templates_mode').val($(this).data('mode'));
|
||||
var dialog = $('#reset_template_confirmation').modal('show');
|
||||
var input = dialog.find('input[type="text"]').val('').focus();
|
||||
var dialog_form = dialog.find('form');
|
||||
dialog_form.submit(function() {
|
||||
if (input.val() == dialog.find('.confirm_word').text()) {
|
||||
dialog.modal('hide');
|
||||
button.prop('disabled', true).text('Working...');
|
||||
const id = document.querySelector('input[id="reset_templates_view_id"]').value;
|
||||
const redirect = document.querySelector('input[name="redirect"]').value;
|
||||
const mode = document.querySelector('input[id="reset_templates_mode"]').value;
|
||||
fetch('/website/reset_template', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
'body': JSON.stringify({'params': {'view_id': id, 'mode': mode}})
|
||||
}).then(() => window.location = redirect);
|
||||
} else {
|
||||
input.val('').focus();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" data-oe-id="8693" data-oe-xpath="/data/xpath[3]/div" data-oe-model="ir.ui.view" data-oe-field="arch">
|
||||
<div class="modal-dialog">
|
||||
<form role="form">
|
||||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Reset templates</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<div class="row mb0">
|
||||
<label for="page-name" class="col-md-12 col-form-label">
|
||||
<p>The selected templates will be reset to their factory settings.</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row mb0">
|
||||
<label for="page-name" class="col-md-9 col-form-label">
|
||||
<p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p>
|
||||
</label>
|
||||
<div class="col-md-3 mt16">
|
||||
<input type="text" id="page-name" class="form-control" required="required" placeholder="yes"/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<button type="button" class="btn" data-bs-dismiss="modal" aria-label="Cancel">Cancel</button>
|
||||
<input type="submit" value="Confirm" class="btn btn-primary"/>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wrapwrap">
|
||||
<header data-oe-model="ir.ui.view" data-oe-id="294" data-oe-field="arch" data-oe-xpath="/t[1]/html[1]/body[1]/div[1]/header[1]">
|
||||
<div class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<div class="container">
|
||||
<div class="collapse navbar-collapse navbar-top-collapse">
|
||||
<ul class="navbar-nav ms-auto" id="top_menu">
|
||||
<li class="nav-item"><a href="/" class="nav-link">Home</a></li>
|
||||
<li class="nav-item"><a href="javascript: window.history.back()" class="nav-link">Back</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div id="error_message" class="oe_structure">
|
||||
<h2 class="container mt32">500: Internal Server Error</h2>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/h4[1]">Template fallback</h4>
|
||||
<p>An error occurred while rendering the template <code>fusion_quotations.portal_quotation_list</code>.</p>
|
||||
<p data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/p[2]">If this error is caused by a change of yours in the templates, you have the possibility to reset the template to its <strong>factory settings</strong>.</p>
|
||||
<form action="#" method="post" id="reset_templates_form">
|
||||
<ul>
|
||||
<li>
|
||||
<label>
|
||||
Equipment Assessments List
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<input type="hidden" name="redirect" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/input[1]" value="/my/quotation/builder"/>
|
||||
<input type="hidden" id="reset_templates_view_id" name="view_id" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/input[2]" value="13488"/>
|
||||
<input type="hidden" id="reset_templates_mode" name="mode" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/input[3]"/>
|
||||
<button data-mode="soft" class="reset_templates_button btn btn-info" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/button[1]">Restore previous version (soft reset).</button>
|
||||
<button data-mode="hard" class="reset_templates_button btn btn-outline-danger" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/button[2]">Reset to initial version (hard reset).</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container accordion mb32 mt32" id="debug_infos">
|
||||
<div class="card">
|
||||
<h4 class="card-header" data-oe-model="ir.ui.view" data-oe-id="290" data-oe-field="arch" data-oe-xpath="/t[1]/div[1]/div[2]/h4[1]">
|
||||
<a data-bs-toggle="collapse" href="#error_qweb">QWeb</a>
|
||||
</h4>
|
||||
<div id="error_qweb" class="collapse show">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
The error occurred while rendering the template <code>fusion_quotations.portal_quotation_list</code>
|
||||
and evaluating the following expression: <code><t t-out="dict(a._fields[\'equipment_type\'].selection).get(a.equipment_type, a.equipment_type or \'\')"/></code>
|
||||
</p>
|
||||
<pre>Error while rendering the template:
|
||||
ValueError: dictionary update sequence element #0 has length 1; 2 is required
|
||||
Template: fusion_quotations.portal_quotation_list
|
||||
Reference: 13488
|
||||
Path: /t/t/div/t[2]/table/tbody/t/tr/td[3]/t
|
||||
Element: <t t-out="dict(a._fields[\'equipment_type\'].selection).get(a.equipment_type, a.equipment_type or \'\')"/>
|
||||
From: (13488, '/t/t', '<t t-call="portal.portal_layout"/>')
|
||||
(13488, '/t/t/div/t[2]/table/tbody/t/tr/td[3]/t', '<t t-out="dict(a._fields[\\\'equipment_type\\\'].selection).get(a.equipment_type, a.equipment_type or \\\'\\\')"/>')</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4 class="card-header" data-oe-model="ir.ui.view" data-oe-id="290" data-oe-field="arch" data-oe-xpath="/t[1]/div[1]/div[3]/h4[1]">
|
||||
<a data-bs-toggle="collapse" href="#error_traceback">Traceback</a>
|
||||
</h4>
|
||||
<div id="error_traceback" class="collapse ">
|
||||
<div class="card-body">
|
||||
<pre id="exception_traceback">Traceback (most recent call last):
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py", line 753, in _render_iterall
|
||||
for item in frame.iterator:
|
||||
File "<13488>", line 152, in template_fusion_quotations_portal_quotation_list_13488_t_call_0
|
||||
ValueError: dictionary update sequence element #0 has length 1; 2 is required
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/usr/lib/python3/dist-packages/odoo/http.py", line 2275, in _serve_db
|
||||
return service_model.retrying(serve_func, env=self.env)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/service/model.py", line 184, in retrying
|
||||
result = func()
|
||||
^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/http.py", line 2330, in _serve_ir_http
|
||||
response = self.dispatcher.dispatch(rule.endpoint, args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/http.py", line 2452, in dispatch
|
||||
return self.request.registry['ir.http']._dispatch(endpoint)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_http.py", line 357, in _dispatch
|
||||
result.flatten()
|
||||
File "/usr/lib/python3/dist-packages/odoo/tools/facade.py", line 83, in wrap_func
|
||||
func(self._wrapped__, *args, **kwargs)
|
||||
File "/usr/lib/python3/dist-packages/odoo/http.py", line 1546, in flatten
|
||||
self.response.append(self.render())
|
||||
^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/http.py", line 1538, in render
|
||||
return request.env["ir.ui.view"]._render_template(self.template, self.qcontext)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/website/models/ir_ui_view.py", line 456, in _render_template
|
||||
return super()._render_template(template, values=values)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_ui_view.py", line 2531, in _render_template
|
||||
return self.env['ir.qweb']._render(template, values)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/mnt/enterprise-addons/web_studio/models/ir_qweb.py", line 14, in _render
|
||||
return super()._render(template, values, **options)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py", line 725, in _render
|
||||
return Markup(''.join(iterator))
|
||||
^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py", line 753, in _render_iterall
|
||||
for item in frame.iterator:
|
||||
File "<13488>", line 264, in template_fusion_quotations_portal_quotation_list_13488
|
||||
File "<13488>", line 250, in template_fusion_quotations_portal_quotation_list_13488_content
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py", line 616, in __str__
|
||||
self.html = ''.join(self.irQweb._render_iterall(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py", line 847, in _render_iterall
|
||||
raise QWebError(qweb_error_info) from error
|
||||
odoo.addons.base.models.ir_qweb.QWebError: Error while rendering the template:
|
||||
ValueError: dictionary update sequence element #0 has length 1; 2 is required
|
||||
Template: fusion_quotations.portal_quotation_list
|
||||
Reference: 13488
|
||||
Path: /t/t/div/t[2]/table/tbody/t/tr/td[3]/t
|
||||
Element: <t t-out="dict(a._fields[\'equipment_type\'].selection).get(a.equipment_type, a.equipment_type or \'\')"/>
|
||||
From: (13488, '/t/t', '<t t-call="portal.portal_layout"/>')
|
||||
(13488, '/t/t/div/t[2]/table/tbody/t/tr/td[3]/t', '<t t-out="dict(a._fields[\\\'equipment_type\\\'].selection).get(a.equipment_type, a.equipment_type or \\\'\\\')"/>')
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -79,7 +79,6 @@ This module provides external portal access for:
|
||||
'views/portal_accessibility_forms.xml',
|
||||
'views/portal_technician_templates.xml',
|
||||
'views/portal_book_assessment.xml',
|
||||
'views/portal_repair_form.xml',
|
||||
'views/portal_schedule.xml',
|
||||
'views/portal_page11_sign_templates.xml',
|
||||
],
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
from . import portal_main
|
||||
from . import portal_assessment
|
||||
from . import pdf_editor
|
||||
from . import portal_repair
|
||||
from . import portal_schedule
|
||||
from . import portal_page11_sign
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 494 KiB After Width: | Height: | Size: 494 KiB |