first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import controllers
from . import models
+36
View File
@@ -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',
}
+1
View File
@@ -0,0 +1 @@
from . import controllers
+21
View File
@@ -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
# })
+30
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
from . import sync_config
+165
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sync_config_user sync.config.user model_sync_config base.group_user 1 1 1 1
3 access_sync_config_system sync.config.system model_sync_config base.group_system 1 1 1 1
+62
View File
@@ -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>
+24
View File
@@ -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>