Public release from ruodoo-project: 19.0 - 2026-05-31 21:19:12 UTC

This commit is contained in:
CI Publish Bot
2026-05-31 21:19:21 +00:00
commit aa4214c195
1213 changed files with 183945 additions and 0 deletions

View File

@ -0,0 +1,7 @@
from . import base
from . import translation_service
from . import res_config_settings
from . import ir_module
from . import ir_model_fields
from . import ir_http
from . import ir_qweb

View File

@ -0,0 +1,87 @@
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class Model(models.AbstractModel):
_inherit = "base"
def _get_record_modules(self):
"""Возвращает список модулей, в которых определены текущие записи."""
xml_items = (
self.env["ir.model.data"]
.sudo()
.search(
[
("res_id", "in", self.ids),
("model", "=", self._name),
]
)
)
return list({item.module for item in xml_items if item.module})
def update_field_translations(self, field_name, translations):
"""OVERRIDE
Update the values of a translated field.
:param str field_name: field name
:param dict translations: if the field has ``translate=True``, it should be a dictionary
like ``{lang: new_value}``; if ``translate`` is a callable, it should be like
``{lang: {old_term: new_term}}``
Odoo 19: calls super().update_field_translations() to preserve original behaviour,
which internally dispatches to _update_field_translations.
"""
modules = self._get_record_modules()
if not modules:
_logger.warning(
"Не удалось определить модуль для модели %s (ids: %s)",
self._name,
self.ids,
)
return super().update_field_translations(field_name, translations)
original_value = self.with_context(lang=None)[field_name]
if not original_value:
_logger.warning(
"Оригинальное значение поля '%s' пустое для модели %s (ids: %s)",
field_name,
self._name,
self.ids,
)
return super().update_field_translations(field_name, translations)
# Обновляем перевод для каждой локали
for lang, translated_value in translations.items():
if not translated_value:
_logger.info("Пропущен пустой перевод для языка: %s", lang)
continue
lang_record = (
self.env["res.lang"].sudo().search([("code", "=", lang)], limit=1)
)
iso_code = lang_record.iso_code or lang.split("_")[0]
source_value = original_value
translated_value_string = translated_value
if isinstance(translated_value, dict):
source_value = translated_value.get("source", original_value)
translated_value_string = list(translated_value.values())[0]
self.env[
"translation.helper.wizard"
].sudo().update_term_translation_in_module(
modules[0], source_value, translated_value_string, iso_code
)
_logger.info(
"Обновлен перевод для '%s' (%s -> %s) [lang: %s]",
field_name,
original_value,
translated_value,
lang,
)
# Odoo 19: use super() instead of calling _update_field_translations directly,
# as the internal method signature may have changed between versions.
return super().update_field_translations(field_name, translations)

View File

@ -0,0 +1,41 @@
from odoo import models
from odoo.http import request
from odoo.tools.misc import str2bool
ALLOWED_TRANSLATE_MODES = ["", "1"]
class IrHttp(models.AbstractModel):
_inherit = "ir.http"
@classmethod
def _handle_translate(cls):
"""Reads ?translate= from request params and stores in session['translate'].
Only updates session if the parameter is explicitly present in the request.
"""
translate = request.httprequest.args.get("translate")
if translate is not None:
request.session["translate"] = ",".join(
mode
if mode in ALLOWED_TRANSLATE_MODES
else "1"
if str2bool(mode, mode)
else ""
for mode in (translate or "").split(",")
)
@classmethod
def _pre_dispatch(cls, rule, args):
"""OVERRIDE: calls _handle_translate after super() to store translate in session."""
res = super()._pre_dispatch(rule, args)
cls._handle_translate()
return res
def session_info(self):
"""OVERRIDE: adds translate to bundle_params in session_info."""
session_info = super().session_info()
translate = request.session.get("translate", "")
if "bundle_params" not in session_info:
session_info["bundle_params"] = {}
session_info["bundle_params"]["translate"] = translate
return session_info

View File

@ -0,0 +1,20 @@
from odoo import api, models, tools
class IrModelFields(models.Model):
_inherit = "ir.model.fields"
@api.model
@tools.ormcache("model_name")
def get_field_string(self, model_name):
"""
Переопределение функции нам нужно для того, чтобы обойти ограничение платформы.
В нашем случае при вызове оригинальной функции перевод берется из кэша системы,
что приводи к тому, что для его появления на экране нужно перезагружать экземпляр.
Для того, чтобы обойти это мы очищаем кэш.
"""
# TODO Необходимо будет сделать сброс кэша не безусловным, а по какому либо флагу,
# например сделать режим переводчика, по аналогии с режимом разработчика
self.env.registry.clear_cache()
return super().get_field_string(model_name)

View File

@ -0,0 +1,47 @@
import logging
import os
from odoo import api, models
from odoo.modules import get_module_path
from odoo.tools.translate import TranslationImporter
_logger = logging.getLogger(__name__)
module_lname = os.path.basename(
os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
)
class Module(models.Model):
_inherit = "ir.module.module"
@api.model
def _load_module_terms(self, modules, langs, overwrite=False):
if module_lname not in modules:
return super()._load_module_terms(modules, langs, overwrite=overwrite)
# load i18n files
translation_importer = TranslationImporter(self.env.cr, verbose=False)
for module_name in modules:
modpath = get_module_path(module_name)
if not modpath:
continue
for lang in langs:
po_paths = []
for subdir in ("i18n", "i18n_extra"):
po_path = os.path.join(modpath, subdir, "%s.po" % lang)
if os.path.exists(po_path):
po_paths.append(po_path)
for po_path in po_paths:
_logger.info(
"module %s: loading translation file %s for language %s",
module_name,
po_path,
lang,
)
translation_importer.load_file(po_path, lang)
if lang != "en_US" and not po_paths:
_logger.info(
"module %s: no translation for language %s", module_name, lang
)
translation_importer.save(overwrite=overwrite, force_overwrite=overwrite)

