# Part of Odoo. See LICENSE file for full copyright and licensing details. """ Wizard: Загрузка перевода из удалённой базы. Connects to a remote Odoo 19 via XML-RPC, runs the standard PO export on the remote side, downloads the resulting PO, and imports it locally using the standard TranslationImporter pipeline. """ import base64 import logging from odoo import _, api, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class RemoteTranslationSyncWizard(models.TransientModel): _name = "translation.remote.sync.wizard" _description = "Загрузка перевода из удалённой базы" # ---- Connection ---- remote_url = fields.Char( string="URL удалённого Odoo", required=True, help="Адрес удалённого сервера (например, http://192.168.1.10:8069)", ) remote_db = fields.Char( string="База данных", required=True, help="Имя базы на удалённом сервере", ) remote_user = fields.Char( string="Логин", required=True, default="admin", ) remote_password = fields.Char( string="Пароль", required=True, ) # ---- What to sync ---- modules = fields.Many2many( "ir.module.module", string="Модули", required=True, domain=[("state", "=", "installed")], help="Модули, переводы которых нужно загрузить", ) language = fields.Many2one( "res.lang", string="Язык", required=True, default=lambda self: self.env["res.lang"].search( [("code", "=", "ru_RU")], limit=1 ), ) # ---- Options ---- overwrite = fields.Boolean( string="Перезаписать существующие переводы", default=True, help="Заменять уже имеющиеся переводы значениями из удалённой базы", ) save_credentials = fields.Boolean( string="Запомнить учётные данные", default=True, help="Сохранить URL, базу, логин и пароль для повторного использования", ) # ---- State / results ---- state = fields.Selection( [("form", "Форма"), ("preview", "Просмотр"), ("done", "Готово")], default="form", readonly=True, ) preview_lines = fields.Text( string="Просмотр", readonly=True, ) result_message = fields.Text( string="Результат", readonly=True, ) # hidden binary to cache PO between preview ↔ sync _po_cache = fields.Binary(attachment=False) # ------------------------------------------------------------------ # Defaults from saved credentials # ------------------------------------------------------------------ @api.model def default_get(self, fields_list): defaults = super().default_get(fields_list) ICP = self.env["ir.config_parameter"].sudo() defaults.setdefault("remote_url", ICP.get_param("translation_helper.remote_url", "")) defaults.setdefault("remote_db", ICP.get_param("translation_helper.remote_db", "")) defaults.setdefault("remote_user", ICP.get_param("translation_helper.remote_user", "admin")) defaults.setdefault("remote_password", ICP.get_param("translation_helper.remote_password", "")) return defaults # ------------------------------------------------------------------ # Actions # ------------------------------------------------------------------ def action_preview(self): """Fetch PO from remote, show preview, cache for later import.""" self.ensure_one() service = self.env["translation.remote.service"] po_bytes, _filename = service.fetch_po_from_remote( remote_url=self.remote_url, remote_db=self.remote_db, remote_user=self.remote_user, remote_password=self.remote_password, module_names=self.modules.mapped("name"), lang_code=self.language.code, ) preview_text = service.preview_po(po_bytes) self.write({ "preview_lines": preview_text, "state": "preview", "_po_cache": base64.b64encode(po_bytes), }) return self._reopen() def action_sync(self): """Import PO into local database.""" self.ensure_one() service = self.env["translation.remote.service"] # Use cached PO if available (from preview), otherwise re-fetch if self._po_cache: po_bytes = base64.b64decode(self._po_cache) else: po_bytes, _filename = service.fetch_po_from_remote( remote_url=self.remote_url, remote_db=self.remote_db, remote_user=self.remote_user, remote_password=self.remote_password, module_names=self.modules.mapped("name"), lang_code=self.language.code, ) # Apply service.apply_po(po_bytes, self.language.code, overwrite=self.overwrite) # Count translated entries for the message import polib po = polib.pofile(po_bytes.decode("utf-8", errors="replace")) translated_count = sum(1 for e in po if e.msgstr and e.msgid) msg = _( "Загрузка завершена!\n\n" "Модули: %(modules)s\n" "Язык: %(lang)s\n" "Переводов в файле: %(total)d\n" "Из них переведено: %(translated)d\n" "Перезапись: %(overwrite)s", modules=", ".join(self.modules.mapped("name")), lang=self.language.name, total=len(po), translated=translated_count, overwrite=_("да") if self.overwrite else _("нет"), ) # Save credentials if self.save_credentials: self._save_credentials() msg += "\n\n" + _("Учётные данные сохранены.") self.write({ "result_message": msg, "state": "done", "_po_cache": False, }) _logger.info("Remote translation sync completed for %s", self.modules.mapped("name")) return self._reopen() def action_open_local_sync(self): """Open the local-storage translation wizard.""" self.ensure_one() return { "name": _("Загрузка перевода из локального хранилища"), "type": "ir.actions.act_window", "res_model": "translation.local.sync.wizard", "view_mode": "form", "target": "new", "context": {"default_language": self.language.id}, } # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ def _save_credentials(self): ICP = self.env["ir.config_parameter"].sudo() ICP.set_param("translation_helper.remote_url", self.remote_url or "") ICP.set_param("translation_helper.remote_db", self.remote_db or "") ICP.set_param("translation_helper.remote_user", self.remote_user or "admin") ICP.set_param("translation_helper.remote_password", self.remote_password or "") def _reopen(self): """Return action that re-opens this wizard record.""" return { "name": _("Загрузка перевода из удалённой базы"), "type": "ir.actions.act_window", "res_model": self._name, "res_id": self.id, "view_mode": "form", "target": "new", }