This commit is contained in:
gsinghpal
2026-03-11 12:15:53 -04:00
parent f81e0cd918
commit db4b9aa278
1210 changed files with 173089 additions and 4044 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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},
}

View File

@@ -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',
}

View File

@@ -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

View File

@@ -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',
},
}

View File

@@ -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()

View File

@@ -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},
}

View File

@@ -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},
}

View File

@@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fusion_fax_user fusion.fax.user model_fusion_fax group_fax_user 1 1 1 0
3 access_fusion_fax_manager fusion.fax.manager model_fusion_fax group_fax_manager 1 1 1 1
4 access_fusion_send_fax_wizard_user fusion.send.fax.wizard.user model_fusion_faxes_send_fax_wizard group_fax_user 1 1 1 1
5 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
6 access_fusion_fax_document_user fusion.fax.document.user model_fusion_fax_document group_fax_user 1 1 1 0
7 access_fusion_fax_document_manager fusion.fax.document.manager model_fusion_fax_document group_fax_manager 1 1 1 1
8 access_fusion_fax_dashboard_user fusion.fax.dashboard.user model_fusion_fax_dashboard group_fax_user 1 1 1 1

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- User group: can send faxes and view own fax history -->
<record id="group_fax_user" model="res.groups">
<field name="name">Fusion Faxes / User</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- Manager group: can view all faxes and configure settings -->
<record id="group_fax_manager" model="res.groups">
<field name="name">Fusion Faxes / Manager</field>
<field name="implied_ids" eval="[(4, ref('group_fax_user'))]"/>
</record>
<!-- Record rules -->
<!-- Users see only their own faxes -->
<record id="rule_fax_user_own" model="ir.rule">
<field name="name">Fax: user sees own faxes</field>
<field name="model_id" ref="model_fusion_fax"/>
<field name="domain_force">[('sent_by_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fax_user'))]"/>
</record>
<!-- Managers see all faxes -->
<record id="rule_fax_manager_all" model="ir.rule">
<field name="name">Fax: manager sees all faxes</field>
<field name="model_id" ref="model_fusion_fax"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_fax_manager'))]"/>
</record>
</data>
</odoo>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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'},
},
}

View File

@@ -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

View File

@@ -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>

Binary file not shown.

View File

@@ -1,7 +0,0 @@
# Synodica Solutions
## Changelog
### 16.0.0.0.0
- Initial version

View File

@@ -1,2 +0,0 @@
from . import models
from . import canada_post_api

View File

@@ -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",
}

View File

@@ -1,2 +0,0 @@
from . import canada_post_response
from . import utils

View File

@@ -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>

View File

@@ -1,3 +0,0 @@
from . import delivery_carrier
from . import product_packaging
from . import res_company

View File

@@ -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 Senders 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 Intl'),
('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)

View File

@@ -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')])

Binary file not shown.

Before

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -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 &amp; 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

View File

@@ -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>

View File

@@ -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

View File

@@ -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>&lt;t t-out=&#34;dict(a._fields[\&#39;equipment_type\&#39;].selection).get(a.equipment_type, a.equipment_type or \&#39;\&#39;)&#34;/&gt;</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: &lt;t t-out=&#34;dict(a._fields[\&#39;equipment_type\&#39;].selection).get(a.equipment_type, a.equipment_type or \&#39;\&#39;)&#34;/&gt;
From: (13488, &#39;/t/t&#39;, &#39;&lt;t t-call=&#34;portal.portal_layout&#34;/&gt;&#39;)
(13488, &#39;/t/t/div/t[2]/table/tbody/t/tr/td[3]/t&#39;, &#39;&lt;t t-out=&#34;dict(a._fields[\\\&#39;equipment_type\\\&#39;].selection).get(a.equipment_type, a.equipment_type or \\\&#39;\\\&#39;)&#34;/&gt;&#39;)</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 &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 753, in _render_iterall
for item in frame.iterator:
File &#34;&lt;13488&gt;&#34;, 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 &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 2275, in _serve_db
return service_model.retrying(serve_func, env=self.env)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/service/model.py&#34;, line 184, in retrying
result = func()
^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 2330, in _serve_ir_http
response = self.dispatcher.dispatch(rule.endpoint, args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 2452, in dispatch
return self.request.registry[&#39;ir.http&#39;]._dispatch(endpoint)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_http.py&#34;, line 357, in _dispatch
result.flatten()
File &#34;/usr/lib/python3/dist-packages/odoo/tools/facade.py&#34;, line 83, in wrap_func
func(self._wrapped__, *args, **kwargs)
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 1546, in flatten
self.response.append(self.render())
^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 1538, in render
return request.env[&#34;ir.ui.view&#34;]._render_template(self.template, self.qcontext)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/website/models/ir_ui_view.py&#34;, line 456, in _render_template
return super()._render_template(template, values=values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_ui_view.py&#34;, line 2531, in _render_template
return self.env[&#39;ir.qweb&#39;]._render(template, values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/mnt/enterprise-addons/web_studio/models/ir_qweb.py&#34;, line 14, in _render
return super()._render(template, values, **options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 725, in _render
return Markup(&#39;&#39;.join(iterator))
^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 753, in _render_iterall
for item in frame.iterator:
File &#34;&lt;13488&gt;&#34;, line 264, in template_fusion_quotations_portal_quotation_list_13488
File &#34;&lt;13488&gt;&#34;, line 250, in template_fusion_quotations_portal_quotation_list_13488_content
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 616, in __str__
self.html = &#39;&#39;.join(self.irQweb._render_iterall(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, 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: &lt;t t-out=&#34;dict(a._fields[\&#39;equipment_type\&#39;].selection).get(a.equipment_type, a.equipment_type or \&#39;\&#39;)&#34;/&gt;
From: (13488, &#39;/t/t&#39;, &#39;&lt;t t-call=&#34;portal.portal_layout&#34;/&gt;&#39;)
(13488, &#39;/t/t/div/t[2]/table/tbody/t/tr/td[3]/t&#39;, &#39;&lt;t t-out=&#34;dict(a._fields[\\\&#39;equipment_type\\\&#39;].selection).get(a.equipment_type, a.equipment_type or \\\&#39;\\\&#39;)&#34;/&gt;&#39;)
</pre>
</div>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@@ -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',
],

View File

@@ -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

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Some files were not shown because too many files have changed in this diff Show More