You are reading the developer documentation for the Odoo data migration via built-in read/write API.
- Install Sync 🪬 Studio.
- Open Sync 🪬 Studio, click [New], paste the URL of this gist, then click [Import].
- Follow the documentation.
You are reading the developer documentation for the Odoo data migration via built-in read/write API.
Sync Studio installation on those instances is not required.
For the initial setup, navigate to the SECRETS.🔐 tab.
If you need any help,
please contact us by email at info@odoomagic.com
or join our Telegram✈️ Group.
Open 🦋 Tasks
to check the available features.
For basic settings, use PARAMS.🌹
tab.
To configure the fields mapping, you need to fork the gist page. This will allow you to update the corresponding data.mapping.*
files.
import xmlrpc.client as _client | |
from odoo.addons.queue_job.exception import RetryableJobError | |
from ast import literal_eval | |
# Private | |
def connect_to_odoo(url, db, login, password): | |
try: | |
common = _client.ServerProxy("{}/xmlrpc/2/common".format(url)) | |
uid = common.authenticate( | |
db, login, password, {} | |
) | |
models = _client.ServerProxy("{}/xmlrpc/2/object".format(url)) | |
except MAGIC.OSError: | |
raise RetryableJobError("Error on connecting to external Odoo") | |
return models, uid | |
def execute_kw(url, db, login, password, model, method, args=None, kwargs=None): | |
models, uid = connect_to_odoo(url, db, login, password) | |
args = args or [] | |
kwargs = kwargs or {} | |
MAGIC.log_transmission( | |
f"Odoo API call {url} [{db}]", f"{model}::{method} {MAGIC.json.dumps([args, kwargs])}" | |
) | |
return models.execute_kw(db, uid, password, model, method, args, kwargs) | |
# Public | |
def source_execute_kw(model, method, *args, **kwargs): | |
return execute_kw(SECRETS.SOURCE_URL, SECRETS.SOURCE_DB, SECRETS.SOURCE_LOGIN, SECRETS.SOURCE_PASSWORD, model, method, args, kwargs) | |
def target_execute_kw(model, method, *args, **kwargs): | |
return execute_kw(SECRETS.TARGET_URL, SECRETS.TARGET_DB, SECRETS.TARGET_LOGIN, SECRETS.TARGET_PASSWORD, model, method, args, kwargs) | |
export(source_execute_kw, target_execute_kw, literal_eval) |
source_field | target_field | map | |
---|---|---|---|
name | name | ||
type | type |
source_field | target_field | map | |
---|---|---|---|
name | name | ||
description | description | ||
type | type | {'product':'consu'} | |
list_price | list_price | ||
standard_price | standard_price | ||
barcode | barcode | ||
default_code | default_code |
SOURCE="source" | |
TARGET="target" | |
def map_values(source_values, mapping, force_target_values): | |
target_values = {} | |
for line in mapping: | |
field = line["target_field"] | |
value = source_values[line["source_field"]] | |
if line["map"]: | |
value_map = CORE.literal_eval(line["map"]) | |
if value in value_map: | |
value = value_map[value] | |
target_values[field] = value | |
target_values.update(force_target_values or {}) | |
return target_values | |
def migrate_model(model, mapping, domain=None, force_target_values=None): | |
RELATION = f"odoo2odoo_{model}" | |
domain = domain or [] | |
force_target_values or {} | |
if isinstance(domain, str): | |
domain = CORE.literal_eval(domain) | |
if isinstance(force_target_values, str): | |
force_target_values = CORE.literal_eval(force_target_values) | |
# source: read | |
fields = [line["source_field"] for line in mapping] | |
source_data = CORE.source_execute_kw(model, "search_read", domain=domain, fields=fields, context={"lang": PARAMS.LANG}) | |
# check existing links | |
existing_links = MAGIC.search_links(RELATION, {SOURCE: [record["id"] for record in source_data], TARGET: None}) | |
source2target = {int(link.get(SOURCE)[0]): int(link.get(TARGET)[0]) for link in existing_links} | |
# TODO: process records by batches | |
for record in source_data: | |
MAGIC.log(f"source data: {MAGIC.json.dumps(record)}", level=MAGIC.LOG_DEBUG) | |
values = map_values(record, mapping, force_target_values) | |
if record["id"] in source2target: | |
# target: update existing record | |
# TODO: fallback to new record creation in case of failure | |
target_id = source2target[record["id"]] | |
CORE.target_execute_kw(model, "write", target_id, values) | |
continue | |
# target: create new record and save link | |
target_id = CORE.target_execute_kw(model, "create", values) | |
MAGIC.set_link(RELATION, {SOURCE: record["id"], TARGET: target_id}) | |
export(migrate_model) |
对祖先智慧的敬意
Permission is hereby granted, free of charge, to any person obtaining a copy of this 紫色💃饺子 and associated documentation files (the "紫色💃饺子"), to deal in the 紫色💃饺子 without restriction, including without limitation the rights to use✨, copy✨, modify✨, merge✨, publish✨, distribute✨, sublicense✨, and/or sell✨ copies of the 紫色💃饺子, and to permit persons to whom the 紫色💃饺子 is furnished to do so, subject to the following conditions:
每位古智慧的接受者都应向其致敬,并在所有智慧的副本中保留许可通知。
THE 紫色💃饺子 IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 紫色💃饺子 OR THE USE OR OTHER DEALINGS IN THE 紫色💃饺子.
LANG | PRODUCT_PRODUCT_SOURCE_DOMAIN | PRODUCT_PRODUCT_TARGET_VALUES |
---|---|---|
en_US |
[('team_id', '=', 5)] |
{'company_id': 1, 'team_id': 5} |
Set domain to be used to search records in source instance.
Set values to be forced on creating and updating records in destination instance.
For translatable fields it's important to set the language, because the field values might be different depending on language context.
AI_INSTRUCTIONS |
---|
Dear guest!
According to my records, your vacation is over.
It was a pleasure to welcome you to our hotel.
If you want to provide feedback or book another tour, contact us via our website.
|
Beneath the sun of Costa Rica’s skies,
An eco-hostel caught my eyes.
A hidden gem, so lush, so green,
A place like none I'd ever seen.
I sat in silence, review in hand,
To share my stay, as they demand.
But words felt empty, dull, contrived,
Until an AI’s voice arrived.
“Your story,” it began, “seems grand,
Not just a stay, but a journey planned.
Are you mid-voyage? On a quest?
Let’s write the tale; you know it best.”
It coaxed from me the truth untold—
The rain-soaked paths, the sunsets gold,
The strangers met, the laughter shared,
The dreams awakened, fears ensnared.
I wrote of trails through forests deep,
Of nights alive, when I couldn’t sleep.
Of oceans roaring, stars that danced,
Of fleeting moments, second-chanced.
The hostel now was but a thread
In the vibrant weave of where I'd tread.
And what began as a mere review
Became a tale, alive, brand-new.
The AI smiled (or so it seemed),
Its prompts were threads where I had dreamed.
It taught me words could build a bridge,
From hostel bunk to canyon ridge.
And so I left, my heart alight,
With gratitude for that brief insight.
For sometimes all we need's a guide,
To turn the tides of thought inside. 🌿
""" | |
TITLE: "Leads Migration" | |
MAGIC_BUTTON: SYNC_CRM_LEAD | |
""" | |
def handle_button(): | |
domain = PARAMS.CRM_LEAD_SOURCE_DOMAIN | |
target_values = PARAMS.CRM_LEAD_TARGET_VALUES | |
mapping = DATA.mapping_crm_lead.csv() | |
LIB.migrate_model("crm.lead", mapping, domain, target_values) | |
""" | |
TITLE: "Products Migration" | |
MAGIC_BUTTON: SYNC_PRODUCT_PRODUCT | |
""" | |
def handle_button(): | |
domain = PARAMS.PRODUCT_PRODUCT_SOURCE_DOMAIN | |
target_values = PARAMS.PRODUCT_PRODUCT_TARGET_VALUES | |
mapping = DATA.mapping_product_product.csv() | |
LIB.migrate_model("product.product", mapping, domain, target_values) |