Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
Service for fetching translations from a remote Odoo 19 instance.
Odoo 16+ / 19 stores translations in JSONB columns, not in ir.translation.
The only reliable cross-version way to export / import translations is through
the standard PO pipeline:
Remote: base.language.export → PO bytes (base64)
Local: TranslationImporter → write JSONB values
This module automates the whole round-trip via XML-RPC.
"""
import base64
import logging
import tempfile
from xmlrpc.client import ServerProxy
from odoo import _, api, models
from odoo.exceptions import UserError
from odoo.tools.translate import TranslationImporter
_logger = logging.getLogger(__name__)
class RemoteTranslationService(models.AbstractModel):
_name = "translation.remote.service"
_description = "Remote Translation Service"
# ------------------------------------------------------------------
# Remote connection helpers
# ------------------------------------------------------------------
@api.model
def _rpc_authenticate(self, url, db, user, password):
"""Authenticate on remote Odoo via XML-RPC. Returns uid."""
common = ServerProxy(f"{url}/xmlrpc/2/common")
try:
uid = common.authenticate(db, user, password, {})
except Exception as exc:
raise UserError(
_("Could not connect to %(url)s:\n%(err)s",
url=url, err=exc)
) from exc
if not uid:
raise UserError(
_("Authentication failed for user '%(user)s' on database '%(db)s'.",
user=user, db=db)
)
return uid
@api.model
def _rpc_object(self, url, db, uid, password):
"""Return a ServerProxy pointing at /xmlrpc/2/object."""
return ServerProxy(f"{url}/xmlrpc/2/object")
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
@api.model
def fetch_po_from_remote(self, remote_url, remote_db, remote_user,
remote_password, module_names, lang_code):
"""
Connect to remote Odoo and export PO for *module_names* / *lang_code*.
Returns
-------
bytes
Raw PO file content (decoded from base64).
"""
if not all([remote_url, remote_db, remote_user, remote_password]):
raise UserError(_("Не заполнены все поля подключения к удалённому серверу."))
if not module_names:
raise UserError(_("Выберите хотя бы один модуль."))
remote_url = remote_url.rstrip("/")
uid = self._rpc_authenticate(remote_url, remote_db,
remote_user, remote_password)
obj = self._rpc_object(remote_url, remote_db, uid, remote_password)
_logger.info(
"Remote export: modules=%s lang=%s url=%s db=%s",
module_names, lang_code, remote_url, remote_db,
)
try:
# 1. Find module ids on remote
remote_module_ids = obj.execute_kw(
remote_db, uid, remote_password,
"ir.module.module", "search",
[[("name", "in", module_names), ("state", "=", "installed")]],
)
if not remote_module_ids:
raise UserError(
_("На удалённом сервере не найдены установленные модули: %s")
% ", ".join(module_names)
)
# 2. Create export wizard on remote
wiz_id = obj.execute_kw(
remote_db, uid, remote_password,
"base.language.export", "create",
[{
"lang": lang_code,
"format": "po",
"export_type": "module",
"modules": [(6, 0, remote_module_ids)],
}],
)
# 3. Execute act_getfile — generates the PO and stores in data field
obj.execute_kw(
remote_db, uid, remote_password,
"base.language.export", "act_getfile",
[[wiz_id]],
)
# 4. Read the resulting base64 data
result = obj.execute_kw(
remote_db, uid, remote_password,
"base.language.export", "read",
[[wiz_id], ["data", "name"]],
)
if not result or not result[0].get("data"):
raise UserError(
_("Удалённый сервер вернул пустой PO-файл.")
)
po_b64 = result[0]["data"]
filename = result[0].get("name", "export.po")
po_bytes = base64.b64decode(po_b64)
_logger.info(
"Fetched PO from remote: file=%s size=%d bytes modules=%s",
filename, len(po_bytes), module_names,
)
return po_bytes, filename
except UserError:
raise
except Exception as exc:
_logger.exception("Remote PO export failed")
raise UserError(
_("Ошибка экспорта переводов с удалённого сервера:\n%s") % exc
) from exc
@api.model
def apply_po(self, po_bytes, lang_code, overwrite=True):
"""
Import PO bytes into the current database using the standard
TranslationImporter pipeline.
Returns
-------
int
Number of translation entries processed (approximate).
"""
importer = TranslationImporter(self.env.cr)
with tempfile.NamedTemporaryFile(suffix=".po", delete=True) as tmp:
tmp.write(po_bytes)
tmp.flush()
tmp.seek(0)
importer.load(tmp, "po", lang_code)
importer.save(overwrite=overwrite)
self.env.cr.precommit.run() # flush pending writes
_logger.info(
"Applied PO (%d bytes) for lang=%s overwrite=%s",
len(po_bytes), lang_code, overwrite,
)
return len(po_bytes)
@api.model
def preview_po(self, po_bytes):
"""
Parse a PO file and return a short textual preview.
"""
import polib
po = polib.pofile(po_bytes.decode("utf-8", errors="replace"))
translated = [e for e in po if e.msgstr and e.msgid]
lines = []
lines.append(_("Всего записей: %d") % len(po))
lines.append(_("Из них переведено: %d") % len(translated))
lines.append("")
lines.append("%-50s │ %s" % (_("Оригинал"), _("Перевод")))
lines.append("─" * 50 + "─┼─" + "─" * 50)
for entry in translated[:25]:
src = (entry.msgid or "")[:50]
val = (entry.msgstr or "")[:50]
lines.append(f"{src:50s} │ {val}")
if len(translated) > 25:
lines.append("")
lines.append(_("… и ещё %d записей") % (len(translated) - 25))
return "\n".join(lines)