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

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
{
'name': 'Fusion Inventory Sync',
'version': '19.0.1.0.0',
'category': 'Inventory',
'summary': 'Sync inventory between Westin Healthcare and Mobility Specialties via XML-RPC',
'description': """
Cross-database inventory sync between two Odoo instances.
- Connects to a remote Odoo instance via XML-RPC
- Syncs product catalog with mapping table
- Shows remote stock levels on local products
- Cron-based periodic sync (configurable interval)
""",
'author': 'Nexa Systems Inc.',
'website': 'https://www.nexasystems.ca',
'license': 'OPL-1',
'depends': [
'base',
'stock',
'product',
],
'data': [
'security/ir.model.access.csv',
'data/ir_cron_data.xml',
'views/sync_config_views.xml',
'views/product_views.xml',
],
'installable': True,
'auto_install': False,
'application': False,
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="cron_sync_remote_inventory" model="ir.cron">
<field name="name">Fusion: Sync Remote Inventory</field>
<field name="model_id" ref="fusion_inventory_sync.model_fusion_sync_config"/>
<field name="state">code</field>
<field name="code">model._cron_sync_inventory()</field>
<field name="interval_number">30</field>
<field name="interval_type">minutes</field>
<field name="active">True</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import sync_config
from . import product_sync_mapping
from . import product_template
from . import stock_move
from . import sync_log

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class FusionProductSyncMapping(models.Model):
"""Maps local products to remote products for inventory sync."""
_name = 'fusion.product.sync.mapping'
_description = 'Product Sync Mapping'
_rec_name = 'remote_product_name'
_order = 'remote_product_name'
config_id = fields.Many2one('fusion.sync.config', string='Sync Config',
required=True, ondelete='cascade')
# Local product link
local_product_id = fields.Many2one('product.template', string='Local Product',
help='The matching product in this Odoo instance')
auto_matched = fields.Boolean(string='Auto-Matched',
help='True if the product was automatically matched by SKU or name')
# Remote product info
remote_product_id = fields.Integer(string='Remote Product ID', index=True)
remote_product_name = fields.Char(string='Remote Product Name')
remote_default_code = fields.Char(string='Remote SKU/Reference')
remote_list_price = fields.Float(string='Remote Price')
remote_category = fields.Char(string='Remote Category')
# Remote stock levels (updated by sync)
remote_qty_available = fields.Float(string='Remote On Hand', readonly=True,
help='Quantity currently on hand at the remote location')
remote_qty_forecast = fields.Float(string='Remote Forecast', readonly=True,
help='Forecasted quantity (on hand - outgoing + incoming)')
last_stock_sync = fields.Datetime(string='Stock Last Updated', readonly=True)
_sql_constraints = [
('unique_remote_product',
'UNIQUE(config_id, remote_product_id)',
'Each remote product can only be mapped once per sync configuration.'),
]

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class ProductTemplate(models.Model):
"""Extend product template with remote inventory visibility."""
_inherit = 'product.template'
sync_mapping_ids = fields.One2many('fusion.product.sync.mapping', 'local_product_id',
string='Remote Inventory Links')
remote_qty_available = fields.Float(
string='Remote On Hand',
compute='_compute_remote_stock',
store=False,
help='Total on-hand quantity at remote locations (Mobility Specialties)')
remote_qty_forecast = fields.Float(
string='Remote Forecast',
compute='_compute_remote_stock',
store=False,
help='Total forecasted quantity at remote locations')
has_remote_mapping = fields.Boolean(
string='Has Remote Link',
compute='_compute_remote_stock',
store=False)
@api.depends('sync_mapping_ids', 'sync_mapping_ids.remote_qty_available',
'sync_mapping_ids.remote_qty_forecast')
def _compute_remote_stock(self):
for product in self:
mappings = product.sync_mapping_ids
product.remote_qty_available = sum(mappings.mapped('remote_qty_available'))
product.remote_qty_forecast = sum(mappings.mapped('remote_qty_forecast'))
product.has_remote_mapping = bool(mappings)

View File

@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
import logging
from odoo import models, fields
_logger = logging.getLogger(__name__)
class StockMove(models.Model):
"""Extend stock moves to push changes to remote Odoo instance."""
_inherit = 'stock.move'
def _action_done(self, cancel_backorder=False):
"""Override to trigger remote sync after stock moves complete."""
res = super()._action_done(cancel_backorder=cancel_backorder)
# After moves are done, queue a remote stock push for affected products
try:
self._push_stock_to_remote()
except Exception as e:
# Never block local operations due to sync failure
_logger.warning(f'Remote stock push failed (non-blocking): {e}')
return res
def _push_stock_to_remote(self):
"""Push stock level changes to the remote Odoo instance.
Only pushes for products that have a sync mapping.
Runs async-safe: failures don't block local operations.
"""
if not self:
return
# Get unique product templates from completed moves
product_tmpls = self.mapped('product_id.product_tmpl_id')
if not product_tmpls:
return
# Find sync mappings for these products
Mapping = self.env['fusion.product.sync.mapping']
mappings = Mapping.search([
('local_product_id', 'in', product_tmpls.ids),
('config_id.active', '=', True),
('config_id.state', '=', 'connected'),
('remote_product_id', '!=', 0),
])
if not mappings:
return
# Group by config for efficient batch push
configs = {}
for mapping in mappings:
config = mapping.config_id
if config.id not in configs:
configs[config.id] = {
'config': config,
'mappings': self.env['fusion.product.sync.mapping'],
}
configs[config.id]['mappings'] |= mapping
for config_data in configs.values():
config = config_data['config']
config_mappings = config_data['mappings']
try:
self._push_stock_levels(config, config_mappings)
except Exception as e:
_logger.warning(
f'Failed to push stock to {config.name}: {e}'
)
def _push_stock_levels(self, config, mappings):
"""Push current local stock levels to the remote instance.
This updates the remote side with our current on-hand qty
so the remote instance knows what we have available.
"""
uid, models_proxy = config._get_xmlrpc_connection()
for mapping in mappings:
local_product = mapping.local_product_id
if not local_product:
continue
# Get current local stock for this product
local_qty = local_product.qty_available
local_forecast = local_product.virtual_available
# Update the mapping record with current local stock
mapping.write({
'last_stock_sync': fields.Datetime.now(),
})
# Log the push for debugging
_logger.info(
f'Stock push: {local_product.name} -> {config.name} '
f'(local_qty={local_qty}, remote_id={mapping.remote_product_id})'
)
# Optionally update a custom field on the remote side
# This writes to a field on the remote product to track
# what the partner store has available
try:
models_proxy.execute_kw(
config.db_name, uid, config.api_key,
'product.template', 'write',
[[mapping.remote_product_id], {
'x_partner_qty_available': local_qty,
}]
)
except Exception as e:
# If the remote field doesn't exist yet, just log it
_logger.debug(
f'Could not update remote field x_partner_qty_available: {e}. '
f'Create this field on the remote instance for full bi-directional sync.'
)

View File

@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
import logging
import xmlrpc.client
from odoo import models, fields, api
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class FusionSyncConfig(models.Model):
"""Configuration for remote Odoo instance connection."""
_name = 'fusion.sync.config'
_description = 'Inventory Sync Configuration'
_rec_name = 'name'
name = fields.Char(string='Connection Name', required=True)
url = fields.Char(string='Remote URL', required=True,
help='Full URL of the remote Odoo instance (e.g., https://erp.mobilityspecialties.com)')
db_name = fields.Char(string='Database Name', required=True,
help='Name of the remote database')
username = fields.Char(string='Username', required=True,
help='Login username for the remote instance')
api_key = fields.Char(string='API Key / Password', required=True,
help='API key or password for authentication')
active = fields.Boolean(default=True)
state = fields.Selection([
('draft', 'Not Connected'),
('connected', 'Connected'),
('error', 'Connection Error'),
], string='Status', default='draft', readonly=True)
last_sync = fields.Datetime(string='Last Sync', readonly=True)
last_sync_status = fields.Text(string='Last Sync Result', readonly=True)
sync_interval = fields.Integer(string='Sync Interval (minutes)', default=30,
help='How often to run the automatic sync')
remote_uid = fields.Integer(string='Remote User ID', readonly=True)
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.company)
# Sync scope
sync_products = fields.Boolean(string='Sync Products', default=True)
sync_stock = fields.Boolean(string='Sync Stock Levels', default=True)
remote_warehouse_name = fields.Char(string='Remote Warehouse Name',
help='Name of the warehouse on the remote instance to read stock from. Leave empty for all.')
def _get_xmlrpc_connection(self):
"""Establish XML-RPC connection to the remote Odoo instance."""
self.ensure_one()
url = self.url.rstrip('/')
try:
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common', allow_none=True)
uid = common.authenticate(self.db_name, self.username, self.api_key, {})
if not uid:
raise UserError('Authentication failed. Check username/API key.')
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object', allow_none=True)
return uid, models_proxy
except xmlrpc.client.Fault as e:
raise UserError(f'XML-RPC error: {e.faultString}')
except Exception as e:
raise UserError(f'Connection error: {str(e)}')
def action_test_connection(self):
"""Test the connection to the remote Odoo instance."""
self.ensure_one()
try:
uid, models_proxy = self._get_xmlrpc_connection()
# Test by reading the server version
version_info = xmlrpc.client.ServerProxy(
f'{self.url.rstrip("/")}/xmlrpc/2/common', allow_none=True
).version()
self.write({
'state': 'connected',
'remote_uid': uid,
'last_sync_status': f'Connection successful. Remote server: {version_info.get("server_serie", "unknown")}',
})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Connection Successful',
'message': f'Connected to {self.url} as user ID {uid}',
'type': 'success',
'sticky': False,
},
}
except Exception as e:
self.write({
'state': 'error',
'last_sync_status': f'Connection failed: {str(e)}',
})
raise
def action_sync_now(self):
"""Manually trigger a full sync."""
self.ensure_one()
self._run_sync()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Sync Complete',
'message': self.last_sync_status,
'type': 'success',
'sticky': False,
},
}
def _run_sync(self):
"""Execute the sync process."""
self.ensure_one()
try:
uid, models_proxy = self._get_xmlrpc_connection()
results = []
if self.sync_products:
count = self._sync_products(uid, models_proxy)
results.append(f'{count} products synced')
if self.sync_stock:
count = self._sync_stock_levels(uid, models_proxy)
results.append(f'{count} stock levels updated')
status = ' | '.join(results) if results else 'Nothing to sync'
self.write({
'state': 'connected',
'last_sync': fields.Datetime.now(),
'last_sync_status': status,
})
# Log the sync operation
self.env['fusion.sync.log'].create({
'config_id': self.id,
'direction': 'pull',
'sync_type': 'full',
'status': 'success',
'summary': status,
'product_count': sum(1 for r in results if 'product' in r.lower()),
})
_logger.info(f'Inventory sync complete: {status}')
except Exception as e:
error_msg = f'Sync failed: {str(e)}'
self.write({
'state': 'error',
'last_sync': fields.Datetime.now(),
'last_sync_status': error_msg,
})
_logger.error(error_msg)
def _sync_products(self, uid, models_proxy):
"""Sync product catalog from remote instance."""
self.ensure_one()
Mapping = self.env['fusion.product.sync.mapping']
# Read remote products (only storable/consumable, with internal reference)
remote_products = models_proxy.execute_kw(
self.db_name, uid, self.api_key,
'product.template', 'search_read',
[[('type', 'in', ['consu', 'product'])]],
{'fields': ['id', 'name', 'default_code', 'list_price', 'type', 'categ_id'],
'limit': 5000}
)
synced = 0
for rp in remote_products:
# Try to find existing mapping
mapping = Mapping.search([
('config_id', '=', self.id),
('remote_product_id', '=', rp['id']),
], limit=1)
vals = {
'config_id': self.id,
'remote_product_id': rp['id'],
'remote_product_name': rp.get('name', ''),
'remote_default_code': rp.get('default_code', '') or '',
'remote_list_price': rp.get('list_price', 0),
'remote_category': rp.get('categ_id', [False, ''])[1] if rp.get('categ_id') else '',
}
if mapping:
mapping.write(vals)
else:
# Try auto-match by internal reference (SKU)
local_product = False
if rp.get('default_code'):
local_product = self.env['product.template'].search([
('default_code', '=', rp['default_code'])
], limit=1)
# Fallback: try match by exact name
if not local_product and rp.get('name'):
local_product = self.env['product.template'].search([
('name', 'ilike', rp['name'])
], limit=1)
vals['local_product_id'] = local_product.id if local_product else False
vals['auto_matched'] = bool(local_product)
Mapping.create(vals)
synced += 1
return synced
def _sync_stock_levels(self, uid, models_proxy):
"""Sync stock quantities from remote instance."""
self.ensure_one()
Mapping = self.env['fusion.product.sync.mapping']
# Get all mappings with remote product IDs
mappings = Mapping.search([
('config_id', '=', self.id),
('remote_product_id', '!=', 0),
])
if not mappings:
return 0
# Build list of remote product template IDs
remote_tmpl_ids = mappings.mapped('remote_product_id')
# Get remote product.product IDs for these templates
remote_products = models_proxy.execute_kw(
self.db_name, uid, self.api_key,
'product.product', 'search_read',
[[('product_tmpl_id', 'in', remote_tmpl_ids)]],
{'fields': ['id', 'product_tmpl_id', 'qty_available', 'virtual_available']}
)
# Build dict: template_id -> stock info
stock_by_tmpl = {}
for rp in remote_products:
tmpl_id = rp['product_tmpl_id'][0] if isinstance(rp['product_tmpl_id'], list) else rp['product_tmpl_id']
if tmpl_id not in stock_by_tmpl:
stock_by_tmpl[tmpl_id] = {'qty_available': 0, 'virtual_available': 0}
stock_by_tmpl[tmpl_id]['qty_available'] += rp.get('qty_available', 0)
stock_by_tmpl[tmpl_id]['virtual_available'] += rp.get('virtual_available', 0)
updated = 0
for mapping in mappings:
stock = stock_by_tmpl.get(mapping.remote_product_id, {})
mapping.write({
'remote_qty_available': stock.get('qty_available', 0),
'remote_qty_forecast': stock.get('virtual_available', 0),
'last_stock_sync': fields.Datetime.now(),
})
updated += 1
return updated
@api.model
def _cron_sync_inventory(self):
"""Cron job: run sync for all active configurations."""
configs = self.search([('active', '=', True), ('state', '!=', 'draft')])
for config in configs:
try:
config._run_sync()
except Exception as e:
_logger.error(f'Cron sync failed for {config.name}: {e}')

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class FusionSyncLog(models.Model):
"""Log sync operations for auditing and debugging."""
_name = 'fusion.sync.log'
_description = 'Inventory Sync Log'
_order = 'create_date desc'
_rec_name = 'summary'
config_id = fields.Many2one('fusion.sync.config', string='Sync Config',
required=True, ondelete='cascade')
direction = fields.Selection([
('pull', 'Pull (Remote -> Local)'),
('push', 'Push (Local -> Remote)'),
], string='Direction', required=True)
sync_type = fields.Selection([
('product', 'Product Catalog'),
('stock', 'Stock Levels'),
('full', 'Full Sync'),
], string='Type', required=True)
status = fields.Selection([
('success', 'Success'),
('partial', 'Partial'),
('error', 'Error'),
], string='Status', required=True)
summary = fields.Char(string='Summary')
details = fields.Text(string='Details')
product_count = fields.Integer(string='Products Affected')

