Refactor: Move SEO content files to root of module
@@ -1,11 +0,0 @@
|
|||||||
Odoo Proprietary License v1.0
|
|
||||||
|
|
||||||
This software and associated files (the “Software”) may only be used (executed, modified, executed after modifications) if you have purchased a valid license from the authors, typically via Odoo Apps, or if you have received a written agreement from the authors of the Software (see the COPYRIGHT file).
|
|
||||||
|
|
||||||
You may develop Odoo modules that use the Software as a library (typically by depending on it, importing it and using its resources), but without copying any source code or material from the Software. You may distribute those modules under the license of your choice, provided that this license is compatible with the terms of the Odoo Proprietary License (For example: LGPL, MIT, or proprietary licenses similar to this one).
|
|
||||||
|
|
||||||
It is forbidden to publish, distribute, sublicense, or sell copies of the Software or modified copies of the Software.
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice must be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
==================
|
|
||||||
Otoolkit Auth
|
|
||||||
==================
|
|
||||||
|
|
||||||
This module allows you to configure your Otoolkit account in odoo. You will then be able to use all the Otoolkit apps.
|
|
||||||
|
|
||||||
|
|
||||||
Question and contact
|
|
||||||
==========
|
|
||||||
If you have any questions or recommendations about this module, please contact us at contact@otoolkit.app
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
To install this module:
|
|
||||||
|
|
||||||
1. Download and place the module in your Odoo addons directory.
|
|
||||||
2. Access your Odoo server and navigate to the Apps menu.
|
|
||||||
3. Enable debug mode, then refresh the list by selecting Update Apps List.
|
|
||||||
4. Finally, click the Install button to complete the installation.
|
|
||||||
|
|
||||||
Upgrade
|
|
||||||
============
|
|
||||||
|
|
||||||
To upgrade this module:
|
|
||||||
|
|
||||||
1. Download the updated module and replace the existing one in your Odoo addons directory.
|
|
||||||
2. Restart your Odoo server.
|
|
||||||
3. Log in, go to the Apps menu, and click the Upgrade button for the module.
|
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
To configure the module, follow these steps:
|
|
||||||
|
|
||||||
1. Go to the Settings menu of your Odoo instance.
|
|
||||||
2. Go to the Otoolkit section.
|
|
||||||
3. Add your API Key from your Otoolkit account.
|
|
||||||
|
|
||||||
How to Generate an API Key ?
|
|
||||||
|
|
||||||
1. Visit https://otoolkit.app and create an account (if you don’t already have one).
|
|
||||||
2. Once logged in, navigate to the Developers section in your Otoolkit dashboard.
|
|
||||||
3. Generate a new API Key and copy it.
|
|
||||||
4. Paste the copied key into the Otoolkit section in your Odoo settings.
|
|
||||||
|
|
||||||
Credits
|
|
||||||
=======
|
|
||||||
|
|
||||||
Author & Maintainer
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
This module is maintained by the Otoolkit team.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
{
|
|
||||||
'name': "O'Toolkit",
|
|
||||||
'summary': "This app is the basic block of the Otoolkit integration. It allows you to connect your Otoolkit account to your Odoo instance.",
|
|
||||||
'description': """""",
|
|
||||||
'author': "O'Solutions Company",
|
|
||||||
'website': "https://osolutions.app/products/o-toolkit",
|
|
||||||
'category': 'Other',
|
|
||||||
'version': '19.0.1.0.0',
|
|
||||||
'depends': [],
|
|
||||||
"external_dependencies": {"python": ["requests"]},
|
|
||||||
'data': [
|
|
||||||
# security
|
|
||||||
"security/otoolkit_security.xml",
|
|
||||||
|
|
||||||
# views
|
|
||||||
"views/menu_views.xml",
|
|
||||||
"views/res_config_settings_views.xml",
|
|
||||||
|
|
||||||
# data
|
|
||||||
"data/ir_config_parameter.xml"
|
|
||||||
],
|
|
||||||
'images': ['static/description/cover.jpeg'],
|
|
||||||
'license': 'OPL-1',
|
|
||||||
'installable': True,
|
|
||||||
'application': True,
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<data noupdate="1">
|
|
||||||
<record id="otoolkit_api_endpoint" model="ir.config_parameter">
|
|
||||||
<field name="key">otoolkit.api.endpoint</field>
|
|
||||||
<field name="value">https://api.otoolkit.app</field>
|
|
||||||
</record>
|
|
||||||
</data>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
==================
|
|
||||||
Otoolkit Auth
|
|
||||||
==================
|
|
||||||
|
|
||||||
This module allows you to configure your Otoolkit account in odoo. You will then be able to use all the Otoolkit apps.
|
|
||||||
|
|
||||||
|
|
||||||
Question and contact
|
|
||||||
==========
|
|
||||||
If you have any questions or recommendations about this module, please contact us at contact@otoolkit.app
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
To install this module:
|
|
||||||
|
|
||||||
1. Download and place the module in your Odoo addons directory.
|
|
||||||
2. Access your Odoo server and navigate to the Apps menu.
|
|
||||||
3. Enable debug mode, then refresh the list by selecting Update Apps List.
|
|
||||||
4. Finally, click the Install button to complete the installation.
|
|
||||||
|
|
||||||
Upgrade
|
|
||||||
============
|
|
||||||
|
|
||||||
To upgrade this module:
|
|
||||||
|
|
||||||
1. Download the updated module and replace the existing one in your Odoo addons directory.
|
|
||||||
2. Restart your Odoo server.
|
|
||||||
3. Log in, go to the Apps menu, and click the Upgrade button for the module.
|
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
To configure the module, follow these steps:
|
|
||||||
|
|
||||||
1. Go to the Settings menu of your Odoo instance.
|
|
||||||
2. Go to the Otoolkit section.
|
|
||||||
3. Add your API Key from your Otoolkit account.
|
|
||||||
|
|
||||||
How to Generate an API Key ?
|
|
||||||
|
|
||||||
1. Visit https://otoolkit.app and create an account (if you don’t already have one).
|
|
||||||
2. Once logged in, navigate to the Developers section in your Otoolkit dashboard.
|
|
||||||
3. Generate a new API Key and copy it.
|
|
||||||
4. Paste the copied key into the Otoolkit section in your Odoo settings.
|
|
||||||
|
|
||||||
Credits
|
|
||||||
=======
|
|
||||||
|
|
||||||
Author & Maintainer
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
This module is maintained by the Otoolkit team.
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# Translation of Odoo Server.
|
|
||||||
# This file contains the translation of the following modules:
|
|
||||||
# * otoolkit_auth
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Odoo Server 17.0+e\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2025-01-16 10:59+0000\n"
|
|
||||||
"PO-Revision-Date: 2025-01-16 10:59+0000\n"
|
|
||||||
"Last-Translator: \n"
|
|
||||||
"Language-Team: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: \n"
|
|
||||||
"Plural-Forms: \n"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid ""
|
|
||||||
"<i class=\"o_button_icon oi oi-fw oi-arrow-right me-1\" aria-hidden=\"true\"/>\n"
|
|
||||||
" <span class=\"pl-2\">Open Website</span>"
|
|
||||||
msgstr ""
|
|
||||||
"<i class=\"o_button_icon oi oi-fw oi-arrow-right me-1\" aria-hidden=\"true\"/>\n"
|
|
||||||
"<span class=\"pl-2\">Aller sur le site</span>"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "API Authentication"
|
|
||||||
msgstr "Authentification"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "API Key"
|
|
||||||
msgstr "Cléf API"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "Available balance on the account"
|
|
||||||
msgstr "Solde disponible sur votre compte"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model:ir.model,name:otoolkit_auth.model_res_config_settings
|
|
||||||
msgid "Config Settings"
|
|
||||||
msgstr "Paramètres de configuration"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "Enter an API key of your Otoolkit account here"
|
|
||||||
msgstr "Entrez une clé API de votre compte Otoolkit ici"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "Otoolkit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auth.field_res_config_settings__otoolkit_api_key
|
|
||||||
msgid "Otoolkit API key"
|
|
||||||
msgstr "Clés API Otoolkit"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "Otoolkit Account"
|
|
||||||
msgstr "Compte Otoolkit"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "Otoolkit Tokens"
|
|
||||||
msgstr "Crédits Otookit"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "Refresh token count"
|
|
||||||
msgstr "Rafraîchir le nombre de crédits"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "The API key is invalid. Please update your settings."
|
|
||||||
msgstr "La clé API n'est pas valide. Veuillez mettre à jour vos paramètres."
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model:ir.model.fields,help:otoolkit_auth.field_res_config_settings__otoolkit_api_key
|
|
||||||
msgid "This API key allow you to communicate with the otoolkit api."
|
|
||||||
msgstr "Cette clé API vous permet de communiquer avec l'API otoolkit."
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid ""
|
|
||||||
"To generate an API key, you need to create an Otoolkit account. To do this, "
|
|
||||||
"follow the link below."
|
|
||||||
msgstr ""
|
|
||||||
"Pour générer une clé API, vous devez créer un compte Otoolkit. Pour ce "
|
|
||||||
"faire, suivez le lien ci-dessous."
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "To use otoolkit services, you need a valid api key."
|
|
||||||
msgstr "Pour utiliser les services otoolkit, vous avez besoin d'une clé api valide."
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auth.field_res_config_settings__otoolkit_api_token_count
|
|
||||||
msgid "Token Count"
|
|
||||||
msgstr "Nombre de crédits"
|
|
||||||
|
|
||||||
#. module: otoolkit_auth
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auth.res_config_settings_view_form
|
|
||||||
msgid "tokens"
|
|
||||||
msgstr "crédits"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from . import res_config_settings
|
|
||||||
from . import schema_builder
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import logging
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from odoo import models, fields, api
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Sentinel values for token count status
|
|
||||||
_NO_KEY = -1
|
|
||||||
_ERROR = -2
|
|
||||||
|
|
||||||
|
|
||||||
class ResConfigSettings(models.TransientModel):
|
|
||||||
_inherit = 'res.config.settings'
|
|
||||||
|
|
||||||
otoolkit_api_key = fields.Char(
|
|
||||||
string="O'Toolkit API Key",
|
|
||||||
config_parameter='otoolkit_api_key',
|
|
||||||
help="API key from your O'Solutions portal (portal.osolutions.app).",
|
|
||||||
)
|
|
||||||
|
|
||||||
otoolkit_api_token_count = fields.Integer(
|
|
||||||
string='Token Balance',
|
|
||||||
compute='_compute_token_count',
|
|
||||||
config_parameter='otoolkit_api_token_count',
|
|
||||||
readonly=True,
|
|
||||||
default=_NO_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
otoolkit_key_status = fields.Selection(
|
|
||||||
[('valid', 'Valid'), ('invalid', 'Invalid'), ('error', 'Connection Error'), ('none', 'No Key')],
|
|
||||||
string='Key Status',
|
|
||||||
compute='_compute_token_count',
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_token_balance(self, api_key):
|
|
||||||
"""Fetch token balance from O'Toolkit API (which validates via FastAPI)."""
|
|
||||||
api_endpoint = self.env['ir.config_parameter'].sudo().get_param(
|
|
||||||
'otoolkit.api.endpoint', 'https://api.otoolkit.app'
|
|
||||||
)
|
|
||||||
if not api_key:
|
|
||||||
return _NO_KEY, 'none'
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Odoo-Api-Key': api_key,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"{api_endpoint.rstrip('/')}/api/get-token-count/",
|
|
||||||
headers=headers,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
_logger.warning("O'Toolkit API connection error: %s", e)
|
|
||||||
return _ERROR, 'error'
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
return data.get('token_count', 0), 'valid'
|
|
||||||
if response.status_code == 401:
|
|
||||||
return 0, 'invalid'
|
|
||||||
|
|
||||||
_logger.warning("O'Toolkit API returned %s: %s", response.status_code, response.text[:200])
|
|
||||||
return _ERROR, 'error'
|
|
||||||
|
|
||||||
@api.depends('otoolkit_api_key')
|
|
||||||
def _compute_token_count(self):
|
|
||||||
for record in self:
|
|
||||||
api_key = record.otoolkit_api_key
|
|
||||||
count, status = record._get_token_balance(api_key)
|
|
||||||
record.otoolkit_api_token_count = count
|
|
||||||
record.otoolkit_key_status = status
|
|
||||||
self.env['ir.config_parameter'].sudo().set_param(
|
|
||||||
'otoolkit_api_token_count', str(count)
|
|
||||||
)
|
|
||||||
|
|
||||||
def refresh_token_count(self):
|
|
||||||
self._compute_token_count()
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from odoo import models, api
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Default business models, grouped by category.
|
|
||||||
# Only included if the corresponding module is installed.
|
|
||||||
_DEFAULT_BUSINESS_MODELS = {
|
|
||||||
'sale': ['sale.order', 'sale.order.line'],
|
|
||||||
'account': ['account.move', 'account.move.line', 'account.payment'],
|
|
||||||
'product': ['product.template', 'product.product', 'product.category'],
|
|
||||||
'base': ['res.partner'],
|
|
||||||
'stock': ['stock.picking', 'stock.move', 'stock.quant'],
|
|
||||||
'purchase': ['purchase.order', 'purchase.order.line'],
|
|
||||||
'crm': ['crm.lead'],
|
|
||||||
'project': ['project.project', 'project.task'],
|
|
||||||
'helpdesk': ['helpdesk.ticket'],
|
|
||||||
'hr': ['hr.employee', 'hr.leave'],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fields to always skip (technical/internal)
|
|
||||||
_SKIP_FIELDS = {
|
|
||||||
'__last_update', 'write_date', 'write_uid', 'create_date', 'create_uid',
|
|
||||||
'display_name', 'message_ids', 'message_follower_ids', 'message_partner_ids',
|
|
||||||
'message_channel_ids', 'message_attachment_count', 'message_has_error',
|
|
||||||
'message_has_error_counter', 'message_has_sms_error', 'message_is_follower',
|
|
||||||
'message_main_attachment_id', 'message_needaction', 'message_needaction_counter',
|
|
||||||
'message_unread', 'message_unread_counter', 'website_message_ids',
|
|
||||||
'activity_ids', 'activity_state', 'activity_user_id', 'activity_type_id',
|
|
||||||
'activity_date_deadline', 'activity_summary', 'activity_exception_decoration',
|
|
||||||
'activity_exception_icon', 'activity_type_icon', 'activity_calendar_event_id',
|
|
||||||
'rating_ids', 'rating_last_value', 'rating_last_feedback', 'rating_last_image',
|
|
||||||
'rating_count', 'rating_avg', 'rating_percentage_satisfaction',
|
|
||||||
'access_url', 'access_token', 'access_warning',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Type abbreviations for compact format
|
|
||||||
_TYPE_MAP = {
|
|
||||||
'char': 'char',
|
|
||||||
'text': 'text',
|
|
||||||
'html': 'html',
|
|
||||||
'integer': 'int',
|
|
||||||
'float': 'float',
|
|
||||||
'monetary': 'mon',
|
|
||||||
'boolean': 'bool',
|
|
||||||
'date': 'date',
|
|
||||||
'datetime': 'dt',
|
|
||||||
'binary': 'bin',
|
|
||||||
'selection': 'sel',
|
|
||||||
'many2one': 'm2o',
|
|
||||||
'one2many': 'o2m',
|
|
||||||
'many2many': 'm2m',
|
|
||||||
'reference': 'ref',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SchemaBuilder(models.AbstractModel):
|
|
||||||
_name = 'otk.schema.builder'
|
|
||||||
_description = "O'Toolkit Schema Builder"
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def get_business_models(self):
|
|
||||||
"""Return list of business-relevant model names.
|
|
||||||
|
|
||||||
Only includes models whose parent module is installed.
|
|
||||||
Can be overridden via ir.config_parameter 'otoolkit.business_models'.
|
|
||||||
"""
|
|
||||||
ICP = self.env['ir.config_parameter'].sudo()
|
|
||||||
custom = ICP.get_param('otoolkit.business_models', '')
|
|
||||||
if custom:
|
|
||||||
return [m.strip() for m in custom.split(',') if m.strip()]
|
|
||||||
|
|
||||||
result = []
|
|
||||||
IrModule = self.env['ir.module.module'].sudo()
|
|
||||||
for module_name, model_names in _DEFAULT_BUSINESS_MODELS.items():
|
|
||||||
if module_name == 'base':
|
|
||||||
result.extend(model_names)
|
|
||||||
continue
|
|
||||||
installed = IrModule.search([
|
|
||||||
('name', '=', module_name),
|
|
||||||
('state', '=', 'installed'),
|
|
||||||
], limit=1)
|
|
||||||
if installed:
|
|
||||||
# Verify model actually exists in registry
|
|
||||||
for model_name in model_names:
|
|
||||||
if model_name in self.env:
|
|
||||||
result.append(model_name)
|
|
||||||
return result
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def check_model_access(self, model_name):
|
|
||||||
"""Check if current user has read access to the given model."""
|
|
||||||
try:
|
|
||||||
self.env[model_name].check_access('read')
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def build_model_schema(self, model_name, depth=1):
|
|
||||||
"""Build a schema dict for a model with field metadata.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model_name: Technical model name (e.g. 'sale.order')
|
|
||||||
depth: How deep to follow relational fields (0 = no relations)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict with model info, fields list, and nested relation schemas
|
|
||||||
"""
|
|
||||||
if model_name not in self.env:
|
|
||||||
return None
|
|
||||||
|
|
||||||
Model = self.env[model_name]
|
|
||||||
model_desc = Model._description or model_name
|
|
||||||
|
|
||||||
fields_info = Model.fields_get(attributes=[
|
|
||||||
'string', 'type', 'relation', 'selection', 'required',
|
|
||||||
'readonly', 'store',
|
|
||||||
])
|
|
||||||
|
|
||||||
schema_fields = []
|
|
||||||
related_schemas = {}
|
|
||||||
|
|
||||||
for fname, finfo in sorted(fields_info.items()):
|
|
||||||
if fname.startswith('_') or fname in _SKIP_FIELDS:
|
|
||||||
continue
|
|
||||||
if not finfo.get('store', True):
|
|
||||||
continue
|
|
||||||
|
|
||||||
field_data = {
|
|
||||||
'name': fname,
|
|
||||||
'type': finfo['type'],
|
|
||||||
'label': finfo.get('string', fname),
|
|
||||||
}
|
|
||||||
|
|
||||||
if finfo['type'] == 'selection' and finfo.get('selection'):
|
|
||||||
field_data['selection'] = finfo['selection']
|
|
||||||
|
|
||||||
if finfo['type'] in ('many2one', 'one2many', 'many2many') and finfo.get('relation'):
|
|
||||||
field_data['relation'] = finfo['relation']
|
|
||||||
if depth > 0 and finfo['relation'] not in related_schemas:
|
|
||||||
related_schemas[finfo['relation']] = self.build_model_schema(
|
|
||||||
finfo['relation'], depth=depth - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if finfo.get('required'):
|
|
||||||
field_data['required'] = True
|
|
||||||
|
|
||||||
schema_fields.append(field_data)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'model': model_name,
|
|
||||||
'description': model_desc,
|
|
||||||
'fields': schema_fields,
|
|
||||||
'related': related_schemas,
|
|
||||||
}
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def compress_schema(self, schema):
|
|
||||||
"""Compress a schema dict into compact text format for API.
|
|
||||||
|
|
||||||
Saves ~75% tokens vs JSON.
|
|
||||||
|
|
||||||
Format:
|
|
||||||
=== sale.order (Sales Order) ===
|
|
||||||
id:int name:char state:sel[draft,sent,sale] date_order:dt
|
|
||||||
partner_id->res.partner amount_total:mon
|
|
||||||
"""
|
|
||||||
if not schema:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
lines.append(f"=== {schema['model']} ({schema['description']}) ===")
|
|
||||||
|
|
||||||
field_parts = []
|
|
||||||
for f in schema['fields']:
|
|
||||||
ftype = _TYPE_MAP.get(f['type'], f['type'])
|
|
||||||
|
|
||||||
if f['type'] == 'selection' and f.get('selection'):
|
|
||||||
# Include labels for AI to understand meaning (key=Label)
|
|
||||||
entries = [f"{s[0]}={s[1]}" for s in f['selection']]
|
|
||||||
part = f"{f['name']}:sel[{','.join(entries)}]"
|
|
||||||
elif f['type'] in ('many2one', 'one2many', 'many2many') and f.get('relation'):
|
|
||||||
arrow = '->' if f['type'] == 'many2one' else ':o2m->' if f['type'] == 'one2many' else ':m2m->'
|
|
||||||
if f['type'] == 'many2one':
|
|
||||||
part = f"{f['name']}->{f['relation']}"
|
|
||||||
else:
|
|
||||||
part = f"{f['name']}:{ftype}->{f['relation']}"
|
|
||||||
else:
|
|
||||||
part = f"{f['name']}:{ftype}"
|
|
||||||
|
|
||||||
field_parts.append(part)
|
|
||||||
|
|
||||||
# Group field parts into lines of ~120 chars
|
|
||||||
current_line = []
|
|
||||||
current_len = 0
|
|
||||||
for part in field_parts:
|
|
||||||
if current_len + len(part) + 1 > 120 and current_line:
|
|
||||||
lines.append(' '.join(current_line))
|
|
||||||
current_line = []
|
|
||||||
current_len = 0
|
|
||||||
current_line.append(part)
|
|
||||||
current_len += len(part) + 1
|
|
||||||
if current_line:
|
|
||||||
lines.append(' '.join(current_line))
|
|
||||||
|
|
||||||
# Add related model schemas (depth-1 only, no nested relations)
|
|
||||||
for rel_name, rel_schema in (schema.get('related') or {}).items():
|
|
||||||
if rel_schema:
|
|
||||||
lines.append('')
|
|
||||||
lines.append(self.compress_schema(rel_schema))
|
|
||||||
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def build_context_for_models(self, model_names=None, depth=1):
|
|
||||||
"""Build compressed schema context for a list of models.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model_names: List of model names. If None, uses get_business_models().
|
|
||||||
depth: Relation traversal depth.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Compressed schema string ready to send to API.
|
|
||||||
"""
|
|
||||||
if model_names is None:
|
|
||||||
model_names = self.get_business_models()
|
|
||||||
|
|
||||||
parts = []
|
|
||||||
seen = set()
|
|
||||||
for model_name in model_names:
|
|
||||||
if model_name in seen:
|
|
||||||
continue
|
|
||||||
seen.add(model_name)
|
|
||||||
if not self.check_model_access(model_name):
|
|
||||||
continue
|
|
||||||
schema = self.build_model_schema(model_name, depth=depth)
|
|
||||||
if schema:
|
|
||||||
parts.append(self.compress_schema(schema))
|
|
||||||
|
|
||||||
return '\n\n'.join(parts)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<record id="module_category_otoolkit" model="ir.module.category">
|
|
||||||
<field name="name">O'Toolkit</field>
|
|
||||||
<field name="sequence">90</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
|
Before Width: | Height: | Size: 616 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<hr style="border: none; height: 1px; background: linear-gradient(to right, transparent, #ccc, transparent); margin: 20px auto;"/>
|
|
||||||
|
|
||||||
<section class="oe_container">
|
|
||||||
<div class="col-md-12 text-center" style="margin-top: 10px;">
|
|
||||||
<a target="_blank" href="https://otoolkit.app">
|
|
||||||
<h2>Otoolkit</h2>
|
|
||||||
</a>
|
|
||||||
<p>
|
|
||||||
<a href="https://otoolkit.app" target="_blank">Website: otoolkit.app</a><br>
|
|
||||||
Email: <a href="mailto:contact@otoolkit.app">contact@otoolkit.app</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<menuitem id="menu_otoolkit_root"
|
|
||||||
name="O'Toolkit"
|
|
||||||
web_icon="otoolkit_auth,static/description/icon.png"
|
|
||||||
sequence="90"/>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<odoo>
|
|
||||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
|
||||||
<field name="name">res.config.settings.view.form.inherit.otoolkit_api</field>
|
|
||||||
<field name="model">res.config.settings</field>
|
|
||||||
<field name="priority" eval="40"/>
|
|
||||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//form" position="inside">
|
|
||||||
<app data-string="O'Toolkit" id="otoolkit_auth" string="O'Toolkit" name="otoolkit_auth">
|
|
||||||
<block title="API Authentication" id="api_authentication">
|
|
||||||
<setting id="otoolkit_api_key_settings" string="API Key"
|
|
||||||
help="Enter an O'Token key from your O'Solutions portal.">
|
|
||||||
<field name="otoolkit_api_key" class="o_light_label mt8"
|
|
||||||
placeholder="oso_k_..."/>
|
|
||||||
</setting>
|
|
||||||
<setting id="otoolkit_account_settings" string="O'Solutions Portal"
|
|
||||||
help="Manage your API keys, token balance and usage from the O'Solutions portal.">
|
|
||||||
<a href="https://portal.osolutions.app/dashboard/api-keys" target="_blank"
|
|
||||||
class="btn-link" role="button">
|
|
||||||
<i class="o_button_icon oi oi-fw oi-arrow-right me-1" aria-hidden="true"/>
|
|
||||||
<span>Open Portal</span>
|
|
||||||
</a>
|
|
||||||
</setting>
|
|
||||||
<setting id="otoolkit_tokens_settings" string="Token Balance"
|
|
||||||
help="Available O'Token balance on your account.">
|
|
||||||
<field name="otoolkit_key_status" invisible="1"/>
|
|
||||||
|
|
||||||
<div invisible="otoolkit_key_status != 'invalid'"
|
|
||||||
class="alert alert-danger mb-0" role="alert">
|
|
||||||
This API key is not valid. Please check your key in the
|
|
||||||
<a href="https://portal.osolutions.app/dashboard/api-keys"
|
|
||||||
target="_blank">O'Solutions Portal</a>.
|
|
||||||
</div>
|
|
||||||
<div invisible="otoolkit_key_status != 'error'"
|
|
||||||
class="alert alert-warning mb-0" role="alert">
|
|
||||||
Could not connect to the O'Toolkit API. Please try again later.
|
|
||||||
</div>
|
|
||||||
<div invisible="otoolkit_key_status != 'none'"
|
|
||||||
class="alert alert-info mb-0" role="alert">
|
|
||||||
Enter an API key to start using O'Toolkit services.
|
|
||||||
<a href="https://portal.osolutions.app/dashboard/api-keys"
|
|
||||||
target="_blank">Get your key</a>
|
|
||||||
</div>
|
|
||||||
<div invisible="otoolkit_key_status != 'valid'">
|
|
||||||
<span class="fw-bold fs-5"><field name="otoolkit_api_token_count" class="d-inline"/> tokens</span>
|
|
||||||
<button string="Refresh" type="object" name="refresh_token_count"
|
|
||||||
class="btn-link ms-2"/>
|
|
||||||
</div>
|
|
||||||
</setting>
|
|
||||||
</block>
|
|
||||||
</app>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
Odoo Proprietary License v1.0
|
|
||||||
|
|
||||||
This software and associated files (the “Software”) may only be used (executed, modified, executed after modifications) if you have purchased a valid license from the authors, typically via Odoo Apps, or if you have received a written agreement from the authors of the Software (see the COPYRIGHT file).
|
|
||||||
|
|
||||||
You may develop Odoo modules that use the Software as a library (typically by depending on it, importing it and using its resources), but without copying any source code or material from the Software. You may distribute those modules under the license of your choice, provided that this license is compatible with the terms of the Odoo Proprietary License (For example: LGPL, MIT, or proprietary licenses similar to this one).
|
|
||||||
|
|
||||||
It is forbidden to publish, distribute, sublicense, or sell copies of the Software or modified copies of the Software.
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice must be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
==================
|
|
||||||
Auto Translate Fields
|
|
||||||
==================
|
|
||||||
|
|
||||||
This module allows you to automatically translate all the fields that can be translated in Odoo.
|
|
||||||
|
|
||||||
It adds a button to the translation popup that will translate the content from the language activated for your user into the other languages of the instance.
|
|
||||||
|
|
||||||
Question and contact
|
|
||||||
==========
|
|
||||||
If you have any questions or recommendations about this module, please contact us at contact@otoolkit.app
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
To install this module:
|
|
||||||
|
|
||||||
1. Download and place the module in your Odoo addons directory.
|
|
||||||
2. Access your Odoo server and navigate to the Apps menu.
|
|
||||||
3. Enable debug mode, then refresh the list by selecting Update Apps List.
|
|
||||||
4. Finally, click the Install button to complete the installation.
|
|
||||||
|
|
||||||
Upgrade
|
|
||||||
============
|
|
||||||
|
|
||||||
To upgrade this module:
|
|
||||||
|
|
||||||
1. Download the updated module and replace the existing one in your Odoo addons directory.
|
|
||||||
2. Restart your Odoo server.
|
|
||||||
3. Log in, go to the Apps menu, and click the Upgrade button for the module.
|
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
This module operates directly, with no configuration required.
|
|
||||||
|
|
||||||
Credits
|
|
||||||
=======
|
|
||||||
|
|
||||||
Author & Maintainer
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
This module is maintained by the Otoolkit team.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from . import models
|
|
||||||
from . import wizards
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
{
|
|
||||||
'name': "Auto Translate Fields",
|
|
||||||
'summary': "Seamlessly translate any translatable fields in Odoo across all available languages with a single click.",
|
|
||||||
'description': """An O'Toolkit app that allow you to seamlessly translate any translatable fields in Odoo across all available languages with a single click.""",
|
|
||||||
'author': "O'Solutions Company",
|
|
||||||
'website': "https://osolutions.app/products/o-toolkit",
|
|
||||||
'category': 'Tools',
|
|
||||||
'version': '19.0.3.0.0',
|
|
||||||
'depends': ['web', 'otoolkit_auth'],
|
|
||||||
'data': [
|
|
||||||
"security/ir.model.access.csv",
|
|
||||||
|
|
||||||
"views/translation_views.xml",
|
|
||||||
"views/translation_config_views.xml",
|
|
||||||
|
|
||||||
"wizards/bulk_translate.xml",
|
|
||||||
|
|
||||||
"data/ir_cron.xml"
|
|
||||||
],
|
|
||||||
"external_dependencies": {"python": ["requests"]},
|
|
||||||
'assets': {
|
|
||||||
'web.assets_backend': [
|
|
||||||
'otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js',
|
|
||||||
'otoolkit_auto_translate_fields/static/src/xml/translation_dialog_auto_translate.xml',
|
|
||||||
'otoolkit_auto_translate_fields/static/src/js/json_pretty_field.js'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'images': ['static/description/cover.gif'],
|
|
||||||
'license': 'OPL-1',
|
|
||||||
'installable': True,
|
|
||||||
'application': True
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo noupdate="1">
|
|
||||||
<record id="otk_cron_process_bulk_translation" model="ir.cron">
|
|
||||||
<field name="name">Process Bulk Translation</field>
|
|
||||||
<field name="interval_number">5</field>
|
|
||||||
<field name="interval_type">minutes</field>
|
|
||||||
<field name="state">code</field>
|
|
||||||
<field name="active" eval="True" />
|
|
||||||
<field name="model_id" ref="otoolkit_auto_translate_fields.model_otk_translation_action"/>
|
|
||||||
<field name="code">model.cron_translate_action()</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
==================
|
|
||||||
Auto Translate Fields
|
|
||||||
==================
|
|
||||||
|
|
||||||
This module allows you to automatically translate all the fields that can be translated in Odoo.
|
|
||||||
|
|
||||||
It adds a button to the translation popup that will translate the content from the language activated for your user into the other languages of the instance.
|
|
||||||
|
|
||||||
Question and contact
|
|
||||||
==========
|
|
||||||
If you have any questions or recommendations about this module, please contact us at contact@otoolkit.app
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
To install this module:
|
|
||||||
|
|
||||||
1. Download and place the module in your Odoo addons directory.
|
|
||||||
2. Access your Odoo server and navigate to the Apps menu.
|
|
||||||
3. Enable debug mode, then refresh the list by selecting Update Apps List.
|
|
||||||
4. Finally, click the Install button to complete the installation.
|
|
||||||
|
|
||||||
Upgrade
|
|
||||||
============
|
|
||||||
|
|
||||||
To upgrade this module:
|
|
||||||
|
|
||||||
1. Download the updated module and replace the existing one in your Odoo addons directory.
|
|
||||||
2. Restart your Odoo server.
|
|
||||||
3. Log in, go to the Apps menu, and click the Upgrade button for the module.
|
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
This module operates directly, with no configuration required.
|
|
||||||
|
|
||||||
Credits
|
|
||||||
=======
|
|
||||||
|
|
||||||
Author & Maintainer
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
This module is maintained by the Otoolkit team.
|
|
||||||
@@ -1,833 +0,0 @@
|
|||||||
# Translation of Odoo Server.
|
|
||||||
# This file contains the translation of the following modules:
|
|
||||||
# * otoolkit_auto_translate_fields
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Odoo Server 19.0+e\n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"POT-Creation-Date: 2026-06-10 18:10+0000\n"
|
|
||||||
"PO-Revision-Date: 2026-06-10 18:10+0000\n"
|
|
||||||
"Last-Translator: \n"
|
|
||||||
"Language-Team: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: \n"
|
|
||||||
"Plural-Forms: \n"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_config.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "%d models have been configured for bulk translation."
|
|
||||||
msgstr "%d modèles ont été configurés pour la traduction en masse."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/wizards/bulk_translate.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"%d records queued for translation.\n"
|
|
||||||
"%d records skipped (no translatable content in selected fields)."
|
|
||||||
msgstr ""
|
|
||||||
"%d enregistrements mis en file d'attente pour traduction.\n"
|
|
||||||
"%d enregistrements ignorés (aucun contenu traduisible dans les champs "
|
|
||||||
"sélectionnés)."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "<i class=\"fa fa-exclamation-triangle\" title=\"Skipped records\"/>"
|
|
||||||
msgstr "<i class=\"fa fa-exclamation-triangle\" title=\"Enregistrements ignorés\"/>"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "<i class=\"fa fa-spinner\" title=\"Translating\"/>"
|
|
||||||
msgstr "<i class=\"fa fa-spinner\" title=\"Traduction en cours\"/>"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_form
|
|
||||||
msgid "<span>Translatable Fields</span>"
|
|
||||||
msgstr "<span>Champs traduisibles</span>"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_form
|
|
||||||
msgid ""
|
|
||||||
"<strong>Warning:</strong> This model has no translatable fields. Bulk "
|
|
||||||
"translation will not be useful."
|
|
||||||
msgstr ""
|
|
||||||
"<strong>Attention :</strong> ce modèle n'a aucun champ traduisible. La "
|
|
||||||
"traduction en masse ne sera d'aucune utilité."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_config.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "A configuration already exists for this model."
|
|
||||||
msgstr "Une configuration existe déjà pour ce modèle."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Access denied to the translation service"
|
|
||||||
msgstr "Accès refusé au service de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__active
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_search
|
|
||||||
msgid "Active"
|
|
||||||
msgstr "Actif"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.actions.act_window,help:otoolkit_auto_translate_fields.action_otk_translation_config
|
|
||||||
msgid ""
|
|
||||||
"Add models here to enable the \"Bulk translate\" action on their list views.\n"
|
|
||||||
" No code changes required - just select a model and it will automatically\n"
|
|
||||||
" get the bulk translation feature."
|
|
||||||
msgstr ""
|
|
||||||
"Ajoutez ici des modèles pour activer l'action « Traduire en masse » sur leurs vues liste.\n"
|
|
||||||
" Aucune modification de code requise : sélectionnez simplement un modèle et il\n"
|
|
||||||
" bénéficiera automatiquement de la traduction en masse."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#, python-format
|
|
||||||
msgid "All translations are already filled."
|
|
||||||
msgstr "Toutes les traductions sont déjà remplies."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"An error has occurred (HTTP %d). You can restart the translation from the "
|
|
||||||
"parent action."
|
|
||||||
msgstr ""
|
|
||||||
"Une erreur s'est produite (HTTP %d). Vous pouvez relancer la traduction "
|
|
||||||
"depuis l'action parente."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "An error occurred while contacting the translation service: %s"
|
|
||||||
msgstr ""
|
|
||||||
"Une erreur s'est produite lors du contact avec le service de traduction : %s"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation__status__api_key_error
|
|
||||||
msgid "Api Key Error"
|
|
||||||
msgstr "Erreur de clé API"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_form
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_search
|
|
||||||
msgid "Archived"
|
|
||||||
msgstr "Archivé"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/xml/translation_dialog_auto_translate.xml:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Auto Translate"
|
|
||||||
msgstr "Tout traduire"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__base_language_id
|
|
||||||
msgid "Base language"
|
|
||||||
msgstr "Langue de base"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_config.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Bulk Translation Enabled"
|
|
||||||
msgstr "Traduction en masse activée"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_config.py:0
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_config.py:0
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_mixin.py:0
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_mixin.py:0
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_bulk_translate_wizard_form
|
|
||||||
#, python-format
|
|
||||||
msgid "Bulk translate"
|
|
||||||
msgstr "Traduire en masse"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model,name:otoolkit_auto_translate_fields.model_otk_translation_bulk_translate
|
|
||||||
msgid "Bulk translate Wizard"
|
|
||||||
msgstr "Assistant de traduction en masse"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.ui.menu,name:otoolkit_auto_translate_fields.otoolkit_translation_actions_menu
|
|
||||||
msgid "Bulk translations"
|
|
||||||
msgstr "Traduction en masse"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_bulk_translate_wizard_form
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Annuler"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "Configuration"
|
|
||||||
msgstr "Configuration"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.actions.act_window,help:otoolkit_auto_translate_fields.action_otk_translation_config
|
|
||||||
msgid "Configure models for bulk translation"
|
|
||||||
msgstr "Configurez les modèles pour la traduction en masse"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__create_uid
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__create_uid
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__create_uid
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__create_uid
|
|
||||||
msgid "Created by"
|
|
||||||
msgstr "Créé par"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__create_date
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__create_date
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__create_date
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__create_date
|
|
||||||
msgid "Created on"
|
|
||||||
msgstr "Créé le"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/xml/translation_dialog_auto_translate.xml:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Discard"
|
|
||||||
msgstr "Annuler"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__display_name
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__display_name
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__display_name
|
|
||||||
msgid "Display Name"
|
|
||||||
msgstr "Nom"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation_action__status__done
|
|
||||||
msgid "Done"
|
|
||||||
msgstr "Terminé"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__done_record_ids
|
|
||||||
msgid "Done translations"
|
|
||||||
msgstr "Traductions terminées"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.actions.server,name:otoolkit_auto_translate_fields.action_enable_all_translatable_models
|
|
||||||
msgid "Enable All Translatable Models"
|
|
||||||
msgstr "Activer tous les modèles traduisibles"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation__status__error
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation_action__status__error
|
|
||||||
msgid "Error"
|
|
||||||
msgstr "Erreur"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__error_record_ids
|
|
||||||
msgid "Error translations"
|
|
||||||
msgstr "Traductions en erreur"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__field_ids
|
|
||||||
msgid "Fields"
|
|
||||||
msgstr "Champs"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__field_ids
|
|
||||||
msgid "Fields to translate"
|
|
||||||
msgstr "Champs à traduire"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Get tokens"
|
|
||||||
msgstr "Obtenir des crédits"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_search
|
|
||||||
msgid "Group By"
|
|
||||||
msgstr "Regrouper par"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__has_translatable_fields
|
|
||||||
msgid "Has Translatable Fields"
|
|
||||||
msgstr "A des champs traduisibles"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__id
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__id
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__id
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__id
|
|
||||||
msgid "ID"
|
|
||||||
msgstr "ID"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__initial_values
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_form
|
|
||||||
msgid "Initial value"
|
|
||||||
msgstr "Valeur de base"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Invalid request data"
|
|
||||||
msgstr "Données de requête invalides"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__base_language_id
|
|
||||||
msgid "Language"
|
|
||||||
msgstr "Langue"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid ""
|
|
||||||
"Large contents can take several minutes. Results are fetched automatically every few minutes —\n"
|
|
||||||
" last check:"
|
|
||||||
msgstr ""
|
|
||||||
"Les contenus volumineux peuvent prendre plusieurs minutes. Les résultats sont récupérés automatiquement à intervalles réguliers —\n"
|
|
||||||
" dernière vérification :"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_mixin__last_translation_date
|
|
||||||
msgid "Last Translation Date"
|
|
||||||
msgstr "Date de dernière traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__write_uid
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__write_uid
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__write_uid
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__write_uid
|
|
||||||
msgid "Last Updated by"
|
|
||||||
msgstr "Dernière modification par"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__write_date
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__write_date
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__write_date
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__write_date
|
|
||||||
msgid "Last Updated on"
|
|
||||||
msgstr "Dernière modification le"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__last_server_check
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__last_server_check
|
|
||||||
msgid "Last server check"
|
|
||||||
msgstr "Dernière vérification du serveur"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,help:otoolkit_auto_translate_fields.field_otk_translation__last_server_check
|
|
||||||
msgid ""
|
|
||||||
"Last time the scheduled action polled the translation server for this task's"
|
|
||||||
" result. A recent timestamp with a 'Translating on server' status means the "
|
|
||||||
"translation is still running server-side — large contents can take several "
|
|
||||||
"minutes."
|
|
||||||
msgstr ""
|
|
||||||
"Dernière fois que l'action planifiée a interrogé le serveur de traduction "
|
|
||||||
"pour le résultat de cette tâche. Un horodatage récent avec le statut "
|
|
||||||
"« Traduction en cours sur le serveur » signifie que la traduction est "
|
|
||||||
"toujours en cours côté serveur — les contenus volumineux peuvent prendre "
|
|
||||||
"plusieurs minutes."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__action_id
|
|
||||||
msgid "Linked action"
|
|
||||||
msgstr "Action liée"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__model_id
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__model_id
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__model_id
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__model_id
|
|
||||||
msgid "Model"
|
|
||||||
msgstr "Modèle"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__display_name
|
|
||||||
msgid "Name"
|
|
||||||
msgstr "Nom"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#, python-format
|
|
||||||
msgid "No other language available to translate from."
|
|
||||||
msgstr "Aucune autre langue disponible comme langue source."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/wizards/bulk_translate.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "No records selected for translation."
|
|
||||||
msgstr "Aucun enregistrement sélectionné pour la traduction."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#, python-format
|
|
||||||
msgid "No translation returned for this term."
|
|
||||||
msgstr "Aucune traduction renvoyée pour ce terme."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "No valid text fields found to translate."
|
|
||||||
msgstr "Aucun champ texte valide à traduire."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/wizards/bulk_translate.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"None of the %d selected records have translatable content in the selected fields (%s).\n"
|
|
||||||
"\n"
|
|
||||||
"Please check that the records have content in the base language (%s)."
|
|
||||||
msgstr ""
|
|
||||||
"Aucun des %d enregistrements sélectionnés n'a de contenu traduisible dans les champs sélectionnés (%s).\n"
|
|
||||||
"\n"
|
|
||||||
"Veuillez vérifier que les enregistrements ont du contenu dans la langue de base (%s)."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__processing_count
|
|
||||||
msgid "On server"
|
|
||||||
msgstr "Sur le serveur"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Open Settings"
|
|
||||||
msgstr "Ouvrir les paramètres"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_form
|
|
||||||
msgid "Open record"
|
|
||||||
msgstr "Ouvrir l'enregistrement"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.ui.menu,name:otoolkit_auto_translate_fields.otoolkit_menu
|
|
||||||
msgid "Otoolkit"
|
|
||||||
msgstr "Otoolkit"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__overwrite_existing
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__overwrite_existing
|
|
||||||
msgid "Overwrite existing translations"
|
|
||||||
msgstr "Écraser les traductions existantes"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.ui.menu,name:otoolkit_auto_translate_fields.otoolkit_translation_menu
|
|
||||||
msgid "Past translations"
|
|
||||||
msgstr "Traductions"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation_action__status__pending
|
|
||||||
msgid "Pending"
|
|
||||||
msgstr "En attente"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__pending_record_ids
|
|
||||||
msgid "Pending translations"
|
|
||||||
msgstr "Traductions en attente"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.actions.server,name:otoolkit_auto_translate_fields.otk_cron_process_bulk_translation_ir_actions_server
|
|
||||||
msgid "Process Bulk Translation"
|
|
||||||
msgstr "Gérer les traductions de masse"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation_action__status__processing
|
|
||||||
msgid "Processing"
|
|
||||||
msgstr "En cours"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__processing_record_ids
|
|
||||||
msgid "Processing translations"
|
|
||||||
msgstr "Traductions en cours"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__progress
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "Progress"
|
|
||||||
msgstr "Progression"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,help:otoolkit_auto_translate_fields.field_otk_translation_action__overwrite_existing
|
|
||||||
#: model:ir.model.fields,help:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__overwrite_existing
|
|
||||||
msgid ""
|
|
||||||
"Re-translate every term, including terms that already have a translation in "
|
|
||||||
"the target languages."
|
|
||||||
msgstr ""
|
|
||||||
"Retraduire chaque terme, y compris ceux qui ont déjà une traduction dans "
|
|
||||||
"les langues cibles."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "Record IDs"
|
|
||||||
msgstr "ID des enregistrements"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__record_id
|
|
||||||
msgid "Record id"
|
|
||||||
msgstr "ID de l'enregistrement"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "Retry translation"
|
|
||||||
msgstr "Réessayer de traduire"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation__status__revert
|
|
||||||
msgid "Revert"
|
|
||||||
msgstr "Rétablir"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_form
|
|
||||||
msgid "Revert translation"
|
|
||||||
msgstr "Rétablir à la valeur initiale"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/xml/translation_dialog_auto_translate.xml:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Save"
|
|
||||||
msgstr "Sauvegarder"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__server_action_id
|
|
||||||
msgid "Server Action"
|
|
||||||
msgstr "Action serveur"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__skipped_count
|
|
||||||
msgid "Skipped"
|
|
||||||
msgstr "Ignorés"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__skipped_record_ids
|
|
||||||
msgid "Skipped (no content)"
|
|
||||||
msgstr "Ignorés (aucun contenu)"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__status
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__status
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "Statut"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation__status__success
|
|
||||||
msgid "Success"
|
|
||||||
msgstr "Succès"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/js/translation_dialog_auto_translate.js:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Successfully translated"
|
|
||||||
msgstr "Traduction réussie"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__target_language_ids
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_bulk_translate__target_language_ids
|
|
||||||
msgid "Target languages"
|
|
||||||
msgstr "Langues cibles"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__task_id
|
|
||||||
msgid "Task ID"
|
|
||||||
msgstr "ID de tâche"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "The default language text cannot be empty."
|
|
||||||
msgstr "Le texte de la langue par défaut ne peut pas être vide."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"The translation service is not configured. Please contact your "
|
|
||||||
"administrator."
|
|
||||||
msgstr ""
|
|
||||||
"Le service de traduction n'est pas configuré. Veuillez contacter votre "
|
|
||||||
"administrateur."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"The translation service is taking too long to respond. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
"Le service de traduction met trop de temps à répondre. Veuillez réessayer."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"The translation service is temporarily unavailable (Error %d). Please try "
|
|
||||||
"again later."
|
|
||||||
msgstr ""
|
|
||||||
"Le service de traduction est temporairement indisponible (erreur %d). "
|
|
||||||
"Veuillez réessayer plus tard."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "The translation service returned an invalid response."
|
|
||||||
msgstr "Le service de traduction a renvoyé une réponse invalide."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__cost
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__token_cost
|
|
||||||
msgid "Token used"
|
|
||||||
msgstr "Crédits utilisés"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Too many requests. The service is rate-limited."
|
|
||||||
msgstr "Trop de requêtes. Le service est limité en débit."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Too many translation requests. Please wait a moment and try again."
|
|
||||||
msgstr ""
|
|
||||||
"Trop de demandes de traduction. Veuillez patienter un instant et réessayer."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.actions.server,name:otoolkit_auto_translate_fields.bulk_translate_config_action_for_product_template
|
|
||||||
msgid "Traduire en masse"
|
|
||||||
msgstr "Traduire en masse"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_config.py:0
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_config__translatable_field_ids
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_form
|
|
||||||
#, python-format
|
|
||||||
msgid "Translatable Fields"
|
|
||||||
msgstr "Champs traduisibles"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_bulk_translate_wizard_form
|
|
||||||
msgid "Translate"
|
|
||||||
msgstr "Traduire"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation__status__pending
|
|
||||||
msgid "Translating on server"
|
|
||||||
msgstr "Traduction en cours sur le serveur"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-javascript
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/static/src/xml/translation_dialog_auto_translate.xml:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translating..."
|
|
||||||
msgstr "Traduction en cours..."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model,name:otoolkit_auto_translate_fields.model_otk_translation_action
|
|
||||||
msgid "Translation Action"
|
|
||||||
msgstr "Action de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.actions.act_window,name:otoolkit_auto_translate_fields.action_otk_translation_config
|
|
||||||
#: model:ir.model,name:otoolkit_auto_translate_fields.model_otk_translation_config
|
|
||||||
msgid "Translation Configuration"
|
|
||||||
msgstr "Configuration de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model,name:otoolkit_auto_translate_fields.model_translation_dialog
|
|
||||||
msgid "Translation Dialog"
|
|
||||||
msgstr "Dialogue de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields.selection,name:otoolkit_auto_translate_fields.selection__otk_translation__status__translation_error
|
|
||||||
msgid "Translation Error"
|
|
||||||
msgstr "Erreur de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model,name:otoolkit_auto_translate_fields.model_otk_translation_mixin
|
|
||||||
msgid "Translation Mixin"
|
|
||||||
msgstr "Mixin de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.ui.menu,name:otoolkit_auto_translate_fields.otoolkit_translation_config_menu
|
|
||||||
msgid "Translation Settings"
|
|
||||||
msgstr "Paramètres de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/wizards/bulk_translate.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translation Started"
|
|
||||||
msgstr "Traduction lancée"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.actions.act_window,name:otoolkit_auto_translate_fields.action_otoolkit_translation
|
|
||||||
#: model:ir.actions.act_window,name:otoolkit_auto_translate_fields.action_otoolkit_translation_action
|
|
||||||
msgid "Translation actions"
|
|
||||||
msgstr "Actions de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__translation_count
|
|
||||||
msgid "Translation count"
|
|
||||||
msgstr "Nombre de traductions"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translation error: %s"
|
|
||||||
msgstr "Erreur de traduction : %s"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model,name:otoolkit_auto_translate_fields.model_otk_translation
|
|
||||||
msgid "Translation object with the details of the translation, the cost..."
|
|
||||||
msgstr "Objet de traduction avec les détails de la traduction, le coût..."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translation service endpoint not found"
|
|
||||||
msgstr "Point de terminaison du service de traduction introuvable"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translation service internal error"
|
|
||||||
msgstr "Erreur interne du service de traduction"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translation service is currently unavailable"
|
|
||||||
msgstr "Le service de traduction est actuellement indisponible"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translation service request timed out"
|
|
||||||
msgstr "La requête au service de traduction a expiré"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid "Translation service temporarily unavailable (bad gateway)"
|
|
||||||
msgstr "Service de traduction temporairement indisponible (bad gateway)"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation__translations
|
|
||||||
#: model:ir.model.fields,field_description:otoolkit_auto_translate_fields.field_otk_translation_action__translation_ids
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_form
|
|
||||||
msgid "Translations"
|
|
||||||
msgstr "Traductions"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Unable to connect to the translation service. Please check your internet "
|
|
||||||
"connection."
|
|
||||||
msgstr ""
|
|
||||||
"Impossible de se connecter au service de traduction. Veuillez vérifier "
|
|
||||||
"votre connexion Internet."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Unexpected response from translation service (Error %d). Please try again."
|
|
||||||
msgstr ""
|
|
||||||
"Réponse inattendue du service de traduction (erreur %d). Veuillez "
|
|
||||||
"réessayer."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otk_translation_config_search
|
|
||||||
msgid "With Translatable Fields"
|
|
||||||
msgstr "Avec champs traduisibles"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"You do not have enough credits to translate. Please add credits to your "
|
|
||||||
"balance to use this function."
|
|
||||||
msgstr ""
|
|
||||||
"Vous n'avez pas assez de crédits pour traduire. Veuillez ajouter des crédits"
|
|
||||||
" à votre solde pour utiliser cette fonction."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation_dialog.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Your API key is invalid. This may be due to an input error, deletion or "
|
|
||||||
"deactivation of the key. Please check your Otoolkit settings."
|
|
||||||
msgstr ""
|
|
||||||
"Votre clé API n'est pas valide. Cela peut être dû à une erreur de saisie, à "
|
|
||||||
"la suppression ou à la désactivation de la clé. Veuillez vérifier les "
|
|
||||||
"paramètres Otoolkit."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#. odoo-python
|
|
||||||
#: code:addons/otoolkit_auto_translate_fields/models/translation.py:0
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Your API key is invalid. Use a valid key and restart the translation from "
|
|
||||||
"the parent action."
|
|
||||||
msgstr ""
|
|
||||||
"Votre clé API n'est pas valide. Utilisez une clé valide et relancez la "
|
|
||||||
"traduction depuis l'action parente."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "record(s) being translated on the O'Toolkit server."
|
|
||||||
msgstr "enregistrement(s) en cours de traduction sur le serveur O'Toolkit."
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid "records skipped"
|
|
||||||
msgstr "enregistrements ignorés"
|
|
||||||
|
|
||||||
#. module: otoolkit_auto_translate_fields
|
|
||||||
#: model_terms:ir.ui.view,arch_db:otoolkit_auto_translate_fields.view_otoolkit_translation_action_form
|
|
||||||
msgid ""
|
|
||||||
"— these records had no translatable content in the selected fields and were "
|
|
||||||
"not sent to the API (no tokens consumed)."
|
|
||||||
msgstr ""
|
|
||||||
"— ces enregistrements n'avaient aucun contenu traduisible dans les champs "
|
|
||||||
"sélectionnés et n'ont pas été envoyés à l'API (aucun crédit consommé)."
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from . import translation_mixin
|
|
||||||
from . import translation_dialog
|
|
||||||
from . import translation
|
|
||||||
from . import translation_config
|
|
||||||
@@ -1,825 +0,0 @@
|
|||||||
import json
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
from odoo import models, fields, api, _
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_null_bytes(value):
|
|
||||||
"""Remove NULL bytes that PostgreSQL cannot store in JSON/text fields.
|
|
||||||
|
|
||||||
PostgreSQL's jsonb type cannot handle \u0000 (NULL byte) characters.
|
|
||||||
These often appear in content copied from PDFs or due to encoding issues.
|
|
||||||
"""
|
|
||||||
if isinstance(value, str):
|
|
||||||
return value.replace('\x00', '').replace('\u0000', '')
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
return {k: _sanitize_null_bytes(v) for k, v in value.items()}
|
|
||||||
elif isinstance(value, list):
|
|
||||||
return [_sanitize_null_bytes(v) for v in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_translatable_value(value):
|
|
||||||
"""Check if a value is valid for translation.
|
|
||||||
|
|
||||||
Returns False for: None, False (bool), empty strings, whitespace-only strings.
|
|
||||||
Returns True for non-empty strings (including HTML content).
|
|
||||||
"""
|
|
||||||
if value is None or value is False:
|
|
||||||
return False
|
|
||||||
if isinstance(value, str):
|
|
||||||
return bool(value.strip())
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class TranslationAction(models.Model):
|
|
||||||
_name = 'otk.translation.action'
|
|
||||||
_description = 'Translation Action'
|
|
||||||
_rec_name = 'display_name'
|
|
||||||
_order = 'id desc'
|
|
||||||
|
|
||||||
display_name = fields.Char(string="Name", compute='_compute_display_name', store=True, readonly=True)
|
|
||||||
|
|
||||||
model_id = fields.Many2one('ir.model', string="Model", ondelete='cascade', readonly=True)
|
|
||||||
base_language_id = fields.Many2one('res.lang', string="Language", readonly=True)
|
|
||||||
target_language_ids = fields.Many2many('res.lang', string="Target languages", required=True, ondelete='cascade',
|
|
||||||
readonly=True)
|
|
||||||
field_ids = fields.Many2many(
|
|
||||||
'ir.model.fields',
|
|
||||||
string="Fields to translate",
|
|
||||||
domain="[('translate', 'in', ['standard', 'html_translate', 'xml_translate']), ('model_id', '=', model_id)]",
|
|
||||||
readonly=True,
|
|
||||||
)
|
|
||||||
overwrite_existing = fields.Boolean(
|
|
||||||
string="Overwrite existing translations",
|
|
||||||
default=False,
|
|
||||||
readonly=True,
|
|
||||||
help="Re-translate every term, including terms that already have a "
|
|
||||||
"translation in the target languages.",
|
|
||||||
)
|
|
||||||
pending_record_ids = fields.Json(default=list, string="Pending translations", readonly=True)
|
|
||||||
processing_record_ids = fields.Json(default=list, string="Processing translations", readonly=True)
|
|
||||||
done_record_ids = fields.Json(default=list, string="Done translations", readonly=True)
|
|
||||||
error_record_ids = fields.Json(default=list, string="Error translations", readonly=True)
|
|
||||||
skipped_record_ids = fields.Json(default=list, string="Skipped (no content)", readonly=True)
|
|
||||||
skipped_count = fields.Integer(string="Skipped", compute='_compute_skipped_count', store=True)
|
|
||||||
processing_count = fields.Integer(string="On server", compute='_compute_processing_count')
|
|
||||||
last_server_check = fields.Datetime(
|
|
||||||
string="Last server check", compute='_compute_last_server_check',
|
|
||||||
)
|
|
||||||
progress = fields.Float(store=True, string="Progress", compute="_compute_progress", readonly=True)
|
|
||||||
translation_count = fields.Integer(readonly=True, string="Translation count")
|
|
||||||
token_cost = fields.Integer(default=0, string="Token used", readonly=True)
|
|
||||||
|
|
||||||
translation_ids = fields.One2many(
|
|
||||||
comodel_name='otk.translation',
|
|
||||||
inverse_name='action_id',
|
|
||||||
string='Translations',
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
status = fields.Selection(
|
|
||||||
[('pending', 'Pending'), ('done', 'Done'), ('error', 'Error'), ('processing', 'Processing')],
|
|
||||||
compute='_compute_status',
|
|
||||||
string='Status',
|
|
||||||
store=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_error_message_for_status(self, status_code, response_text=""):
|
|
||||||
"""Get a user-friendly error message based on HTTP status code."""
|
|
||||||
error_messages = {
|
|
||||||
400: _("Invalid request data"),
|
|
||||||
401: _("Your API key is invalid. Use a valid key and restart the translation from the parent action."),
|
|
||||||
403: _("Access denied to the translation service"),
|
|
||||||
404: _("Translation service endpoint not found"),
|
|
||||||
429: _("Too many requests. The service is rate-limited."),
|
|
||||||
500: _("Translation service internal error"),
|
|
||||||
502: _("Translation service temporarily unavailable (bad gateway)"),
|
|
||||||
503: _("Translation service is currently unavailable"),
|
|
||||||
504: _("Translation service request timed out"),
|
|
||||||
}
|
|
||||||
return error_messages.get(status_code, _("An error has occurred (HTTP %d). You can restart the translation from the parent action.") % status_code)
|
|
||||||
|
|
||||||
def _create_error_translation(self, record_id, status_code, error_details=""):
|
|
||||||
"""Create a translation record with error status and detailed message."""
|
|
||||||
if status_code == 401:
|
|
||||||
status = 'api_key_error'
|
|
||||||
else:
|
|
||||||
status = 'translation_error'
|
|
||||||
|
|
||||||
error_message = self._get_error_message_for_status(status_code)
|
|
||||||
if error_details:
|
|
||||||
error_message = f"{error_message}\nDetails: {error_details}"
|
|
||||||
|
|
||||||
self.env["otk.translation"].create({
|
|
||||||
'action_id': self.id,
|
|
||||||
'cost': 0,
|
|
||||||
'record_id': record_id,
|
|
||||||
'status': status,
|
|
||||||
'translations': error_message,
|
|
||||||
})
|
|
||||||
|
|
||||||
def _move_record_to_error(self, record_id):
|
|
||||||
"""Move a record from pending to error state."""
|
|
||||||
pending = self.pending_record_ids if self.pending_record_ids else []
|
|
||||||
error = self.error_record_ids if self.error_record_ids else []
|
|
||||||
if record_id in pending:
|
|
||||||
pending.remove(record_id)
|
|
||||||
if record_id not in error:
|
|
||||||
error.append(record_id)
|
|
||||||
self.write({
|
|
||||||
'pending_record_ids': pending,
|
|
||||||
'error_record_ids': error,
|
|
||||||
})
|
|
||||||
|
|
||||||
def _mark_pending_translations_as_error(self, translations, error_message):
|
|
||||||
"""Mark multiple pending translations as error with a message."""
|
|
||||||
for translation in translations:
|
|
||||||
translation.write({
|
|
||||||
'status': 'translation_error',
|
|
||||||
'translations': error_message,
|
|
||||||
})
|
|
||||||
# Move record to error state in the action
|
|
||||||
action = translation.action_id
|
|
||||||
if action:
|
|
||||||
processing = action.processing_record_ids if action.processing_record_ids else []
|
|
||||||
error_ids = action.error_record_ids if action.error_record_ids else []
|
|
||||||
record_id = translation.record_id
|
|
||||||
if record_id in processing:
|
|
||||||
try:
|
|
||||||
processing.remove(record_id)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if record_id not in error_ids:
|
|
||||||
error_ids.append(record_id)
|
|
||||||
action.write({
|
|
||||||
'processing_record_ids': processing,
|
|
||||||
'error_record_ids': error_ids,
|
|
||||||
})
|
|
||||||
|
|
||||||
def _request_terms_translation(self, terms, terms_meta, target_langs, base_lang_code, record_id):
|
|
||||||
"""Queue a terms-mode translation task on the OToolKit API.
|
|
||||||
|
|
||||||
:param dict terms: ``{field_name: [text, ...]}`` base-language texts
|
|
||||||
:param dict terms_meta: ``{field_name: [{"source": term_en, "langs": [...]}]}``
|
|
||||||
aligned with ``terms``; echoed back by the API so the cron can map
|
|
||||||
translations to Odoo's source terms at apply time
|
|
||||||
:return: the API task id, or False on error
|
|
||||||
"""
|
|
||||||
api_key = self.env['ir.config_parameter'].sudo().get_param('otoolkit_api_key')
|
|
||||||
api_endpoint = self.env['ir.config_parameter'].sudo().get_param('otoolkit.api.endpoint')
|
|
||||||
|
|
||||||
if not api_key:
|
|
||||||
_logger.error("OToolKit API key not configured for bulk translation")
|
|
||||||
self._create_error_translation(record_id, 401, "API key not configured")
|
|
||||||
self._move_record_to_error(record_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not api_endpoint:
|
|
||||||
_logger.error("OToolKit API endpoint not configured for bulk translation")
|
|
||||||
self._create_error_translation(record_id, 500, "API endpoint not configured")
|
|
||||||
self._move_record_to_error(record_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
payload = json.dumps({
|
|
||||||
"terms": terms,
|
|
||||||
"terms_meta": terms_meta,
|
|
||||||
"target_langs": target_langs,
|
|
||||||
"base_language": base_lang_code,
|
|
||||||
"record_id": record_id,
|
|
||||||
"odoo_user_id": self.env.user.id
|
|
||||||
})
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Odoo-Api-Key': api_key,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
url = f"{api_endpoint}/api/auto-translate-fields/v2/translate-terms/"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.request("POST", url, headers=headers, data=payload, timeout=180)
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
_logger.warning("OToolKit API request timed out for record %d: %s", record_id, url)
|
|
||||||
self._create_error_translation(record_id, 504, "Request timed out")
|
|
||||||
self._move_record_to_error(record_id)
|
|
||||||
return False
|
|
||||||
except requests.exceptions.ConnectionError as e:
|
|
||||||
_logger.error("OToolKit API connection error for record %d: %s - %s", record_id, url, str(e))
|
|
||||||
self._create_error_translation(record_id, 503, f"Connection error: {str(e)}")
|
|
||||||
self._move_record_to_error(record_id)
|
|
||||||
return False
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
_logger.error("OToolKit API request failed for record %d: %s - %s", record_id, url, str(e))
|
|
||||||
self._create_error_translation(record_id, 500, f"Request failed: {str(e)}")
|
|
||||||
self._move_record_to_error(record_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
# Try to extract error details from response
|
|
||||||
error_details = ""
|
|
||||||
try:
|
|
||||||
error_json = response.json()
|
|
||||||
error_details = error_json.get("error", "") or error_json.get("detail", "")
|
|
||||||
error_type = error_json.get("type", "")
|
|
||||||
if error_type:
|
|
||||||
error_details = f"[{error_type}] {error_details}"
|
|
||||||
except (json.JSONDecodeError, KeyError):
|
|
||||||
error_details = response.text[:200] if response.text else ""
|
|
||||||
|
|
||||||
_logger.warning(
|
|
||||||
"OToolKit API returned status %d for record %d: %s",
|
|
||||||
response.status_code, record_id, error_details
|
|
||||||
)
|
|
||||||
|
|
||||||
self._create_error_translation(record_id, response.status_code, error_details)
|
|
||||||
self._move_record_to_error(record_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = response.json()
|
|
||||||
return data["task_id"]
|
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
|
||||||
_logger.error("Failed to parse OToolKit API response for record %d: %s", record_id, str(e))
|
|
||||||
self._create_error_translation(record_id, 500, f"Invalid response format: {str(e)}")
|
|
||||||
self._move_record_to_error(record_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cron_translate_action(self):
|
|
||||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
|
||||||
max_time = int(get_param('otoolkit.translate.cron_max_time', 60))
|
|
||||||
max_record = int(get_param('otoolkit.translate.cron_max_records', 10))
|
|
||||||
max_iter = 10
|
|
||||||
action = self.env['otk.translation.action'].sudo().search([('status', '=', 'pending')], order='id asc', limit=1)
|
|
||||||
start = int(time.time())
|
|
||||||
|
|
||||||
while action and max_iter > 0:
|
|
||||||
can_restart = self.translate_action(action, max_record)
|
|
||||||
if not can_restart:
|
|
||||||
break
|
|
||||||
action = self.env['otk.translation.action'].sudo().search([('status', '=', 'pending')], order='id asc',
|
|
||||||
limit=1)
|
|
||||||
if int(time.time()) - start > max_time:
|
|
||||||
break
|
|
||||||
|
|
||||||
max_iter -= 1
|
|
||||||
|
|
||||||
self.cron_retrieve_tasks()
|
|
||||||
|
|
||||||
def cron_retrieve_tasks(self):
|
|
||||||
pending_translations = self.env["otk.translation"].sudo().search(
|
|
||||||
[('status', '=', 'pending')], order='id asc', limit=100
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(pending_translations) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
api_key = self.env['ir.config_parameter'].sudo().get_param('otoolkit_api_key')
|
|
||||||
api_endpoint = self.env['ir.config_parameter'].sudo().get_param('otoolkit.api.endpoint')
|
|
||||||
if not api_key:
|
|
||||||
return
|
|
||||||
|
|
||||||
payload = json.dumps({
|
|
||||||
"task_ids": pending_translations.mapped('task_id'),
|
|
||||||
})
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Odoo-Api-Key': api_key,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
url = f"{api_endpoint}/api/tasks/"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.request("POST", url, headers=headers, data=payload, timeout=120)
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
_logger.error("OToolKit API timeout while retrieving tasks")
|
|
||||||
self._mark_pending_translations_as_error(pending_translations, "API timeout while retrieving translation status")
|
|
||||||
return
|
|
||||||
except requests.exceptions.ConnectionError as e:
|
|
||||||
_logger.error("OToolKit API connection error while retrieving tasks: %s", str(e))
|
|
||||||
self._mark_pending_translations_as_error(pending_translations, f"Connection error: {str(e)}")
|
|
||||||
return
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
_logger.error("OToolKit API request failed while retrieving tasks: %s", str(e))
|
|
||||||
self._mark_pending_translations_as_error(pending_translations, f"Request failed: {str(e)}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
error_msg = f"API returned status {response.status_code}"
|
|
||||||
try:
|
|
||||||
error_json = response.json()
|
|
||||||
error_detail = error_json.get("error", "") or error_json.get("detail", "")
|
|
||||||
if error_detail:
|
|
||||||
error_msg = f"{error_msg}: {error_detail}"
|
|
||||||
except (json.JSONDecodeError, KeyError):
|
|
||||||
if response.text:
|
|
||||||
error_msg = f"{error_msg}: {response.text[:200]}"
|
|
||||||
_logger.error("OToolKit API error while retrieving tasks: %s", error_msg)
|
|
||||||
self._mark_pending_translations_as_error(pending_translations, error_msg)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
tasks = response.json()["tasks"]
|
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
|
||||||
_logger.error("Failed to parse OToolKit API response for tasks: %s", str(e))
|
|
||||||
self._mark_pending_translations_as_error(pending_translations, f"Invalid API response format: {str(e)}")
|
|
||||||
return
|
|
||||||
|
|
||||||
task_map = {task["id"]: task for task in tasks}
|
|
||||||
|
|
||||||
# Every polled translation gets a check timestamp so the UI can show
|
|
||||||
# that a long-running server-side task is being watched, not stalled.
|
|
||||||
pending_translations.write({'last_server_check': fields.Datetime.now()})
|
|
||||||
|
|
||||||
action_update = {}
|
|
||||||
|
|
||||||
for translation in pending_translations:
|
|
||||||
action_id = translation.action_id.id
|
|
||||||
if action_id not in action_update:
|
|
||||||
action_update[action_id] = {
|
|
||||||
'error': [],
|
|
||||||
'completed': [],
|
|
||||||
'action': translation.action_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
related_task = task_map.get(translation.task_id)
|
|
||||||
|
|
||||||
# Skip if task not found in API response (still pending on server side)
|
|
||||||
if not related_task:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Safely extract record_id with error handling
|
|
||||||
try:
|
|
||||||
active_id = related_task['params']['record_id']
|
|
||||||
except (KeyError, TypeError) as e:
|
|
||||||
_logger.error("Invalid task structure for task %s: missing params.record_id - %s", translation.task_id, str(e))
|
|
||||||
translation.write({
|
|
||||||
'status': 'translation_error',
|
|
||||||
'translations': f"Invalid task response structure: {str(e)}",
|
|
||||||
})
|
|
||||||
action_update[action_id]['error'].append(translation.record_id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if related_task["status"] == "completed":
|
|
||||||
try:
|
|
||||||
action_update[action_id]['completed'].append(active_id)
|
|
||||||
action = action_update[action_id]['action']
|
|
||||||
record = self.env[action.model_id.model].with_context(lang=action.base_language_id.code).browse(active_id)
|
|
||||||
|
|
||||||
if not record.exists():
|
|
||||||
translation.write({
|
|
||||||
'status': 'translation_error',
|
|
||||||
'translations': "Record was deleted before the translations could be applied",
|
|
||||||
})
|
|
||||||
action_update[action_id]['completed'].remove(active_id)
|
|
||||||
action_update[action_id]['error'].append(active_id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
cost = related_task['result'].get('cost', 0)
|
|
||||||
|
|
||||||
applied, previous_values = self._apply_terms_translations(record, related_task)
|
|
||||||
|
|
||||||
translation.write({
|
|
||||||
'cost': cost,
|
|
||||||
'status': 'success',
|
|
||||||
'translations': applied,
|
|
||||||
'initial_values': previous_values,
|
|
||||||
})
|
|
||||||
# Update last_translation_date only if the field exists on the model
|
|
||||||
# (for backward compatibility with models using the mixin)
|
|
||||||
if 'last_translation_date' in self.env[action.model_id.model]._fields:
|
|
||||||
record.write({
|
|
||||||
"last_translation_date": datetime.datetime.now(),
|
|
||||||
})
|
|
||||||
except (KeyError, TypeError) as e:
|
|
||||||
_logger.error("Error processing completed task %s for record %d: %s", translation.task_id, active_id, str(e))
|
|
||||||
translation.write({
|
|
||||||
'status': 'translation_error',
|
|
||||||
'translations': f"Error processing translation result: {str(e)}",
|
|
||||||
})
|
|
||||||
# Move from completed to error
|
|
||||||
if active_id in action_update[action_id]['completed']:
|
|
||||||
action_update[action_id]['completed'].remove(active_id)
|
|
||||||
action_update[action_id]['error'].append(active_id)
|
|
||||||
except Exception as e:
|
|
||||||
_logger.error("Unexpected error processing task %s for record %d: %s", translation.task_id, active_id, str(e))
|
|
||||||
translation.write({
|
|
||||||
'status': 'translation_error',
|
|
||||||
'translations': f"Unexpected error: {str(e)}",
|
|
||||||
})
|
|
||||||
if active_id in action_update[action_id]['completed']:
|
|
||||||
action_update[action_id]['completed'].remove(active_id)
|
|
||||||
action_update[action_id]['error'].append(active_id)
|
|
||||||
|
|
||||||
elif related_task["status"] == "failed":
|
|
||||||
# Extract error message from the API response
|
|
||||||
error_message = "Translation failed"
|
|
||||||
try:
|
|
||||||
result = related_task.get('result', {})
|
|
||||||
if isinstance(result, dict):
|
|
||||||
error_message = result.get('error', '') or result.get('message', '') or result.get('detail', '')
|
|
||||||
if not error_message:
|
|
||||||
error_message = str(result) if result else "Translation failed (no details provided)"
|
|
||||||
except Exception:
|
|
||||||
error_message = "Translation failed (could not parse error details)"
|
|
||||||
|
|
||||||
_logger.warning("Translation task %s failed for record %d: %s", translation.task_id, active_id, error_message)
|
|
||||||
action_update[action_id]['error'].append(active_id)
|
|
||||||
translation.write({
|
|
||||||
'status': 'translation_error',
|
|
||||||
'translations': error_message,
|
|
||||||
})
|
|
||||||
|
|
||||||
for key, value in action_update.items():
|
|
||||||
action = value['action']
|
|
||||||
processing = action.processing_record_ids if action.processing_record_ids else []
|
|
||||||
error = action.error_record_ids if action.error_record_ids else []
|
|
||||||
done = action.done_record_ids if action.done_record_ids else []
|
|
||||||
|
|
||||||
for translation_id in value['completed']:
|
|
||||||
try:
|
|
||||||
processing.remove(translation_id)
|
|
||||||
except ValueError:
|
|
||||||
_logger.debug("Record %d not found in processing list", translation_id)
|
|
||||||
if translation_id not in done:
|
|
||||||
done.append(translation_id)
|
|
||||||
|
|
||||||
for translation_id in value['error']:
|
|
||||||
try:
|
|
||||||
processing.remove(translation_id)
|
|
||||||
except ValueError:
|
|
||||||
_logger.debug("Record %d not found in processing list", translation_id)
|
|
||||||
if translation_id not in error:
|
|
||||||
error.append(translation_id)
|
|
||||||
|
|
||||||
action.write({
|
|
||||||
'processing_record_ids': processing,
|
|
||||||
'error_record_ids': error,
|
|
||||||
'done_record_ids': done,
|
|
||||||
})
|
|
||||||
|
|
||||||
def _extract_record_terms(self, record, field_names, base_lang_code, target_langs, overwrite=False):
|
|
||||||
"""Build the per-term translation payload for one record.
|
|
||||||
|
|
||||||
Term extraction goes through ``get_field_translations`` so it matches
|
|
||||||
Odoo's own translation model exactly: per-language values keep an
|
|
||||||
identical structure (only text terms differ), and a term whose target
|
|
||||||
value is empty or identical to the source is untranslated. Only those
|
|
||||||
terms are sent, each restricted to the languages that actually need it
|
|
||||||
— unless ``overwrite`` re-translates everything.
|
|
||||||
|
|
||||||
:return: (terms, terms_meta) where
|
|
||||||
terms: ``{field_name: [text, ...]}`` base-language texts to translate
|
|
||||||
terms_meta: ``{field_name: [{"source": term_en, "langs": [...]}]}``
|
|
||||||
aligned with ``terms``
|
|
||||||
"""
|
|
||||||
terms = {}
|
|
||||||
terms_meta = {}
|
|
||||||
langs = list(set(target_langs) | {base_lang_code})
|
|
||||||
|
|
||||||
for field_name in field_names:
|
|
||||||
field = record._fields.get(field_name)
|
|
||||||
if field is None or not field.translate:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
translations, _ctx = record.get_field_translations(field_name, langs=langs)
|
|
||||||
except Exception as e:
|
|
||||||
_logger.warning(
|
|
||||||
"Could not extract translation terms for %s.%s (record %d): %s",
|
|
||||||
record._name, field_name, record.id, str(e),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
by_source = {}
|
|
||||||
for entry in translations:
|
|
||||||
by_source.setdefault(entry['source'], {})[entry['lang']] = entry['value']
|
|
||||||
|
|
||||||
field_terms = []
|
|
||||||
field_meta = []
|
|
||||||
for term_en, values in by_source.items():
|
|
||||||
if callable(field.translate):
|
|
||||||
# value == '' means untranslated (or identical to the
|
|
||||||
# en_US term — Odoo's copy-of-source default)
|
|
||||||
needed = [l for l in target_langs if overwrite or not values.get(l)]
|
|
||||||
else:
|
|
||||||
# model-level translation: one term == the whole field
|
|
||||||
# value; untranslated langs fall back to the en_US value
|
|
||||||
needed = [
|
|
||||||
l for l in target_langs
|
|
||||||
if overwrite or not values.get(l) or values.get(l) == term_en
|
|
||||||
]
|
|
||||||
text = values.get(base_lang_code) or term_en
|
|
||||||
if not needed or not _is_valid_translatable_value(text):
|
|
||||||
continue
|
|
||||||
field_terms.append(_sanitize_null_bytes(text))
|
|
||||||
field_meta.append({"source": term_en, "langs": needed})
|
|
||||||
|
|
||||||
if field_terms:
|
|
||||||
terms[field_name] = field_terms
|
|
||||||
terms_meta[field_name] = field_meta
|
|
||||||
|
|
||||||
return terms, terms_meta
|
|
||||||
|
|
||||||
def _apply_terms_translations(self, record, task):
|
|
||||||
"""Apply a completed terms-translation task through the ORM.
|
|
||||||
|
|
||||||
Writes via ``update_field_translations`` so cache invalidation,
|
|
||||||
sanitization and ``write_date`` behave like any user write, and the
|
|
||||||
per-language values keep a structure identical to the source. Terms
|
|
||||||
whose source disappeared from the record between send and apply (the
|
|
||||||
record was edited meanwhile) simply don't match and are skipped.
|
|
||||||
|
|
||||||
:return: (applied, previous_values) where
|
|
||||||
applied: ``{field: {lang: {source_term: new_term}}}``
|
|
||||||
previous_values: revert payload — ``{field: {lang: {new_term: old_term}}}``
|
|
||||||
for term-translated fields, ``{field: {lang: old_value}}`` for
|
|
||||||
model-translated fields
|
|
||||||
"""
|
|
||||||
terms_meta = task['params'].get('terms_meta') or {}
|
|
||||||
translations_by_field = task['result'].get('translations') or {}
|
|
||||||
target_langs = task['params']['target_langs']
|
|
||||||
|
|
||||||
applied = {}
|
|
||||||
previous_values = {}
|
|
||||||
|
|
||||||
for field_name, field_meta in terms_meta.items():
|
|
||||||
field = record._fields.get(field_name)
|
|
||||||
if field is None or not field.translate:
|
|
||||||
continue
|
|
||||||
translated_terms = translations_by_field.get(field_name) or []
|
|
||||||
if len(translated_terms) != len(field_meta):
|
|
||||||
_logger.warning(
|
|
||||||
"Translation count mismatch for %s.%s (record %d): sent %d terms, got %d",
|
|
||||||
record._name, field_name, record.id, len(field_meta), len(translated_terms),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Map each en_US source term to its current value per language:
|
|
||||||
# update_field_translations matches old terms against the value as
|
|
||||||
# it currently stands in the target language, not the en_US source.
|
|
||||||
current, _ctx = record.get_field_translations(field_name, langs=target_langs)
|
|
||||||
current_map = {(e['lang'], e['source']): e['value'] for e in current}
|
|
||||||
|
|
||||||
payload = {}
|
|
||||||
revert = {}
|
|
||||||
field_applied = {}
|
|
||||||
for meta, term_translations in zip(field_meta, translated_terms):
|
|
||||||
source = meta['source']
|
|
||||||
for lang in meta['langs']:
|
|
||||||
new_term = term_translations.get(lang)
|
|
||||||
if not new_term or not isinstance(new_term, str):
|
|
||||||
_logger.warning(
|
|
||||||
"Missing translation for field %s, lang %s in record %d",
|
|
||||||
field_name, lang, record.id,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
new_term = _sanitize_null_bytes(new_term)
|
|
||||||
if callable(field.translate):
|
|
||||||
old_term = current_map.get((lang, source)) or source
|
|
||||||
if old_term == new_term:
|
|
||||||
continue
|
|
||||||
payload.setdefault(lang, {})[old_term] = new_term
|
|
||||||
revert.setdefault(lang, {})[new_term] = old_term
|
|
||||||
else:
|
|
||||||
old_value = current_map.get((lang, source)) or source
|
|
||||||
if old_value == new_term:
|
|
||||||
continue
|
|
||||||
payload[lang] = new_term
|
|
||||||
revert[lang] = old_value
|
|
||||||
field_applied.setdefault(lang, {})[source] = new_term
|
|
||||||
|
|
||||||
if payload:
|
|
||||||
record.update_field_translations(field_name, payload)
|
|
||||||
applied[field_name] = field_applied
|
|
||||||
previous_values[field_name] = revert
|
|
||||||
|
|
||||||
return applied, previous_values
|
|
||||||
|
|
||||||
def translate_action(self, action, max_record):
|
|
||||||
active_ids = action.pending_record_ids[0:max_record]
|
|
||||||
langs = action.target_language_ids
|
|
||||||
target_langs = []
|
|
||||||
for lang in langs:
|
|
||||||
if lang.code != action.base_language_id.code:
|
|
||||||
target_langs.append(lang.code)
|
|
||||||
|
|
||||||
error_count = 0
|
|
||||||
translations_to_create = []
|
|
||||||
model_name = action.model_id.model
|
|
||||||
base_lang_code = action.base_language_id.code
|
|
||||||
field_names = action.field_ids.mapped('name')
|
|
||||||
|
|
||||||
for active_id in active_ids:
|
|
||||||
record = self.env[model_name].browse(active_id)
|
|
||||||
terms = {}
|
|
||||||
terms_meta = {}
|
|
||||||
if record.exists():
|
|
||||||
terms, terms_meta = self._extract_record_terms(
|
|
||||||
record, field_names, base_lang_code, target_langs,
|
|
||||||
overwrite=action.overwrite_existing,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Skip records with nothing to translate (no content, or every
|
|
||||||
# term already translated in every target language)
|
|
||||||
if not terms:
|
|
||||||
_logger.info(
|
|
||||||
"Skipping record %d (model=%s, lang=%s): no terms left to translate.",
|
|
||||||
active_id, model_name, base_lang_code,
|
|
||||||
)
|
|
||||||
pending = action.pending_record_ids if action.pending_record_ids else []
|
|
||||||
skipped = action.skipped_record_ids if action.skipped_record_ids else []
|
|
||||||
if active_id in pending:
|
|
||||||
pending.remove(active_id)
|
|
||||||
if active_id not in skipped:
|
|
||||||
skipped.append(active_id)
|
|
||||||
action.write({
|
|
||||||
'pending_record_ids': pending,
|
|
||||||
'skipped_record_ids': skipped,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
task_id = action._request_terms_translation(
|
|
||||||
terms, terms_meta, target_langs, base_lang_code, active_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not task_id:
|
|
||||||
error_count += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
translations_to_create.append({
|
|
||||||
'action_id': action.id,
|
|
||||||
'cost': 0,
|
|
||||||
'record_id': active_id,
|
|
||||||
'status': 'pending',
|
|
||||||
'translations': {},
|
|
||||||
'initial_values': {},
|
|
||||||
'task_id': task_id
|
|
||||||
})
|
|
||||||
|
|
||||||
pending = action.pending_record_ids if action.pending_record_ids else []
|
|
||||||
processing = action.processing_record_ids if action.processing_record_ids else []
|
|
||||||
pending.remove(active_id)
|
|
||||||
if active_id not in processing:
|
|
||||||
processing.append(active_id)
|
|
||||||
action.write({
|
|
||||||
'pending_record_ids': pending,
|
|
||||||
'processing_record_ids': processing,
|
|
||||||
})
|
|
||||||
|
|
||||||
if translations_to_create:
|
|
||||||
self.env["otk.translation"].create(translations_to_create)
|
|
||||||
|
|
||||||
return error_count != len(active_ids)
|
|
||||||
|
|
||||||
def retry_error_translation(self):
|
|
||||||
for record in self:
|
|
||||||
pending = record.pending_record_ids if record.pending_record_ids else []
|
|
||||||
error = record.error_record_ids if record.error_record_ids else []
|
|
||||||
pending.extend(error)
|
|
||||||
self.write({
|
|
||||||
'pending_record_ids': pending,
|
|
||||||
'error_record_ids': [],
|
|
||||||
})
|
|
||||||
|
|
||||||
@api.depends('pending_record_ids', 'done_record_ids')
|
|
||||||
def _compute_progress(self):
|
|
||||||
for record in self:
|
|
||||||
if not record.done_record_ids:
|
|
||||||
record.progress = 0.0
|
|
||||||
elif not record.pending_record_ids and not record.processing_record_ids:
|
|
||||||
record.progress = 100.0
|
|
||||||
else:
|
|
||||||
record.progress = len(record.done_record_ids) / record.translation_count * 100.0
|
|
||||||
|
|
||||||
@api.depends('skipped_record_ids')
|
|
||||||
def _compute_skipped_count(self):
|
|
||||||
for record in self:
|
|
||||||
record.skipped_count = len(record.skipped_record_ids) if record.skipped_record_ids else 0
|
|
||||||
|
|
||||||
@api.depends('processing_record_ids')
|
|
||||||
def _compute_processing_count(self):
|
|
||||||
for record in self:
|
|
||||||
record.processing_count = len(record.processing_record_ids) if record.processing_record_ids else 0
|
|
||||||
|
|
||||||
@api.depends('translation_ids.last_server_check')
|
|
||||||
def _compute_last_server_check(self):
|
|
||||||
for record in self:
|
|
||||||
checks = record.translation_ids.mapped('last_server_check')
|
|
||||||
record.last_server_check = max(filter(None, checks), default=False)
|
|
||||||
|
|
||||||
@api.depends('progress', 'error_record_ids')
|
|
||||||
def _compute_status(self):
|
|
||||||
for record in self:
|
|
||||||
if record.error_record_ids and len(record.error_record_ids) > 0 and (
|
|
||||||
not record.pending_record_ids or len(record.pending_record_ids) == 0) and (
|
|
||||||
not record.processing_record_ids or len(record.processing_record_ids) == 0):
|
|
||||||
record.status = 'error'
|
|
||||||
else:
|
|
||||||
record.status = 'done' if record.progress >= 100 else 'pending' if record.pending_record_ids and len(
|
|
||||||
record.pending_record_ids) > 0 else 'processing'
|
|
||||||
|
|
||||||
@api.depends('model_id')
|
|
||||||
def _compute_display_name(self):
|
|
||||||
for record in self:
|
|
||||||
record.display_name = "Action #" + str(record.id) + " - " + record.model_id.name
|
|
||||||
|
|
||||||
|
|
||||||
class Translation(models.Model):
|
|
||||||
_name = 'otk.translation'
|
|
||||||
_description = 'Translation object with the details of the translation, the cost...'
|
|
||||||
_order = 'id desc'
|
|
||||||
|
|
||||||
action_id = fields.Many2one('otk.translation.action', string="Linked action", ondelete='cascade', readonly=True)
|
|
||||||
record_id = fields.Integer(string='Record id', readonly=True)
|
|
||||||
model_id = fields.Many2one('ir.model', string='Model', readonly=True, related='action_id.model_id')
|
|
||||||
|
|
||||||
cost = fields.Float(string="Token used", readonly=True)
|
|
||||||
status = fields.Selection(
|
|
||||||
[
|
|
||||||
('error', "Error"),
|
|
||||||
('translation_error', "Translation Error"),
|
|
||||||
('api_key_error', "Api Key Error"),
|
|
||||||
('success', "Success"),
|
|
||||||
('revert', "Revert"),
|
|
||||||
('pending', "Translating on server"),
|
|
||||||
],
|
|
||||||
string="Status", readonly=True
|
|
||||||
)
|
|
||||||
last_server_check = fields.Datetime(
|
|
||||||
string="Last server check",
|
|
||||||
readonly=True,
|
|
||||||
help="Last time the scheduled action polled the translation server "
|
|
||||||
"for this task's result. A recent timestamp with a 'Translating "
|
|
||||||
"on server' status means the translation is still running "
|
|
||||||
"server-side — large contents can take several minutes.",
|
|
||||||
)
|
|
||||||
translations = fields.Json(string='Translations', readonly=True)
|
|
||||||
initial_values = fields.Json(string='Initial value', readonly=True)
|
|
||||||
|
|
||||||
task_id = fields.Integer(string="Task ID", readonly=True)
|
|
||||||
|
|
||||||
def revert_translation(self):
|
|
||||||
for record in self:
|
|
||||||
target = self.env[record.model_id.model].browse(record.record_id)
|
|
||||||
if not target.exists():
|
|
||||||
record.status = 'revert'
|
|
||||||
continue
|
|
||||||
|
|
||||||
for field_name, by_lang in (record.initial_values or {}).items():
|
|
||||||
field = target._fields.get(field_name)
|
|
||||||
if field is None or not field.translate or not by_lang:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if callable(field.translate) and any(
|
|
||||||
not isinstance(value, dict) for value in by_lang.values()
|
|
||||||
):
|
|
||||||
# Records translated before the terms-mode upgrade stored a
|
|
||||||
# whole-jsonb snapshot per language; only a raw column
|
|
||||||
# restore can reapply those faithfully.
|
|
||||||
record._revert_legacy_field(field_name, by_lang)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Term-level revert: {lang: {translated_term: original_term}}
|
|
||||||
# for term-translated fields, {lang: original_value} for
|
|
||||||
# model-translated fields — both are exactly the payload
|
|
||||||
# update_field_translations expects.
|
|
||||||
target.update_field_translations(field_name, by_lang)
|
|
||||||
|
|
||||||
record.status = 'revert'
|
|
||||||
|
|
||||||
def _revert_legacy_field(self, field_name, lang_values):
|
|
||||||
"""Restore a pre-terms-mode whole-jsonb snapshot of one field."""
|
|
||||||
self.ensure_one()
|
|
||||||
table = self.env[self.model_id.model]._table
|
|
||||||
query = f"""
|
|
||||||
SELECT {field_name}
|
|
||||||
FROM {table}
|
|
||||||
WHERE id = %s
|
|
||||||
"""
|
|
||||||
self.env.cr.execute(query, (self.record_id,))
|
|
||||||
result = self.env.cr.fetchone()
|
|
||||||
translate = _sanitize_null_bytes(result[0]) if result and result[0] else {}
|
|
||||||
|
|
||||||
for lang, initial_value in lang_values.items():
|
|
||||||
translate[lang] = initial_value or False
|
|
||||||
|
|
||||||
update_query = f"""
|
|
||||||
UPDATE {table}
|
|
||||||
SET {field_name} = %s
|
|
||||||
WHERE id = %s
|
|
||||||
"""
|
|
||||||
self.env.cr.execute(update_query, (json.dumps(_sanitize_null_bytes(translate)), self.record_id))
|
|
||||||
self.env[self.model_id.model].browse(self.record_id).invalidate_recordset([field_name])
|
|
||||||
|
|
||||||
def open_record(self):
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.act_window',
|
|
||||||
'name': 'Linked record',
|
|
||||||
'res_model': self.model_id.model,
|
|
||||||
'res_id': self.record_id,
|
|
||||||
'view_mode': 'form',
|
|
||||||
'target': 'current', # or 'new' to open in a popup
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
from odoo import models, fields, api, _
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
|
||||||
class TranslationConfig(models.Model):
|
|
||||||
_name = 'otk.translation.config'
|
|
||||||
_description = 'Translation Configuration'
|
|
||||||
_rec_name = 'model_id'
|
|
||||||
|
|
||||||
model_id = fields.Many2one(
|
|
||||||
'ir.model',
|
|
||||||
string="Model",
|
|
||||||
required=True,
|
|
||||||
ondelete='cascade',
|
|
||||||
domain="[('transient', '=', False)]",
|
|
||||||
)
|
|
||||||
active = fields.Boolean(default=True)
|
|
||||||
has_translatable_fields = fields.Boolean(
|
|
||||||
compute='_compute_has_translatable_fields',
|
|
||||||
store=True,
|
|
||||||
)
|
|
||||||
translatable_field_ids = fields.Many2many(
|
|
||||||
'ir.model.fields',
|
|
||||||
string="Translatable Fields",
|
|
||||||
compute='_compute_has_translatable_fields',
|
|
||||||
store=True,
|
|
||||||
)
|
|
||||||
server_action_id = fields.Many2one(
|
|
||||||
'ir.actions.server',
|
|
||||||
string="Server Action",
|
|
||||||
readonly=True,
|
|
||||||
ondelete='set null',
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.constrains('model_id')
|
|
||||||
def _check_unique_model_id(self):
|
|
||||||
for record in self:
|
|
||||||
if record.model_id:
|
|
||||||
if self.search([('model_id', '=', record.model_id.id), ('id', '!=', record.id)]):
|
|
||||||
raise ValidationError(_("A configuration already exists for this model."))
|
|
||||||
|
|
||||||
@api.depends('model_id')
|
|
||||||
def _compute_has_translatable_fields(self):
|
|
||||||
for record in self:
|
|
||||||
if record.model_id:
|
|
||||||
translatable_fields = self.env['ir.model.fields'].search([
|
|
||||||
('model_id', '=', record.model_id.id),
|
|
||||||
('translate', 'in', ['standard', 'html_translate', 'xml_translate']),
|
|
||||||
('related', '=', False),
|
|
||||||
('store', '=', True),
|
|
||||||
])
|
|
||||||
record.translatable_field_ids = translatable_fields
|
|
||||||
record.has_translatable_fields = bool(translatable_fields)
|
|
||||||
else:
|
|
||||||
record.translatable_field_ids = False
|
|
||||||
record.has_translatable_fields = False
|
|
||||||
|
|
||||||
@api.model_create_multi
|
|
||||||
def create(self, vals_list):
|
|
||||||
records = super().create(vals_list)
|
|
||||||
records._create_server_actions()
|
|
||||||
return records
|
|
||||||
|
|
||||||
def write(self, vals):
|
|
||||||
model_changed = 'model_id' in vals
|
|
||||||
result = super().write(vals)
|
|
||||||
if 'active' in vals or model_changed:
|
|
||||||
self._update_server_actions(model_changed=model_changed)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def unlink(self):
|
|
||||||
self.server_action_id.unlink()
|
|
||||||
return super().unlink()
|
|
||||||
|
|
||||||
def _create_server_actions(self):
|
|
||||||
"""Create server actions for each configuration."""
|
|
||||||
for record in self:
|
|
||||||
if not record.server_action_id and record.model_id:
|
|
||||||
record._create_single_server_action()
|
|
||||||
|
|
||||||
def _create_single_server_action(self):
|
|
||||||
"""Create a single server action for this configuration."""
|
|
||||||
self.ensure_one()
|
|
||||||
if not self.model_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
action_name = _('Bulk translate')
|
|
||||||
server_action = self.env['ir.actions.server'].sudo().create({
|
|
||||||
'name': action_name,
|
|
||||||
'model_id': self.model_id.id,
|
|
||||||
'binding_model_id': self.model_id.id,
|
|
||||||
'binding_view_types': 'list',
|
|
||||||
'state': 'code',
|
|
||||||
'code': "action = env['otk.translation.config'].action_bulk_translate_generic()",
|
|
||||||
})
|
|
||||||
|
|
||||||
# Create external ID for the server action
|
|
||||||
action_xml_id = 'bulk_translate_config_action_for_' + self.model_id.model.replace('.', '_')
|
|
||||||
existing_data = self.env['ir.model.data'].sudo().search([
|
|
||||||
('module', '=', 'otoolkit_auto_translate_fields'),
|
|
||||||
('name', '=', action_xml_id),
|
|
||||||
])
|
|
||||||
if existing_data:
|
|
||||||
existing_data.unlink()
|
|
||||||
|
|
||||||
self.env['ir.model.data'].sudo().create({
|
|
||||||
'name': action_xml_id,
|
|
||||||
'model': 'ir.actions.server',
|
|
||||||
'module': 'otoolkit_auto_translate_fields',
|
|
||||||
'res_id': server_action.id,
|
|
||||||
'noupdate': True,
|
|
||||||
})
|
|
||||||
|
|
||||||
self.server_action_id = server_action
|
|
||||||
|
|
||||||
def _update_server_actions(self, model_changed=False):
|
|
||||||
"""Update server actions when configuration changes."""
|
|
||||||
for record in self:
|
|
||||||
# If model changed, delete old action and create new one
|
|
||||||
if model_changed and record.server_action_id:
|
|
||||||
record.server_action_id.unlink()
|
|
||||||
record.server_action_id = False
|
|
||||||
|
|
||||||
if record.active and not record.server_action_id:
|
|
||||||
record._create_single_server_action()
|
|
||||||
elif not record.active and record.server_action_id:
|
|
||||||
record.server_action_id.unlink()
|
|
||||||
record.server_action_id = False
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def action_bulk_translate_generic(self):
|
|
||||||
"""Generic action to open bulk translate wizard - called from server actions."""
|
|
||||||
active_model = self.env.context.get('active_model')
|
|
||||||
if not active_model:
|
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.act_window',
|
|
||||||
'name': _('Bulk translate'),
|
|
||||||
'res_model': 'otk.translation.bulk.translate',
|
|
||||||
'view_mode': 'form',
|
|
||||||
'view_id': self.env.ref('otoolkit_auto_translate_fields.view_bulk_translate_wizard_form').id,
|
|
||||||
'target': 'new',
|
|
||||||
}
|
|
||||||
|
|
||||||
def action_enable_all_translatable_models(self):
|
|
||||||
"""Enable bulk translation for all models that have translatable fields."""
|
|
||||||
# Find all models with translatable fields
|
|
||||||
translatable_models = self.env['ir.model.fields'].search([
|
|
||||||
('translate', 'in', ['standard', 'html_translate', 'xml_translate']),
|
|
||||||
('related', '=', False),
|
|
||||||
('store', '=', True),
|
|
||||||
]).mapped('model_id')
|
|
||||||
|
|
||||||
# Filter out transient models and already configured models
|
|
||||||
existing_model_ids = self.search([]).mapped('model_id.id')
|
|
||||||
models_to_add = translatable_models.filtered(
|
|
||||||
lambda m: m.id not in existing_model_ids and not m.transient
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create configurations
|
|
||||||
vals_list = [{'model_id': model.id} for model in models_to_add]
|
|
||||||
created = self.create(vals_list)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.client',
|
|
||||||
'tag': 'display_notification',
|
|
||||||
'params': {
|
|
||||||
'title': _('Bulk Translation Enabled'),
|
|
||||||
'message': _('%d models have been configured for bulk translation.') % len(created),
|
|
||||||
'type': 'success',
|
|
||||||
'sticky': False,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def action_view_translatable_fields(self):
|
|
||||||
"""View the translatable fields for this model."""
|
|
||||||
self.ensure_one()
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.act_window',
|
|
||||||
'name': _('Translatable Fields'),
|
|
||||||
'res_model': 'ir.model.fields',
|
|
||||||
'view_mode': 'list,form',
|
|
||||||
'domain': [('id', 'in', self.translatable_field_ids.ids)],
|
|
||||||
'target': 'current',
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
import requests
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from odoo import models, api, _
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class TranslationDialog(models.AbstractModel):
|
|
||||||
_name = "translation.dialog"
|
|
||||||
_description = "Translation Dialog"
|
|
||||||
|
|
||||||
def _get_api_config(self):
|
|
||||||
"""Get API configuration parameters."""
|
|
||||||
api_key = self.env['ir.config_parameter'].sudo().get_param('otoolkit_api_key')
|
|
||||||
api_endpoint = self.env['ir.config_parameter'].sudo().get_param('otoolkit.api.endpoint')
|
|
||||||
return api_key, api_endpoint
|
|
||||||
|
|
||||||
def _make_api_request(self, url, headers, payload, timeout=180):
|
|
||||||
"""Make an API request with proper error handling and logging.
|
|
||||||
|
|
||||||
Default timeout is 180 seconds (3 minutes) to allow for OpenAI processing
|
|
||||||
when translating to many languages.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = requests.request(
|
|
||||||
"POST",
|
|
||||||
url,
|
|
||||||
headers=headers,
|
|
||||||
data=payload,
|
|
||||||
timeout=timeout
|
|
||||||
)
|
|
||||||
return response, None
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
_logger.warning("OToolKit API request timed out: %s", url)
|
|
||||||
return None, _("The translation service is taking too long to respond. Please try again.")
|
|
||||||
except requests.exceptions.ConnectionError as e:
|
|
||||||
_logger.error("OToolKit API connection error: %s - %s", url, str(e))
|
|
||||||
return None, _("Unable to connect to the translation service. Please check your internet connection.")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
_logger.error("OToolKit API request failed: %s - %s", url, str(e))
|
|
||||||
return None, _("An error occurred while contacting the translation service: %s") % str(e)
|
|
||||||
|
|
||||||
def _handle_api_response(self, response):
|
|
||||||
"""Handle API response and return appropriate result."""
|
|
||||||
if response.status_code == 200:
|
|
||||||
return [True, response.json(), ""]
|
|
||||||
|
|
||||||
if response.status_code == 401:
|
|
||||||
_logger.warning("OToolKit API authentication failed (401)")
|
|
||||||
return [False, _("Your API key is invalid. This may be due to an input error, deletion or deactivation of the key. Please check your Otoolkit settings."), "settings"]
|
|
||||||
|
|
||||||
if response.status_code == 400:
|
|
||||||
try:
|
|
||||||
error = response.json()
|
|
||||||
error_type = error.get("type", "unknown")
|
|
||||||
if error_type == "empty_text":
|
|
||||||
return [False, _("The default language text cannot be empty."), ""]
|
|
||||||
if error_type == "insufficient_funds":
|
|
||||||
return [False, _("You do not have enough credits to translate. Please add credits to your balance to use this function."), "credits"]
|
|
||||||
if error_type == "invalid_object":
|
|
||||||
return [False, _("No valid text fields found to translate."), ""]
|
|
||||||
# Log unhandled 400 error types
|
|
||||||
_logger.warning("OToolKit API returned unhandled 400 error: %s", error)
|
|
||||||
return [False, _("Translation error: %s") % error.get("error", "Unknown error"), ""]
|
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
|
||||||
_logger.error("Failed to parse OToolKit API 400 response: %s", str(e))
|
|
||||||
return [False, _("The translation service returned an invalid response."), ""]
|
|
||||||
|
|
||||||
if response.status_code == 429:
|
|
||||||
_logger.warning("OToolKit API rate limited (429)")
|
|
||||||
return [False, _("Too many translation requests. Please wait a moment and try again."), ""]
|
|
||||||
|
|
||||||
if response.status_code >= 500:
|
|
||||||
_logger.error("OToolKit API server error (%d): %s", response.status_code, response.text[:500])
|
|
||||||
return [False, _("The translation service is temporarily unavailable (Error %d). Please try again later.") % response.status_code, ""]
|
|
||||||
|
|
||||||
# Log any other unexpected status codes
|
|
||||||
_logger.error("OToolKit API unexpected status code (%d): %s", response.status_code, response.text[:500])
|
|
||||||
return [False, _("Unexpected response from translation service (Error %d). Please try again.") % response.status_code, ""]
|
|
||||||
|
|
||||||
def _get_effective_value(self, term, updated_terms):
|
|
||||||
"""Get the effective value of a term, considering user edits in the dialog.
|
|
||||||
|
|
||||||
If the user edited the term (present in updated_terms), always use that
|
|
||||||
value — even if empty (the user explicitly cleared it).
|
|
||||||
"""
|
|
||||||
str_id = str(term["id"])
|
|
||||||
if str_id in updated_terms:
|
|
||||||
return (updated_terms[str_id] or "").strip()
|
|
||||||
return (term.get("value") or "").strip()
|
|
||||||
|
|
||||||
def _detect_source_and_filter(self, terms, updated_terms, source_lang_hint=None, target_lang_hint=None):
|
|
||||||
"""Detect the best source language and filter terms to only translate empty ones.
|
|
||||||
|
|
||||||
Returns (base_language, filtered_terms) or (None, []) if nothing to translate.
|
|
||||||
|
|
||||||
When the caller knows the source/target (single-term flow), it must pass
|
|
||||||
the hints — relying on auto-detection over `filled_langs` is unsafe
|
|
||||||
because `next(iter(set))` is non-deterministic and may pick the user's
|
|
||||||
target language as the source.
|
|
||||||
|
|
||||||
Auto-detection priority (used only when hints are absent):
|
|
||||||
1. User's language (if it has content)
|
|
||||||
2. en_US (Odoo's typical base language)
|
|
||||||
3. Any language other than the target hint with content
|
|
||||||
4. First language with content (last resort)
|
|
||||||
|
|
||||||
Terms whose value is identical to the source language's value are treated
|
|
||||||
as untranslated copies (Odoo duplicates the source value to all languages
|
|
||||||
by default) and will be sent for translation.
|
|
||||||
|
|
||||||
When all values are empty but source text exists (e.g. HTML fields where
|
|
||||||
translations haven't been set yet), the source text is used as the base
|
|
||||||
content for translation.
|
|
||||||
"""
|
|
||||||
user_lang = self.env.user.lang
|
|
||||||
|
|
||||||
# Build a map of lang -> effective value
|
|
||||||
lang_values = {}
|
|
||||||
for term in terms:
|
|
||||||
lang_values[term["lang"]] = self._get_effective_value(term, updated_terms)
|
|
||||||
|
|
||||||
# First pass: identify which languages have any content
|
|
||||||
filled_langs = {lang for lang, val in lang_values.items() if val}
|
|
||||||
source_from_source_field = False
|
|
||||||
|
|
||||||
if not filled_langs:
|
|
||||||
# All values are empty — fall back to source text if available
|
|
||||||
# (e.g. HTML fields where translations haven't been set yet)
|
|
||||||
for term in terms:
|
|
||||||
source = (term.get("source") or "").strip()
|
|
||||||
if source:
|
|
||||||
lang_values[term["lang"]] = source
|
|
||||||
filled_langs = {lang for lang, val in lang_values.items() if val}
|
|
||||||
if not filled_langs:
|
|
||||||
return None, []
|
|
||||||
source_from_source_field = True
|
|
||||||
|
|
||||||
# Determine source language. Explicit hints win — the JS knows which
|
|
||||||
# row the user clicked, so trust it over heuristic guessing.
|
|
||||||
if source_lang_hint and source_lang_hint in filled_langs:
|
|
||||||
source_lang = source_lang_hint
|
|
||||||
elif user_lang in filled_langs and user_lang != target_lang_hint:
|
|
||||||
source_lang = user_lang
|
|
||||||
elif "en_US" in filled_langs and "en_US" != target_lang_hint:
|
|
||||||
source_lang = "en_US"
|
|
||||||
else:
|
|
||||||
# Prefer any filled lang that is NOT the target — never pick the
|
|
||||||
# user's target as source.
|
|
||||||
non_target = [l for l in filled_langs if l != target_lang_hint]
|
|
||||||
source_lang = non_target[0] if non_target else next(iter(filled_langs))
|
|
||||||
|
|
||||||
source_value = lang_values[source_lang]
|
|
||||||
|
|
||||||
# Second pass: languages that are empty OR identical to source are
|
|
||||||
# considered untranslated (Odoo copies the source value by default)
|
|
||||||
empty_langs = set()
|
|
||||||
for lang, val in lang_values.items():
|
|
||||||
if lang == source_lang:
|
|
||||||
continue
|
|
||||||
if not val or val == source_value:
|
|
||||||
empty_langs.add(lang)
|
|
||||||
|
|
||||||
# If a target hint was provided, restrict the translation to that
|
|
||||||
# language only — a single-term click should never translate every
|
|
||||||
# untranslated peer in the dialog as a side effect.
|
|
||||||
if target_lang_hint:
|
|
||||||
empty_langs = {target_lang_hint} if target_lang_hint != source_lang else set()
|
|
||||||
|
|
||||||
if not empty_langs:
|
|
||||||
return None, []
|
|
||||||
|
|
||||||
keep_langs = {source_lang} | empty_langs
|
|
||||||
filtered_terms = [t for t in terms if t["lang"] in keep_langs]
|
|
||||||
|
|
||||||
if source_from_source_field:
|
|
||||||
# Populate the base language terms' value with source text so the
|
|
||||||
# API receives the actual content to translate from
|
|
||||||
filtered_terms = [
|
|
||||||
{**t, "value": (t.get("source") or "")} if t["lang"] == source_lang else t
|
|
||||||
for t in filtered_terms
|
|
||||||
]
|
|
||||||
|
|
||||||
return source_lang, filtered_terms
|
|
||||||
|
|
||||||
def otoolkit_api_translation(self, terms, updated_terms, source_lang_hint=None, target_lang_hint=None):
|
|
||||||
api_key, api_endpoint = self._get_api_config()
|
|
||||||
|
|
||||||
if not api_key:
|
|
||||||
return [False, _("Your API key is invalid. This may be due to an input error, deletion or deactivation of the key. Please check your Otoolkit settings."), "settings"]
|
|
||||||
|
|
||||||
if not api_endpoint:
|
|
||||||
_logger.error("OToolKit API endpoint not configured")
|
|
||||||
return [False, _("The translation service is not configured. Please contact your administrator."), ""]
|
|
||||||
|
|
||||||
base_language, filtered_terms = self._detect_source_and_filter(
|
|
||||||
terms, updated_terms,
|
|
||||||
source_lang_hint=source_lang_hint,
|
|
||||||
target_lang_hint=target_lang_hint,
|
|
||||||
)
|
|
||||||
if not base_language:
|
|
||||||
return [True, [], ""]
|
|
||||||
|
|
||||||
payload = json.dumps({
|
|
||||||
"terms": filtered_terms,
|
|
||||||
"updated_terms": updated_terms,
|
|
||||||
"base_language": base_language,
|
|
||||||
"odoo_user_id": self.env.user.id
|
|
||||||
})
|
|
||||||
headers = {
|
|
||||||
'Odoo-Api-Key': api_key,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
url = f"{api_endpoint}/api/auto-translate-fields/translate/"
|
|
||||||
|
|
||||||
response, error_message = self._make_api_request(url, headers, payload)
|
|
||||||
|
|
||||||
if error_message:
|
|
||||||
return [False, error_message, ""]
|
|
||||||
|
|
||||||
return self._handle_api_response(response)
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def translate_text(self, terms, updated_terms, source_lang_hint=None, target_lang_hint=None):
|
|
||||||
return self.otoolkit_api_translation(
|
|
||||||
terms, updated_terms,
|
|
||||||
source_lang_hint=source_lang_hint,
|
|
||||||
target_lang_hint=target_lang_hint,
|
|
||||||
)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from odoo import models, fields, api, _
|
|
||||||
|
|
||||||
|
|
||||||
class TranslationMixin(models.AbstractModel):
|
|
||||||
_name = 'otk.translation.mixin'
|
|
||||||
_description = 'Translation Mixin'
|
|
||||||
|
|
||||||
last_translation_date = fields.Datetime('Last Translation Date')
|
|
||||||
|
|
||||||
def action_bulk_translate(self):
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.act_window',
|
|
||||||
'name': _('Bulk translate'),
|
|
||||||
'res_model': 'otk.translation.bulk.translate',
|
|
||||||
'view_mode': 'form',
|
|
||||||
'view_id': self.env.ref('otoolkit_auto_translate_fields.view_bulk_translate_wizard_form').id,
|
|
||||||
'target': 'new',
|
|
||||||
}
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _get_view(self, view_id=None, view_type='search', **options):
|
|
||||||
arch, view = super()._get_view(view_id, view_type, **options)
|
|
||||||
if view_type == 'list':
|
|
||||||
# Here we check if the action already exists
|
|
||||||
model_name = self._name
|
|
||||||
action_xml_id = 'bulk_translate_action_for_' + model_name.replace('.', '_')
|
|
||||||
|
|
||||||
server_action = self.env.ref(f'otoolkit_auto_translate_fields.{action_xml_id}', raise_if_not_found=False)
|
|
||||||
|
|
||||||
if not server_action:
|
|
||||||
# Create the server action dynamically
|
|
||||||
model = self.env['ir.model']._get(model_name)
|
|
||||||
action_name = _('Bulk translate')
|
|
||||||
server_action = self.env['ir.actions.server'].sudo().create({
|
|
||||||
'name': action_name,
|
|
||||||
'model_id': model.id,
|
|
||||||
'binding_model_id': model.id,
|
|
||||||
'binding_view_types': 'list',
|
|
||||||
'state': 'code',
|
|
||||||
'code': "action = env['%s'].action_bulk_translate()" % model_name,
|
|
||||||
})
|
|
||||||
self.env['ir.model.data'].sudo().create({
|
|
||||||
'name': action_xml_id,
|
|
||||||
'model': 'ir.actions.server',
|
|
||||||
'module': "otoolkit_auto_translate_fields",
|
|
||||||
'res_id': server_action.id,
|
|
||||||
'noupdate': True,
|
|
||||||
})
|
|
||||||
return arch, view
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|
||||||
access_bulk_translate,access_bulk_translate,model_otk_translation_bulk_translate,base.group_user,1,1,1,1
|
|
||||||
access_translation_action,access_translation_action,model_otk_translation_action,base.group_user,1,1,1,1
|
|
||||||
access_translation,access_translation,model_otk_translation,base.group_user,1,1,1,1
|
|
||||||
access_translation_config,access_translation_config,model_otk_translation_config,base.group_system,1,1,1,1
|
|
||||||
|
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 536 KiB |
|
Before Width: | Height: | Size: 961 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,71 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="row position-relative pt-lg-3 px-lg-5 pb-lg-3 p-md-4 p-3 mx-auto d-flex align-items-center" style="color:#333333; font-weight:500; font-size:16px; width:95%">
|
|
||||||
<section class="oe_container">
|
|
||||||
<div class="col-md-12 text-center" style="margin-top: 10px;">
|
|
||||||
<a target="_blank" href="https://otoolkit.app">
|
|
||||||
<h2>Otoolkit</h2>
|
|
||||||
</a>
|
|
||||||
<p>
|
|
||||||
<a href="https://otoolkit.app" target="_blank">Website: otoolkit.app</a><br>
|
|
||||||
Email: <a href="mailto:contact@otoolkit.app">contact@otoolkit.app</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 text-center" style="margin-top: 30px;">
|
|
||||||
<a target="_blank" href="https://otoolkit.app/apps/auto-translate-fields">
|
|
||||||
<h2>Auto Translate Fields</h2>
|
|
||||||
</a>
|
|
||||||
<p>The <strong>Auto Translate Fields</strong> module enhances the translation process in Odoo by adding a convenient "Translate" button to the translation popup of any translatable field. When clicked, it automatically translates the field content into all configured languages in the system, using the current user's language as the source. This feature simplifies and accelerates the workflow for multilingual environments, ensuring that translations are consistent and up-to-date across the platform. Ideal for businesses that operate in multiple languages, this module improves efficiency by reducing manual translation efforts.</p>
|
|
||||||
<p>Instructions on how to use this module can be found at <a href="https://otoolkit.app/apps/auto-translate-fields">https://otoolkit.app/apps/auto-translate-fields</a></p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 text-center" style="margin-top: 30px;text-align: left !important;">
|
|
||||||
<h2 style="text-align: center !important">
|
|
||||||
<b>How it works</b>
|
|
||||||
</h2>
|
|
||||||
<div class="row text-dark">
|
|
||||||
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
|
|
||||||
<img src="assets/translate-field.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="Classic text input with auto translate in odoo">
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<b>Auto Translate</b>
|
|
||||||
</h3>
|
|
||||||
<p class="mt-4">When you translate your fields, you'll find a new button called «Auto Translate». Click on it to translate your text into all the languages of your instance. The user's base language is used as the starting language.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="d-lg-none">
|
|
||||||
|
|
||||||
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<b>Also works with HTML fields</b>
|
|
||||||
</h3>
|
|
||||||
<p class="mt-4">Automatic translation also works with HTML fields. This saves time by avoiding line-by-line translation.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
|
|
||||||
<img src="assets/markdown-input.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="Markdown input with auto translate in odoo">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 text-center mt-5 p-4" style="background-color: #f8f9fa; border-radius: 15px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
|
||||||
<h3 style="color: rgb(124, 58, 237);">Have any questions?</h3>
|
|
||||||
<p class="mt-3" style="color: #333;">If you have any questions or need more information, feel free to visit our website or contact us directly.</p>
|
|
||||||
<p>
|
|
||||||
<span href="https://otoolkit.app" class="btn otoolkit-open-site" style="background-color: rgb(124, 58, 237); color: #fff; border-radius: 5px; padding: 10px 20px; text-decoration: none;">Visit Our Website</span>
|
|
||||||
<a href="mailto:contact@otoolkit.app" class="btn" style="background-color: #fff; color: rgb(124, 58, 237); border: 2px solid rgb(124, 58, 237); border-radius: 5px; padding: 10px 20px; text-decoration: none;">Email Us</a>
|
|
||||||
</p>
|
|
||||||
<a href="https://otoolkit.app">https://otoolkit.app</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/** @odoo-module **/
|
|
||||||
|
|
||||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
|
||||||
import { Component, xml } from "@odoo/owl";
|
|
||||||
import { registry } from "@web/core/registry";
|
|
||||||
|
|
||||||
export class JsonPrettyField extends Component {
|
|
||||||
static template = xml`
|
|
||||||
<p style="white-space: pre-wrap; background: #f5f5f5; padding: 12px; border-radius: 6px;">
|
|
||||||
<t t-esc="this.prettyJson"/>
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
static props = standardFieldProps;
|
|
||||||
|
|
||||||
get prettyJson() {
|
|
||||||
try {
|
|
||||||
const value = this.props.record.data[this.props.name];
|
|
||||||
return JSON.stringify(value ?? {}, null, 4);
|
|
||||||
} catch (e) {
|
|
||||||
return "Invalid JSON";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.category("fields").add("json_pretty", { component: JsonPrettyField });
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
/** @odoo-module **/
|
|
||||||
|
|
||||||
import { TranslationDialog } from "@web/views/fields/translation_dialog";
|
|
||||||
import { patch } from "@web/core/utils/patch";
|
|
||||||
import { useService } from "@web/core/utils/hooks";
|
|
||||||
import { _t } from "@web/core/l10n/translation";
|
|
||||||
import { useState } from "@odoo/owl";
|
|
||||||
|
|
||||||
patch(TranslationDialog.prototype, {
|
|
||||||
setup() {
|
|
||||||
super.setup(...arguments);
|
|
||||||
this.orm = useService('orm');
|
|
||||||
this.notification = useService("notification");
|
|
||||||
this.action = useService("action");
|
|
||||||
this.state = useState({ translating: false, translatingTermIds: {} });
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the best source term to translate from for a given target term.
|
|
||||||
* Picks a term in a different language with the same source text.
|
|
||||||
* Priority: any candidate with actual translated content > en_US > first
|
|
||||||
* candidate. en_US is deprioritized because Odoo populates en_US from the
|
|
||||||
* record's base value — which may itself be in another language (e.g. a
|
|
||||||
* record edited primarily in French) and would mislabel the source to GPT.
|
|
||||||
*/
|
|
||||||
_findSourceTerm(targetTerm) {
|
|
||||||
const candidates = this.terms.filter(
|
|
||||||
t => t.lang !== targetTerm.lang && t.source === targetTerm.source
|
|
||||||
);
|
|
||||||
if (candidates.length === 0) return null;
|
|
||||||
return candidates.find(t => (this.updatedTerms[t.id] ?? t.value ?? "").trim())
|
|
||||||
|| candidates.find(t => t.lang === "en_US")
|
|
||||||
|| candidates[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle translation error notifications.
|
|
||||||
* Returns true if an error was displayed (caller should stop).
|
|
||||||
*/
|
|
||||||
_handleTranslationNotification(result) {
|
|
||||||
if (!result[0]) {
|
|
||||||
if (result[2] === "credits") {
|
|
||||||
this.notification.add(result[1], {
|
|
||||||
type: "danger",
|
|
||||||
buttons: [{
|
|
||||||
onClick: () => window.open("https://osolutions.app/pricing"),
|
|
||||||
primary: false,
|
|
||||||
name: _t("Get tokens"),
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
} else if (result[2] === "settings") {
|
|
||||||
this.notification.add(result[1], {
|
|
||||||
type: "danger",
|
|
||||||
buttons: [{
|
|
||||||
onClick: () => this.action.doAction({
|
|
||||||
type: "ir.actions.act_window",
|
|
||||||
res_model: "res.config.settings",
|
|
||||||
views: [[false, "form"]],
|
|
||||||
target: "current",
|
|
||||||
context: { module: "otoolkit_auth" },
|
|
||||||
}),
|
|
||||||
primary: false,
|
|
||||||
name: _t("Open Settings"),
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.notification.add(result[1], { type: "danger" });
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply translated terms to the dialog state.
|
|
||||||
* Returns the count of terms updated.
|
|
||||||
*/
|
|
||||||
_applyTranslationResults(translatedTerms) {
|
|
||||||
let count = 0;
|
|
||||||
for (const item of translatedTerms) {
|
|
||||||
if (item.translation) {
|
|
||||||
this.updatedTerms[item.id] = item.translation;
|
|
||||||
const target = this.terms.find(t => t.id === item.id);
|
|
||||||
if (target) {
|
|
||||||
target.value = item.translation;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether any translation (bulk or single-term) is currently running.
|
|
||||||
*/
|
|
||||||
get anyTranslating() {
|
|
||||||
if (this.state.translating) return true;
|
|
||||||
for (const id in this.state.translatingTermIds) {
|
|
||||||
if (this.state.translatingTermIds[id]) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bulk auto-translate all empty terms.
|
|
||||||
*/
|
|
||||||
async autoTranslate() {
|
|
||||||
this.state.translating = true;
|
|
||||||
try {
|
|
||||||
const result = await this._translateText(this.terms, this.updatedTerms, null, null);
|
|
||||||
if (this._handleTranslationNotification(result)) return;
|
|
||||||
|
|
||||||
if (result[1].length === 0) {
|
|
||||||
return this.notification.add(
|
|
||||||
_t("All translations are already filled."), { type: "info" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = this._applyTranslationResults(result[1]);
|
|
||||||
this.render();
|
|
||||||
if (count > 0) {
|
|
||||||
this.notification.add(_t("Successfully translated"), { type: "success" });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.state.translating = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a single empty term.
|
|
||||||
*/
|
|
||||||
async translateSingleTerm(targetTerm) {
|
|
||||||
const sourceTerm = this._findSourceTerm(targetTerm);
|
|
||||||
if (!sourceTerm) {
|
|
||||||
return this.notification.add(
|
|
||||||
_t("No other language available to translate from."), { type: "warning" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.translatingTermIds[targetTerm.id] = true;
|
|
||||||
try {
|
|
||||||
const result = await this._translateText(
|
|
||||||
[sourceTerm, targetTerm], this.updatedTerms,
|
|
||||||
sourceTerm.lang, targetTerm.lang,
|
|
||||||
);
|
|
||||||
if (this._handleTranslationNotification(result)) return;
|
|
||||||
|
|
||||||
if (result[1].length === 0) {
|
|
||||||
return this.notification.add(
|
|
||||||
_t("No translation returned for this term."), { type: "warning" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply only the target term's translation. The API also returns
|
|
||||||
// an "echo" entry for the source-language term (its `translation`
|
|
||||||
// is the source text itself) — applying it would overwrite the
|
|
||||||
// source row's displayed value, which is wrong when the source
|
|
||||||
// peer is an empty row whose displayed `value` is empty (HTML
|
|
||||||
// showSource case).
|
|
||||||
const targetOnly = result[1].filter(item => item.id === targetTerm.id);
|
|
||||||
const count = this._applyTranslationResults(targetOnly);
|
|
||||||
this.render();
|
|
||||||
if (count > 0) {
|
|
||||||
this.notification.add(_t("Successfully translated"), { type: "success" });
|
|
||||||
} else {
|
|
||||||
this.notification.add(
|
|
||||||
_t("No translation returned for this term."), { type: "warning" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
delete this.state.translatingTermIds[targetTerm.id];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async _translateText(terms, updated_terms, source_lang_hint = null, target_lang_hint = null) {
|
|
||||||
return await this.orm.call(
|
|
||||||
"translation.dialog", "translate_text",
|
|
||||||
[terms, updated_terms, source_lang_hint, target_lang_hint],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async onSave() {
|
|
||||||
const translations = {};
|
|
||||||
|
|
||||||
this.terms.forEach((term) => {
|
|
||||||
const updatedTermValue = this.updatedTerms[term.id];
|
|
||||||
|
|
||||||
if (term.id in this.updatedTerms && updatedTermValue !== term.originalValue) {
|
|
||||||
if (this.props.showSource) {
|
|
||||||
if (!translations[term.lang]) {
|
|
||||||
translations[term.lang] = {};
|
|
||||||
}
|
|
||||||
const oldTermValue = term.originalValue ? term.originalValue : term.source;
|
|
||||||
translations[term.lang][oldTermValue] = updatedTermValue || term.source;
|
|
||||||
} else {
|
|
||||||
translations[term.lang] = updatedTermValue || false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.orm.call(this.props.resModel, "update_field_translations", [
|
|
||||||
[this.props.resId],
|
|
||||||
this.props.fieldName,
|
|
||||||
translations,
|
|
||||||
]);
|
|
||||||
|
|
||||||
await this.props.onSave();
|
|
||||||
this.props.close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<templates>
|
|
||||||
<t t-inherit="web.TranslationDialog" t-inherit-mode="extension">
|
|
||||||
<!-- Make translation column a flex container for inline button -->
|
|
||||||
<xpath expr="//div[hasclass('translation')]" position="attributes">
|
|
||||||
<attribute name="class" add="d-flex align-items-start gap-1" separator=" "/>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<!-- Make textarea fill remaining space -->
|
|
||||||
<xpath expr="//div[hasclass('translation')]//textarea" position="attributes">
|
|
||||||
<attribute name="class" add="flex-grow-1" separator=" "/>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<!-- Make input fill remaining space -->
|
|
||||||
<xpath expr="//div[hasclass('translation')]//input[@type='text']" position="attributes">
|
|
||||||
<attribute name="class" add="flex-grow-1" separator=" "/>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<!-- Single-term translate button (shown only for empty terms) -->
|
|
||||||
<xpath expr="//div[hasclass('translation')]" position="inside">
|
|
||||||
<button t-if="!term.value"
|
|
||||||
class="btn btn-sm btn-outline-primary px-2 o_translate_single_btn flex-shrink-0"
|
|
||||||
t-on-click="() => this.translateSingleTerm(term)"
|
|
||||||
t-att-disabled="anyTranslating"
|
|
||||||
t-att-title="'Translate to ' + term.langName">
|
|
||||||
<t t-if="state.translatingTermIds[term.id]">
|
|
||||||
<i class="fa fa-spinner fa-spin"/>
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<i class="fa fa-language"/>
|
|
||||||
</t>
|
|
||||||
</button>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<!-- Footer: Save, Discard, Auto Translate -->
|
|
||||||
<xpath expr="//t[@t-set-slot='footer']" position="replace">
|
|
||||||
<t t-set-slot="footer">
|
|
||||||
<button class="btn btn-primary" t-on-click="onSave">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" t-on-click="props.close">
|
|
||||||
Discard
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-secondary ml-3" t-on-click="autoTranslate" t-att-disabled="anyTranslating">
|
|
||||||
<t t-if="state.translating">
|
|
||||||
<i class="fa fa-spinner fa-spin me-1"/>Translating...
|
|
||||||
</t>
|
|
||||||
<t t-else="">Auto Translate</t>
|
|
||||||
</button>
|
|
||||||
</t>
|
|
||||||
</xpath>
|
|
||||||
</t>
|
|
||||||
</templates>
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<record id="view_otk_translation_config_list" model="ir.ui.view">
|
|
||||||
<field name="name">otk.translation.config.list</field>
|
|
||||||
<field name="model">otk.translation.config</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<list>
|
|
||||||
<field name="model_id"/>
|
|
||||||
<field name="has_translatable_fields"/>
|
|
||||||
<field name="translatable_field_ids" widget="many2many_tags"/>
|
|
||||||
<field name="active" widget="boolean_toggle"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_otk_translation_config_form" model="ir.ui.view">
|
|
||||||
<field name="name">otk.translation.config.form</field>
|
|
||||||
<field name="model">otk.translation.config</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<sheet>
|
|
||||||
<div class="oe_button_box" name="button_box">
|
|
||||||
<button name="action_view_translatable_fields"
|
|
||||||
type="object"
|
|
||||||
class="oe_stat_button"
|
|
||||||
icon="fa-list"
|
|
||||||
invisible="not has_translatable_fields">
|
|
||||||
<field name="has_translatable_fields" invisible="1"/>
|
|
||||||
<span>Translatable Fields</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
|
||||||
<group>
|
|
||||||
<group>
|
|
||||||
<field name="model_id" options="{'no_create': True}"/>
|
|
||||||
<field name="active" widget="boolean_toggle"/>
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<field name="server_action_id" readonly="1"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<group string="Translatable Fields" invisible="not has_translatable_fields">
|
|
||||||
<field name="translatable_field_ids" widget="many2many_tags" nolabel="1" colspan="2" readonly="1"/>
|
|
||||||
</group>
|
|
||||||
<div class="alert alert-warning" role="alert" invisible="has_translatable_fields">
|
|
||||||
<strong>Warning:</strong> This model has no translatable fields. Bulk translation will not be useful.
|
|
||||||
</div>
|
|
||||||
</sheet>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_otk_translation_config_search" model="ir.ui.view">
|
|
||||||
<field name="name">otk.translation.config.search</field>
|
|
||||||
<field name="model">otk.translation.config</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<search>
|
|
||||||
<field name="model_id"/>
|
|
||||||
<filter name="active" string="Active" domain="[('active', '=', True)]"/>
|
|
||||||
<filter name="inactive" string="Archived" domain="[('active', '=', False)]"/>
|
|
||||||
<filter name="has_fields" string="With Translatable Fields" domain="[('has_translatable_fields', '=', True)]"/>
|
|
||||||
<group>
|
|
||||||
<filter string="Active" name="group_active" context="{'group_by': 'active'}"/>
|
|
||||||
</group>
|
|
||||||
</search>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="action_otk_translation_config" model="ir.actions.act_window">
|
|
||||||
<field name="name">Translation Configuration</field>
|
|
||||||
<field name="res_model">otk.translation.config</field>
|
|
||||||
<field name="view_mode">list,form</field>
|
|
||||||
<field name="context">{'search_default_active': 1}</field>
|
|
||||||
<field name="help" type="html">
|
|
||||||
<p class="o_view_nocontent_smiling_face">
|
|
||||||
Configure models for bulk translation
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Add models here to enable the "Bulk translate" action on their list views.
|
|
||||||
No code changes required - just select a model and it will automatically
|
|
||||||
get the bulk translation feature.
|
|
||||||
</p>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- Wizard to enable all translatable models at once -->
|
|
||||||
<record id="action_enable_all_translatable_models" model="ir.actions.server">
|
|
||||||
<field name="name">Enable All Translatable Models</field>
|
|
||||||
<field name="model_id" ref="model_otk_translation_config"/>
|
|
||||||
<field name="state">code</field>
|
|
||||||
<field name="code">action = model.action_enable_all_translatable_models()</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<menuitem
|
|
||||||
id="otoolkit_translation_config_menu"
|
|
||||||
name="Translation Settings"
|
|
||||||
parent="otoolkit_menu"
|
|
||||||
action="action_otk_translation_config"
|
|
||||||
sequence="120"
|
|
||||||
/>
|
|
||||||
</odoo>
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<record id="view_otoolkit_translation_action_list" model="ir.ui.view">
|
|
||||||
<field name="name">otk.translation.action.list</field>
|
|
||||||
<field name="model">otk.translation.action</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<list>
|
|
||||||
<field name="id"/>
|
|
||||||
<field name="model_id" />
|
|
||||||
<field name="field_ids" widget="many2many_tags" />
|
|
||||||
<field name="translation_count" />
|
|
||||||
<field name="skipped_count" optional="show"/>
|
|
||||||
<field name="token_cost" />
|
|
||||||
<field name="status" />
|
|
||||||
<field name="progress" widget="progressbar"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_otoolkit_translation_action_form" model="ir.ui.view">
|
|
||||||
<field name="name">otk.translation.action.form</field>
|
|
||||||
<field name="model">otk.translation.action</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<header>
|
|
||||||
<button invisible="status!='error'" name="retry_error_translation" string="Retry translation" type="object" class="btn-primary" />
|
|
||||||
</header>
|
|
||||||
<sheet>
|
|
||||||
<!-- Skipped records alert -->
|
|
||||||
<div class="alert alert-warning" role="alert"
|
|
||||||
invisible="skipped_count == 0">
|
|
||||||
<strong>
|
|
||||||
<i class="fa fa-exclamation-triangle" title="Skipped records"/>
|
|
||||||
<field name="skipped_count" class="d-inline"/> records skipped
|
|
||||||
</strong>
|
|
||||||
— these records had no translatable content in the selected fields and were not sent to the API (no tokens consumed).
|
|
||||||
</div>
|
|
||||||
<!-- Server-side processing alert -->
|
|
||||||
<div class="alert alert-info" role="alert"
|
|
||||||
invisible="status != 'processing' or processing_count == 0">
|
|
||||||
<strong>
|
|
||||||
<i class="fa fa-spinner" title="Translating"/>
|
|
||||||
<field name="processing_count" class="d-inline"/> record(s) being translated on the O'Toolkit server.
|
|
||||||
</strong>
|
|
||||||
Large contents can take several minutes. Results are fetched automatically every few minutes —
|
|
||||||
last check: <field name="last_server_check" class="d-inline"/>
|
|
||||||
</div>
|
|
||||||
<group>
|
|
||||||
<group string="Configuration">
|
|
||||||
<field name="model_id" />
|
|
||||||
<field name="status" />
|
|
||||||
<field name="base_language_id" />
|
|
||||||
<field name="target_language_ids" widget="many2many_tags" />
|
|
||||||
<field name="field_ids" widget="many2many_tags" />
|
|
||||||
<field name="overwrite_existing" />
|
|
||||||
</group>
|
|
||||||
<group string="Progress">
|
|
||||||
<field name="translation_count" />
|
|
||||||
<field name="token_cost" />
|
|
||||||
<field name="progress" widget="progressbar" />
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<notebook>
|
|
||||||
<page string="Translations" name="translations">
|
|
||||||
<field name="translation_ids">
|
|
||||||
<list create="0" delete="0">
|
|
||||||
<field name="record_id" />
|
|
||||||
<field name="cost" />
|
|
||||||
<field name="status" />
|
|
||||||
<field name="last_server_check" optional="show" />
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
<page string="Record IDs" name="record_ids">
|
|
||||||
<group>
|
|
||||||
<field name="pending_record_ids" widget="jsonb" />
|
|
||||||
<field name="processing_record_ids" widget="jsonb" />
|
|
||||||
<field name="done_record_ids" widget="jsonb" />
|
|
||||||
<field name="error_record_ids" widget="jsonb" />
|
|
||||||
<field name="skipped_record_ids" widget="jsonb" />
|
|
||||||
</group>
|
|
||||||
</page>
|
|
||||||
</notebook>
|
|
||||||
</sheet>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_otoolkit_translation_list" model="ir.ui.view">
|
|
||||||
<field name="name">otk.translation.list</field>
|
|
||||||
<field name="model">otk.translation</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<list>
|
|
||||||
<field name="id"/>
|
|
||||||
<field name="model_id" />
|
|
||||||
<field name="record_id" />
|
|
||||||
<field name="action_id" />
|
|
||||||
<field name="status" />
|
|
||||||
<field name="last_server_check" optional="show" />
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_otoolkit_translation_form" model="ir.ui.view">
|
|
||||||
<field name="name">otk.translation.form</field>
|
|
||||||
<field name="model">otk.translation</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<header>
|
|
||||||
<button invisible="status!='success'" name="revert_translation" string="Revert translation" type="object" class="btn-primary" />
|
|
||||||
<button name="open_record" string="Open record" type="object" />
|
|
||||||
</header>
|
|
||||||
<sheet>
|
|
||||||
<group>
|
|
||||||
<field name="model_id" />
|
|
||||||
<field name="record_id" />
|
|
||||||
<field name="status" />
|
|
||||||
<field name="last_server_check" invisible="status != 'pending'" />
|
|
||||||
</group>
|
|
||||||
<notebook>
|
|
||||||
<page string="Translations">
|
|
||||||
<field name="translations" widget="json_pretty" />
|
|
||||||
</page>
|
|
||||||
<page string="Initial value" invisible="status!='success' and status!='revert'">
|
|
||||||
<field name="initial_values" widget="json_pretty" />
|
|
||||||
</page>
|
|
||||||
</notebook>
|
|
||||||
</sheet>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="action_otoolkit_translation" model="ir.actions.act_window">
|
|
||||||
<field name="name">Translation actions</field>
|
|
||||||
<field name="res_model">otk.translation</field>
|
|
||||||
<field name="view_mode">list,form</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="action_otoolkit_translation_action" model="ir.actions.act_window">
|
|
||||||
<field name="name">Translation actions</field>
|
|
||||||
<field name="res_model">otk.translation.action</field>
|
|
||||||
<field name="view_mode">list,form</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<menuitem
|
|
||||||
id="otoolkit_menu"
|
|
||||||
name="Otoolkit"
|
|
||||||
parent="base.menu_custom"
|
|
||||||
sequence="100"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<menuitem
|
|
||||||
id="otoolkit_translation_actions_menu"
|
|
||||||
name="Bulk translations"
|
|
||||||
parent="otoolkit_menu"
|
|
||||||
action="action_otoolkit_translation_action"
|
|
||||||
sequence="105"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<menuitem
|
|
||||||
id="otoolkit_translation_menu"
|
|
||||||
name="Past translations"
|
|
||||||
parent="otoolkit_menu"
|
|
||||||
action="action_otoolkit_translation"
|
|
||||||
sequence="110"
|
|
||||||
/>
|
|
||||||
</odoo>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import bulk_translate
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
from odoo import fields, models, api, _
|
|
||||||
from odoo.exceptions import UserError
|
|
||||||
|
|
||||||
from ..models.translation import _is_valid_translatable_value
|
|
||||||
|
|
||||||
|
|
||||||
class BulkTranslate(models.TransientModel):
|
|
||||||
_name = 'otk.translation.bulk.translate'
|
|
||||||
_description = "Bulk translate Wizard"
|
|
||||||
|
|
||||||
model_id = fields.Many2one('ir.model', string="Model", ondelete='cascade')
|
|
||||||
base_language_id = fields.Many2one('res.lang', string="Base language", required=True, ondelete='cascade')
|
|
||||||
target_language_ids = fields.Many2many('res.lang', string="Target languages", required=True, ondelete='cascade')
|
|
||||||
field_ids = fields.Many2many(
|
|
||||||
'ir.model.fields',
|
|
||||||
string="Fields",
|
|
||||||
domain="[('translate', 'in', ['standard', 'html_translate', 'xml_translate']), ('model_id', '=', model_id), ('related', '=', False), ('store', '=', True)]",
|
|
||||||
required=True
|
|
||||||
)
|
|
||||||
overwrite_existing = fields.Boolean(
|
|
||||||
string="Overwrite existing translations",
|
|
||||||
default=False,
|
|
||||||
help="Re-translate every term, including terms that already have a "
|
|
||||||
"translation in the target languages.",
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.depends('model_id')
|
|
||||||
def _compute_model_name(self):
|
|
||||||
for record in self:
|
|
||||||
record.model_name = record.model_id.model
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def default_get(self, fields_list):
|
|
||||||
defaults = super().default_get(fields_list)
|
|
||||||
active_model = self.env.context.get('active_model')
|
|
||||||
if active_model:
|
|
||||||
model = self.env['ir.model'].search([('model', '=', active_model)], limit=1)
|
|
||||||
defaults['model_id'] = model.id
|
|
||||||
defaults['base_language_id'] = self.env['res.lang'].search([('code', '=', self.env.user.lang)], limit=1)
|
|
||||||
defaults['target_language_ids'] = self.env['res.lang'].search([('code', '!=', self.env.user.lang)])
|
|
||||||
return defaults
|
|
||||||
|
|
||||||
def _resolve_field_value(self, model_name, record_id, field_name, base_lang_code):
|
|
||||||
"""Resolve a translatable field value with language fallback.
|
|
||||||
|
|
||||||
Tries base_lang first, then en_US. Handles html_translate fields that
|
|
||||||
may return False for a specific lang context.
|
|
||||||
"""
|
|
||||||
candidates = [base_lang_code]
|
|
||||||
if base_lang_code != 'en_US':
|
|
||||||
candidates.append('en_US')
|
|
||||||
|
|
||||||
for try_lang in candidates:
|
|
||||||
record = self.env[model_name].with_context(lang=try_lang).browse(record_id)
|
|
||||||
if not record.exists():
|
|
||||||
return False
|
|
||||||
value = getattr(record, field_name, False)
|
|
||||||
if _is_valid_translatable_value(value):
|
|
||||||
return value
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _pre_filter_records(self, active_ids):
|
|
||||||
"""Pre-filter records to separate translatable from empty ones.
|
|
||||||
|
|
||||||
Uses language fallback (base_lang → en_US) to handle html_translate
|
|
||||||
fields that may not resolve for a specific lang context.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (translatable_ids, skipped_ids)
|
|
||||||
"""
|
|
||||||
model_name = self.model_id.model
|
|
||||||
base_lang_code = self.base_language_id.code
|
|
||||||
|
|
||||||
translatable_ids = []
|
|
||||||
skipped_ids = []
|
|
||||||
|
|
||||||
for record_id in active_ids:
|
|
||||||
has_content = False
|
|
||||||
for field in self.field_ids:
|
|
||||||
value = self._resolve_field_value(
|
|
||||||
model_name, record_id, field.name, base_lang_code
|
|
||||||
)
|
|
||||||
if _is_valid_translatable_value(value):
|
|
||||||
has_content = True
|
|
||||||
break
|
|
||||||
if has_content:
|
|
||||||
translatable_ids.append(record_id)
|
|
||||||
else:
|
|
||||||
skipped_ids.append(record_id)
|
|
||||||
|
|
||||||
return translatable_ids, skipped_ids
|
|
||||||
|
|
||||||
def action_translate(self):
|
|
||||||
active_ids = self.env.context.get('active_ids', [])
|
|
||||||
|
|
||||||
if not active_ids:
|
|
||||||
raise UserError(_("No records selected for translation."))
|
|
||||||
|
|
||||||
# Pre-filter: separate records with content from empty ones
|
|
||||||
translatable_ids, skipped_ids = self._pre_filter_records(active_ids)
|
|
||||||
|
|
||||||
if not translatable_ids:
|
|
||||||
raise UserError(_(
|
|
||||||
"None of the %d selected records have translatable content "
|
|
||||||
"in the selected fields (%s).\n\n"
|
|
||||||
"Please check that the records have content in the base language (%s)."
|
|
||||||
) % (
|
|
||||||
len(active_ids),
|
|
||||||
', '.join(self.field_ids.mapped('field_description')),
|
|
||||||
self.base_language_id.name,
|
|
||||||
))
|
|
||||||
|
|
||||||
self.env['otk.translation.action'].sudo().create({
|
|
||||||
'model_id': self.model_id.id,
|
|
||||||
'base_language_id': self.base_language_id.id,
|
|
||||||
'target_language_ids': self.target_language_ids.ids,
|
|
||||||
'field_ids': self.field_ids.ids,
|
|
||||||
'overwrite_existing': self.overwrite_existing,
|
|
||||||
'pending_record_ids': translatable_ids,
|
|
||||||
'skipped_record_ids': skipped_ids,
|
|
||||||
'translation_count': len(translatable_ids),
|
|
||||||
})
|
|
||||||
|
|
||||||
# Show summary notification if some records were skipped
|
|
||||||
if skipped_ids:
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.client',
|
|
||||||
'tag': 'display_notification',
|
|
||||||
'params': {
|
|
||||||
'title': _("Translation Started"),
|
|
||||||
'message': _(
|
|
||||||
"%d records queued for translation.\n"
|
|
||||||
"%d records skipped (no translatable content in selected fields)."
|
|
||||||
) % (len(translatable_ids), len(skipped_ids)),
|
|
||||||
'type': 'warning',
|
|
||||||
'sticky': True,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<record id="view_bulk_translate_wizard_form" model="ir.ui.view">
|
|
||||||
<field name="name">translation.bulk.translate.wizard.form</field>
|
|
||||||
<field name="model">otk.translation.bulk.translate</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form string="Bulk translate">
|
|
||||||
<sheet>
|
|
||||||
<group>
|
|
||||||
<field invisible="1" name="model_id" />
|
|
||||||
<field options="{'no_create': True, 'no_open': True}" name="field_ids" widget="many2many_tags" />
|
|
||||||
<field options="{'no_create': True, 'no_open': True}" name="base_language_id" />
|
|
||||||
<field options="{'no_create': True, 'no_open': True}" name="target_language_ids" widget="many2many_tags" />
|
|
||||||
<field name="overwrite_existing" />
|
|
||||||
</group>
|
|
||||||
</sheet>
|
|
||||||
<footer>
|
|
||||||
<button string="Translate" name="action_translate" type="object" class="btn-primary"/>
|
|
||||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
|
500 Internal Server Error
Gitea Version: 1.28.0+dev-115-gb06002f449 |