Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

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}')