View File

@ -0,0 +1,13 @@
from odoo import models
from odoo.http import request
class IrQWeb(models.AbstractModel):
_inherit = "ir.qweb"
def _prepare_environment(self, values):
result = super()._prepare_environment(values)
if not values.get("minimal_qcontext"):
translate = request.session.get("translate", "") if request else ""
values.setdefault("translate", translate)
return result

View File

@ -0,0 +1,33 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
translation_weblate_server_address = fields.Char(
string="Адрес",
help="Запишите адрес вашего Weblate сервера",
config_parameter="translation_helper.translation_weblate_server_address",
)
translation_weblate_server_protocol = fields.Selection(
string="Протокол",
help="Выберите протокол вашего Weblate сервера",
selection=[
("http", "HTTP"),
("https", "HTTPS"),
],
config_parameter="translation_helper.translation_weblate_server_protocol",
)
translation_weblate_project_alias = fields.Char(
string="Алиас проекта",
help="Запишите алиас вашего Weblate проекта",
config_parameter="translation_helper.translation_weblate_project_alias",
)
translation_weblate_project_language = fields.Many2one(
string="Язык проекта",
help="Выберите язык вашего алиаса проекта",
comodel_name="res.lang",
config_parameter="translation_helper.translation_weblate_project_language",
)

View File

@ -0,0 +1,135 @@
from siphashc import siphash
from odoo import _, api, models
from odoo.exceptions import UserError
DEFAULT_WEBLATE_ADDRESS = "weblate.rudoo.ru"
DEFAULT_WEBLATE_PROTOCOL = "https"
DEFAULT_WEBLATE_PROJECT_ALIAS = "testovyj-proekt"
def raw_hash(*parts: str) -> int:
"""Calculate checksum identifying translation."""
if not parts:
data = ""
elif len(parts) == 1:
data = parts[0]
else:
data = "".join(part for part in parts)
return siphash("Weblate Sip Hash", data)
def calculate_checksum(*parts: str):
"""Calculate siphashc checksum for given strings."""
return format(raw_hash(*parts), "016x")
class TranslationHelper(models.AbstractModel):
_name = "translation.service"
_description = "Translation Helper"
@api.model
def get_action(self, data=None):
model_name = data.get("resModel")
field_name = data.get("field", {}).get("name")
# Use current user's language as the default language (Odoo 19 compatible)
default_lang = self.env.user.lang
query = """
SELECT field_description
FROM ir_model_fields
WHERE model = %s AND name = %s
"""
self.env.cr.execute(query, [model_name, field_name])
translation_field_data = self.env.cr.fetchone()
current_user_language = self.env.context.get("lang", default_lang)
translation_value = translation_field_data[0].get(current_user_language)
term_to_translate = translation_field_data[0].get(default_lang)
if not term_to_translate:
term_to_translate = data.get("label")
data["translation_field_data"] = translation_field_data[0]
field = (
self.env["ir.model.fields"]
.sudo()
.search([("model", "=", model_name), ("name", "=", field_name)])
)
if not field:
raise UserError(
_("Field '%s' not found on model '%s'.") % (field_name, model_name)
)
# field.modules may be empty or a comma-separated string in Odoo 19
if field.modules:
module_names_list = field.modules.split(", ")
else:
module_names_list = []
checksum = calculate_checksum(term_to_translate)
weblate_address = (
self.env["ir.config_parameter"]
.sudo()
.get_param(
"translation_helper.translation_weblate_server_address",
default=DEFAULT_WEBLATE_ADDRESS,
)
)
weblate_protocol = (
self.env["ir.config_parameter"]
.sudo()
.get_param(
"translation_helper.translation_weblate_server_protocol",
default=DEFAULT_WEBLATE_PROTOCOL,
)
)
weblate_project_alias = (
self.env["ir.config_parameter"]
.sudo()
.get_param(
"translation_helper.translation_weblate_project_alias",
default=DEFAULT_WEBLATE_PROJECT_ALIAS,
)
)
weblate_project_language = (
self.env["res.lang"]
.sudo()
.search(
[
("code", "=", current_user_language),
"|",
("active", "=", True),
("active", "=", False),
]
)
)
for module_name in module_names_list:
url_for_module = f"{weblate_protocol}://{weblate_address}/translate/{weblate_project_alias}/{module_name}/{weblate_project_language.iso_code}/?checksum={checksum}"
wizard_record = (
self.env["translation.helper.wizard"]
.sudo()
.create(
{
"weblate_link": url_for_module,
"term_value": term_to_translate,
"language_id": weblate_project_language.id,
"translation_value": translation_value,
"modules": field.modules,
"metadata": data,
}
)
)
window_action = {
"name": _("Write your translation"),
"target": "new",
"view_mode": "form",
"res_model": "translation.helper.wizard",
"type": "ir.actions.act_window",
"res_id": wizard_record.id,
"views": [
[
self.env.ref(
"translation_helper.translation_helper_wizard_form"
).id,
"form",
]
],
}
return window_action