first push message
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
'name': "db_data_sync",
|
||||
|
||||
'summary': "Short (1 phrase/line) summary of the module's purpose",
|
||||
|
||||
'description': """
|
||||
Long description of module's purpose
|
||||
""",
|
||||
|
||||
'author': "My Company",
|
||||
'website': "https://www.yourcompany.com",
|
||||
|
||||
# Categories can be used to filter modules in modules listing
|
||||
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
# for the full list
|
||||
'category': 'Uncategorized',
|
||||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/sync_config_views.xml',
|
||||
'views/templates.xml',
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
'demo': [
|
||||
'demo/demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import controllers
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
# from odoo import http
|
||||
|
||||
|
||||
# class DbDataSync(http.Controller):
|
||||
# @http.route('/db_data_sync/db_data_sync', auth='public')
|
||||
# def index(self, **kw):
|
||||
# return "Hello, world"
|
||||
|
||||
# @http.route('/db_data_sync/db_data_sync/objects', auth='public')
|
||||
# def list(self, **kw):
|
||||
# return http.request.render('db_data_sync.listing', {
|
||||
# 'root': '/db_data_sync/db_data_sync',
|
||||
# 'objects': http.request.env['db_data_sync.db_data_sync'].search([]),
|
||||
# })
|
||||
|
||||
# @http.route('/db_data_sync/db_data_sync/objects/<model("db_data_sync.db_data_sync"):obj>', auth='public')
|
||||
# def object(self, obj, **kw):
|
||||
# return http.request.render('db_data_sync.object', {
|
||||
# 'object': obj
|
||||
# })
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!--
|
||||
<record id="object0" model="db_data_sync.db_data_sync">
|
||||
<field name="name">Object 0</field>
|
||||
<field name="value">0</field>
|
||||
</record>
|
||||
|
||||
<record id="object1" model="db_data_sync.db_data_sync">
|
||||
<field name="name">Object 1</field>
|
||||
<field name="value">10</field>
|
||||
</record>
|
||||
|
||||
<record id="object2" model="db_data_sync.db_data_sync">
|
||||
<field name="name">Object 2</field>
|
||||
<field name="value">20</field>
|
||||
</record>
|
||||
|
||||
<record id="object3" model="db_data_sync.db_data_sync">
|
||||
<field name="name">Object 3</field>
|
||||
<field name="value">30</field>
|
||||
</record>
|
||||
|
||||
<record id="object4" model="db_data_sync.db_data_sync">
|
||||
<field name="name">Object 4</field>
|
||||
<field name="value">40</field>
|
||||
</record>
|
||||
-->
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
from . import sync_config
|
||||
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sync_config_user,sync.config.user,model_sync_config,base.group_user,1,1,1,1
|
||||
access_sync_config_system,sync.config.system,model_sync_config,base.group_system,1,1,1,1
|
||||
|
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View -->
|
||||
<record id="view_sync_config_form" model="ir.ui.view">
|
||||
<field name="name">sync.config.form</field>
|
||||
<field name="model">sync.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Database Sync Configuration">
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Target Database Details">
|
||||
<field name="name"/>
|
||||
<field name="target_url" widget="url"/>
|
||||
<field name="target_db"/>
|
||||
<field name="target_username"/>
|
||||
<field name="target_password"/>
|
||||
</group>
|
||||
<group string="Sync Settings">
|
||||
<field name="model_to_sync"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Actions">
|
||||
<p class="text-muted">Click the button below to push all active records of the selected model to the target database.</p>
|
||||
<button name="action_sync_data" string="🔄 Sync Data Now" type="object" class="btn-primary"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree (List) View -->
|
||||
<record id="view_sync_config_tree" model="ir.ui.view">
|
||||
<field name="name">sync.config.tree</field>
|
||||
<field name="model">sync.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Sync Configurations">
|
||||
<field name="name"/>
|
||||
<field name="target_url"/>
|
||||
<field name="target_db"/>
|
||||
<field name="model_to_sync"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_sync_config" model="ir.actions.act_window">
|
||||
<field name="name">Database Sync</field>
|
||||
<field name="res_model">sync.config</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new target database configuration to start syncing.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem id="menu_sync_root" name="Data Sync" web_icon="db_data_sync,static/description/icon.png"/>
|
||||
<menuitem id="menu_sync_config" name="Configurations" parent="menu_sync_root" action="action_sync_config"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,24 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!--
|
||||
<template id="listing">
|
||||
<ul>
|
||||
<li t-foreach="objects" t-as="object">
|
||||
<a t-attf-href="#{ root }/objects/#{ object.id }">
|
||||
<t t-esc="object.display_name"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template id="object">
|
||||
<h1><t t-esc="object.display_name"/></h1>
|
||||
<dl>
|
||||
<t t-foreach="object._fields" t-as="field">
|
||||
<dt><t t-esc="field"/></dt>
|
||||
<dd><t t-esc="object[field]"/></dd>
|
||||
</t>
|
||||
</dl>
|
||||
</template>
|
||||
-->
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user