first push message
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import xmlrpc.client
|
||||
import logging
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncConfig(models.Model):
|
||||
_name = 'sync.config'
|
||||
_description = 'Target Database Sync Configuration'
|
||||
|
||||
name = fields.Char(string='Configuration Name', required=True, default='Main Target DB')
|
||||
target_url = fields.Char(string='Target Odoo URL', required=True, help='e.g., https://target-domain.com')
|
||||
target_db = fields.Char(string='Target Database Name', required=True)
|
||||
target_username = fields.Char(string='Target Username', required=True)
|
||||
target_password = fields.Char(string='Target Password', required=True, password=True)
|
||||
|
||||
# Expanded to include complex modules
|
||||
model_to_sync = fields.Selection([
|
||||
('res.partner', 'Contacts'),
|
||||
('product.template', 'Products'),
|
||||
('project.project', 'Projects'),
|
||||
('project.task', 'Project Tasks'),
|
||||
('sale.order', 'Sales Orders'),
|
||||
('account.move', 'Accounting Entries (Invoices/Bills)'),
|
||||
], string='Model to Sync', default='res.partner', required=True)
|
||||
|
||||
@api.model
|
||||
def _get_target_connection(self):
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy(f'{self.target_url}/xmlrpc/2/common')
|
||||
uid = common.authenticate(self.target_db, self.target_username, self.target_password, {})
|
||||
if not uid:
|
||||
raise UserError(_("Authentication failed. Check credentials."))
|
||||
models_api = xmlrpc.client.ServerProxy(f'{self.target_url}/xmlrpc/2/object')
|
||||
return uid, models_api
|
||||
except Exception as e:
|
||||
raise UserError(_("Connection failed: %s") % str(e))
|
||||
|
||||
def _get_target_id_by_xmlid(self, models_api, db, uid, pwd, model, source_id):
|
||||
"""Finds the target database ID using a deterministic External ID."""
|
||||
xmlid_module = 'sync_source_db'
|
||||
xmlid_name = f"{model.replace('.', '_')}_{source_id}"
|
||||
|
||||
# Search ir.model.data in target DB for this xml_id
|
||||
data_ids = models_api.execute_kw(
|
||||
db, uid, pwd, 'ir.model.data', 'search',
|
||||
[[['module', '=', xmlid_module], ['name', '=', xmlid_name]]]
|
||||
)
|
||||
if data_ids:
|
||||
data = models_api.execute_kw(
|
||||
db, uid, pwd, 'ir.model.data', 'read',
|
||||
[data_ids[0]], {'fields': ['res_id']}
|
||||
)
|
||||
return data[0]['res_id']
|
||||
return False
|
||||
|
||||
def _set_target_xmlid(self, models_api, db, uid, pwd, model, target_res_id, source_id):
|
||||
"""Creates or updates the External ID mapping in the target database."""
|
||||
xmlid_module = 'sync_source_db'
|
||||
xmlid_name = f"{model.replace('.', '_')}_{source_id}"
|
||||
|
||||
# Check if mapping exists
|
||||
data_ids = models_api.execute_kw(
|
||||
db, uid, pwd, 'ir.model.data', 'search',
|
||||
[[['module', '=', xmlid_module], ['name', '=', xmlid_name]]]
|
||||
)
|
||||
|
||||
vals = {
|
||||
'module': xmlid_module,
|
||||
'name': xmlid_name,
|
||||
'model': model,
|
||||
'res_id': target_res_id,
|
||||
'noupdate': True, # Prevents target DB from overwriting this mapping
|
||||
}
|
||||
|
||||
if data_ids:
|
||||
models_api.execute_kw(db, uid, pwd, 'ir.model.data', 'write', [data_ids, vals])
|
||||
else:
|
||||
models_api.execute_kw(db, uid, pwd, 'ir.model.data', 'create', [vals])
|
||||
|
||||
def action_sync_data(self):
|
||||
self.ensure_one()
|
||||
uid, models_api = self._get_target_connection()
|
||||
model = self.model_to_sync
|
||||
|
||||
# 1. Get source records (limit to 50 for safety in this example, remove limit for production)
|
||||
source_records = self.env[model].search([], limit=50)
|
||||
synced_count = 0
|
||||
|
||||
for record in source_records:
|
||||
# 2. Read all field values from source
|
||||
# We exclude 'id' and let the target DB generate its own, but we keep relational IDs to map them
|
||||
record_data = record.read()[0]
|
||||
|
||||
# 3. Resolve Relational Fields (Many2one)
|
||||
# Example: If syncing a Sale Order, map the source partner_id to the target partner_id
|
||||
resolved_vals = {}
|
||||
for field_name, field_value in record_data.items():
|
||||
if isinstance(field_value, tuple) and len(field_value) == 2: # It's a Many2one field (id, name)
|
||||
source_rel_id = field_value[0]
|
||||
if source_rel_id:
|
||||
# Determine the related model (Odoo 19: use self.env[model]._fields[field_name].comodel_name)
|
||||
comodel = self.env[model]._fields[field_name].comodel_name
|
||||
if comodel:
|
||||
target_rel_id = self._get_target_id_by_xmlid(
|
||||
models_api, self.target_db, uid, self.target_password, comodel, source_rel_id
|
||||
)
|
||||
if target_rel_id:
|
||||
resolved_vals[field_name] = target_rel_id
|
||||
else:
|
||||
_logger.warning(
|
||||
f"Related {comodel} ID {source_rel_id} not found in target. Skipping record {record.id}.")
|
||||
break # Skip this record if dependency is missing
|
||||
elif field_name not in ['id', 'create_uid', 'write_uid', 'create_date', 'write_date']:
|
||||
# Copy standard fields (Char, Integer, Boolean, Selection, etc.)
|
||||
resolved_vals[field_name] = field_value
|
||||
|
||||
# Handle One2many fields (e.g., order_line in sale.order)
|
||||
# We convert them to Odoo's (0, 0, {vals}) command format
|
||||
for field_name, field_value in record_data.items():
|
||||
field_obj = self.env[model]._fields.get(field_name)
|
||||
if field_obj and field_obj.type == 'one2many' and field_name in resolved_vals or field_name not in resolved_vals:
|
||||
# Simplified One2many handling: recreate lines
|
||||
# Note: For production, you should also map xml_ids for child records!
|
||||
pass # Kept simple for this example; see notes below.
|
||||
|
||||
if 'id' in resolved_vals:
|
||||
del resolved_vals['id']
|
||||
|
||||
# 4. Check if record exists in Target via XML ID
|
||||
target_id = self._get_target_id_by_xmlid(
|
||||
models_api, self.target_db, uid, self.target_password, model, record.id
|
||||
)
|
||||
|
||||
try:
|
||||
if target_id:
|
||||
# Update existing
|
||||
models_api.execute_kw(
|
||||
self.target_db, uid, self.target_password, model, 'write', [target_id, resolved_vals]
|
||||
)
|
||||
else:
|
||||
# Create new
|
||||
new_id = models_api.execute_kw(
|
||||
self.target_db, uid, self.target_password, model, 'create', [resolved_vals]
|
||||
)
|
||||
# Save the new mapping!
|
||||
self._set_target_xmlid(models_api, self.target_db, uid, self.target_password, model, new_id,
|
||||
record.id)
|
||||
|
||||
synced_count += 1
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to sync {model} record {record.id}: {str(e)}")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Sync Complete'),
|
||||
'message': _('Successfully synced/updated %s %s records.') % (synced_count, model),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user