View File

@@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sync_config_manager,fusion.sync.config.manager,model_fusion_sync_config,stock.group_stock_manager,1,1,1,1
access_sync_config_user,fusion.sync.config.user,model_fusion_sync_config,stock.group_stock_user,1,0,0,0
access_sync_mapping_manager,fusion.product.sync.mapping.manager,model_fusion_product_sync_mapping,stock.group_stock_manager,1,1,1,1
access_sync_mapping_user,fusion.product.sync.mapping.user,model_fusion_product_sync_mapping,stock.group_stock_user,1,0,0,0
access_sync_log_manager,fusion.sync.log.manager,model_fusion_sync_log,stock.group_stock_manager,1,1,1,1
access_sync_log_user,fusion.sync.log.user,model_fusion_sync_log,stock.group_stock_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sync_config_manager fusion.sync.config.manager model_fusion_sync_config stock.group_stock_manager 1 1 1 1
3 access_sync_config_user fusion.sync.config.user model_fusion_sync_config stock.group_stock_user 1 0 0 0
4 access_sync_mapping_manager fusion.product.sync.mapping.manager model_fusion_product_sync_mapping stock.group_stock_manager 1 1 1 1
5 access_sync_mapping_user fusion.product.sync.mapping.user model_fusion_product_sync_mapping stock.group_stock_user 1 0 0 0
6 access_sync_log_manager fusion.sync.log.manager model_fusion_sync_log stock.group_stock_manager 1 1 1 1
7 access_sync_log_user fusion.sync.log.user model_fusion_sync_log stock.group_stock_user 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Add remote stock columns to product list view -->
<record id="view_product_template_list_inherit_sync" model="ir.ui.view">
<field name="name">product.template.list.sync</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<field name="remote_qty_available" string="Remote Stock" optional="show"
decoration-danger="remote_qty_available == 0 and has_remote_mapping"
decoration-success="remote_qty_available > 0"/>
<field name="has_remote_mapping" column_invisible="1"/>
</xpath>
</field>
</record>
<!-- Add remote stock info to product form view -->
<record id="view_product_template_form_inherit_sync" model="ir.ui.view">
<field name="name">product.template.form.sync</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Remote Inventory" name="remote_inventory"
invisible="not has_remote_mapping">
<group>
<group string="Remote Stock Levels">
<field name="remote_qty_available"/>
<field name="remote_qty_forecast"/>
</group>
</group>
<field name="sync_mapping_ids" readonly="1">
<list>
<field name="config_id"/>
<field name="remote_product_name"/>
<field name="remote_default_code"/>
<field name="remote_qty_available"/>
<field name="remote_qty_forecast"/>
<field name="last_stock_sync"/>
</list>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Sync Config Form View -->
<record id="view_fusion_sync_config_form" model="ir.ui.view">
<field name="name">fusion.sync.config.form</field>
<field name="model">fusion.sync.config</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_test_connection" type="object"
string="Test Connection" class="btn-primary"
invisible="state == 'connected'"/>
<button name="action_test_connection" type="object"
string="Re-Test Connection"
invisible="state != 'connected'"/>
<button name="action_sync_now" type="object"
string="Sync Now" class="btn-primary"
invisible="state != 'connected'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,connected"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="e.g., Mobility Specialties"/></h1>
</div>
<group>
<group string="Connection">
<field name="url" placeholder="https://erp.mobilityspecialties.com"/>
<field name="db_name" placeholder="mobility-prod"/>
<field name="username" placeholder="admin"/>
<field name="api_key" password="True"/>
</group>
<group string="Sync Settings">
<field name="sync_products"/>
<field name="sync_stock"/>
<field name="sync_interval"/>
<field name="remote_warehouse_name"
placeholder="Leave empty for all warehouses"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<group string="Status">
<field name="last_sync"/>
<field name="last_sync_status"/>
<field name="remote_uid" invisible="state != 'connected'"/>
</group>
<notebook>
<page string="Product Mappings" name="mappings">
<field name="active" invisible="1"/>
<p class="text-muted" invisible="state == 'connected'">
Connect and sync to see product mappings here.
</p>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Sync Config List View -->
<record id="view_fusion_sync_config_list" model="ir.ui.view">
<field name="name">fusion.sync.config.list</field>
<field name="model">fusion.sync.config</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="url"/>
<field name="state" widget="badge"
decoration-success="state == 'connected'"
decoration-danger="state == 'error'"
decoration-info="state == 'draft'"/>
<field name="last_sync"/>
<field name="last_sync_status"/>
</list>
</field>
</record>
<!-- Product Mapping List View -->
<record id="view_fusion_product_sync_mapping_list" model="ir.ui.view">
<field name="name">fusion.product.sync.mapping.list</field>
<field name="model">fusion.product.sync.mapping</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="remote_product_name"/>
<field name="remote_default_code"/>
<field name="local_product_id"/>
<field name="auto_matched" widget="boolean"/>
<field name="remote_qty_available"
decoration-danger="remote_qty_available == 0"
decoration-success="remote_qty_available > 0"/>
<field name="remote_qty_forecast"/>
<field name="last_stock_sync"/>
<field name="config_id" column_invisible="1"/>
</list>
</field>
</record>
<!-- Product Mapping Search View -->
<record id="view_fusion_product_sync_mapping_search" model="ir.ui.view">
<field name="name">fusion.product.sync.mapping.search</field>
<field name="model">fusion.product.sync.mapping</field>
<field name="arch" type="xml">
<search>
<field name="remote_product_name"/>
<field name="remote_default_code"/>
<field name="local_product_id"/>
<separator/>
<filter name="mapped" string="Mapped"
domain="[('local_product_id', '!=', False)]"/>
<filter name="unmapped" string="Unmapped"
domain="[('local_product_id', '=', False)]"/>
<filter name="in_stock" string="Remote In Stock"
domain="[('remote_qty_available', '&gt;', 0)]"/>
</search>
</field>
</record>
<!-- Menu Items -->
<menuitem id="menu_fusion_sync_root"
name="Inventory Sync"
parent="stock.menu_stock_config_settings"
sequence="100"/>
<record id="action_fusion_sync_config" model="ir.actions.act_window">
<field name="name">Sync Configurations</field>
<field name="res_model">fusion.sync.config</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_fusion_sync_config"
name="Remote Connections"
parent="menu_fusion_sync_root"
action="action_fusion_sync_config"
sequence="10"/>
<record id="action_fusion_product_mapping" model="ir.actions.act_window">
<field name="name">Product Mappings</field>
<field name="res_model">fusion.product.sync.mapping</field>
<field name="view_mode">list</field>
<field name="context">{'search_default_mapped': 1}</field>
</record>
<menuitem id="menu_fusion_product_mapping"
name="Product Mappings"
parent="menu_fusion_sync_root"
action="action_fusion_product_mapping"
sequence="20"/>
<!-- Sync Log List View -->
<record id="view_fusion_sync_log_list" model="ir.ui.view">
<field name="name">fusion.sync.log.list</field>
<field name="model">fusion.sync.log</field>
<field name="arch" type="xml">
<list>
<field name="create_date" string="Date"/>
<field name="config_id"/>
<field name="direction"/>
<field name="sync_type"/>
<field name="status" widget="badge"
decoration-success="status == 'success'"
decoration-warning="status == 'partial'"
decoration-danger="status == 'error'"/>
<field name="summary"/>
<field name="product_count"/>
</list>
</field>
</record>
<record id="action_fusion_sync_log" model="ir.actions.act_window">
<field name="name">Sync Log</field>
<field name="res_model">fusion.sync.log</field>
<field name="view_mode">list</field>
</record>
<menuitem id="menu_fusion_sync_log"
name="Sync Log"
parent="menu_fusion_sync_root"
action="action_fusion_sync_log"
sequence="30"/>
</odoo>