diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst new file mode 100644 index 0000000..cfa1415 --- /dev/null +++ b/ai_oca_bridge/README.rst @@ -0,0 +1,205 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============= +AI OCA Bridge +============= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c702d03816331f686ec7f037a5bbef08e513f54645bfdb51d11b15bcd5db77f2 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github + :target: https://github.com/OCA/ai/tree/19.0/ai_oca_bridge + :alt: OCA/ai +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/ai-19-0/ai-19-0-ai_oca_bridge + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is used to create a bridge between Odoo and other AI systems +like n8n. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Right now, there are 2 different approaches for AI integration with +Odoo: + +1. Make everything inside Odoo. +2. Make it using other tools and integrate Odoo with these tools. + +IMO, it would be better to make use of option 2 for different reasons: + +- Odoo server is intended as a transactional system. AI systems requires + other kind of characteristics +- Everything changes too fast. I am not confident that Odoo can keep the + pace in this topic +- There are OSS tools that fills the gap perfectly and are created just + for this topic. + +Anyway, OCA is open to everyone and we don't intend to force an +opinionated way of doing. For this reason, we have this module, that can +be used as Bridge with AI systems. + +Configuration +============= + +As an administrator access ``AI Bridge\AI Bridge``. + +Create a new bridge. Define the name, model, url and configuration. + +In order to improve the view of the AI configuration, use groups and +domain to set better filters. + +Payload Configuration +--------------------- + +On the external system, you will receive a POST payload. The data +included will be the following: + +General +~~~~~~~ + +- \_odoo: Standard data to identify the Odoo Database +- \_model: Model of the related object +- \_id: Id of the related object +- \_response_url: Url to call with the response in case of async calls + +Record Payload +~~~~~~~~~~~~~~ + +Adds a new item called record with all the fields. + +Asynchronous and synchronous calls +---------------------------------- + +The new system allows asynchronous and synchronous calls. Asynchronous +calls makes sense when the task to be processed don't need to be +immediate. For example, reviewing an invoice and leave a comment with +the result. The same would happen with a chat message. We expect that +the system will leave time to the AI to answer and Odoo's user can do +other things. + +Meanwhile, Synchronous calls will froze odoo system and wait for an +answer. This makes sense when we expect some feedback from odoo user. It +makes sense, when we open an action for example. + +In the synchronous call, the result is processed when the AI system +answers on the webhook. On the other hand, it will be processed +automatically on the synchronous call. + +Result processing +----------------- + +With the answers of the system we expect to do something about it. We +have the following options: + +No processing +~~~~~~~~~~~~~ + +In this case, the result will do nothing + +Post a Message +~~~~~~~~~~~~~~ + +We will post a message on the original thread of the system. The thread +is computed by a function, so it can be overriden in future modules. It +expects the keyword arguments of the ``message_post`` function. + +Action +~~~~~~ + +It expects to launch an action on the user interface. It only makes +sense on synchronous calls. + +It expects an action item with the following parameters: + +- action: xmlid of the action +- context: Context to pass to the action (not required) +- res_id: Id of the resource (not required) + +Usage +===== + +Use the bolt widget in the chatter to execute the different AI options. + +The options will be filtered according to the configuration. + +Known issues / Roadmap +====================== + +- Define examples to use and import +- Allow child fields. Right now, only first level fields are accepted. +- Information popover is not working properly when there is large data. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- `Dixmit `__ + + - Enric Tobella + +- `Sygel Technology `__ + + - Valentín Vinagre + +- `Binhex `__ + + - Ariel Barreiros + - Adria Hortoneda + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/ai `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/ai_oca_bridge/__init__.py b/ai_oca_bridge/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/ai_oca_bridge/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/ai_oca_bridge/__manifest__.py b/ai_oca_bridge/__manifest__.py new file mode 100644 index 0000000..8f8a92d --- /dev/null +++ b/ai_oca_bridge/__manifest__.py @@ -0,0 +1,39 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "AI OCA Bridge", + "summary": """ + Makes a basic configuration to be used as bridge with external AI systems + """, + "version": "19.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/ai", + "category": "AI", + "development_status": "Beta", + "depends": [ + "base", + "mail", + "web", + ], + "data": [ + "data/ir_module_category.xml", + "security/ir.model.access.csv", + "security/security.xml", + "views/menu.xml", + "views/ai_bridge_execution.xml", + "views/ai_bridge.xml", + ], + "assets": { + "web.assets_backend": [ + "ai_oca_bridge/static/src/chatter_ai_registry.esm.js", + "ai_oca_bridge/static/src/**/*.xml", + "ai_oca_bridge/static/src/**/*.esm.js", + ], + "web.assets_unit_tests": [ + "ai_oca_bridge/static/tests/**/*", + ], + }, + "application": True, +} diff --git a/ai_oca_bridge/controllers/__init__.py b/ai_oca_bridge/controllers/__init__.py new file mode 100644 index 0000000..5a0e9a5 --- /dev/null +++ b/ai_oca_bridge/controllers/__init__.py @@ -0,0 +1 @@ +from . import ai diff --git a/ai_oca_bridge/controllers/ai.py b/ai_oca_bridge/controllers/ai.py new file mode 100644 index 0000000..76929a0 --- /dev/null +++ b/ai_oca_bridge/controllers/ai.py @@ -0,0 +1,50 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from odoo import fields, http +from odoo.http import request +from odoo.tools import consteq + + +class AIController(http.Controller): + @http.route( + [ + "/ai/response//", + ], + type="http", + auth="public", + cors="*", + csrf=False, + ) + def ai_process_response(self, execution_id, token): + execution = request.env["ai.bridge.execution"].sudo().browse(execution_id) + if not execution.exists(): + return request.make_response(self.env._("Execution not found."), status=404) + if not consteq(execution._generate_token(), token): + return request.make_response( + self.env._("Token is not allowed for this execution."), + status=404, + ) + if ( + not execution.expiration_date + or execution.expiration_date < fields.Datetime.now() + ): + return request.make_response( + self.env._("Execution is expired."), status=404 + ) + try: + charset = request.httprequest.charset + except AttributeError: + charset = "utf-8" + return request.make_response( + json.dumps( + execution._process_response( + json.loads(request.httprequest.get_data().decode(charset)) + ) + ), + headers=[ + ("Content-Type", "application/json"), + ], + ) diff --git a/ai_oca_bridge/data/ir_module_category.xml b/ai_oca_bridge/data/ir_module_category.xml new file mode 100644 index 0000000..3917769 --- /dev/null +++ b/ai_oca_bridge/data/ir_module_category.xml @@ -0,0 +1,7 @@ + + + + AI + 99 + + diff --git a/ai_oca_bridge/examples/filter_spam_tickets.xml b/ai_oca_bridge/examples/filter_spam_tickets.xml new file mode 100644 index 0000000..fe2842e --- /dev/null +++ b/ai_oca_bridge/examples/filter_spam_tickets.xml @@ -0,0 +1,42 @@ + + + + Spam + 1 + + + + AI Action: Mark Ticket as Spam + + code + +ai_data = env.context.get('ai_response', {}) + +reason = ai_data.get('reason', 'AI System Flagged') +confidence = ai_data.get('confidence', 'N/A') + +spam_tag = env.ref('ai_oca_bridge.tag_helpdesk_spam', raise_if_not_found=False) + +if spam_tag: + for ticket in records: + if spam_tag.id not in ticket.tag_ids.ids: + ticket.write({'tag_ids': [(4, spam_tag.id)]}) + + msg_body = f"🤖 <b>SPAM DETECTED</b><br/>Reason: {reason} (Confidence: {confidence})" + ticket.message_post(body=msg_body) + + + + + Helpdesk Ticket Spam Identification + + ai_thread_create + async + server_action + + + + diff --git a/ai_oca_bridge/examples/filter_spam_tickets_n8n.json b/ai_oca_bridge/examples/filter_spam_tickets_n8n.json new file mode 100644 index 0000000..9908422 --- /dev/null +++ b/ai_oca_bridge/examples/filter_spam_tickets_n8n.json @@ -0,0 +1,147 @@ +{ + "name": "Spam Ticket Identification Flow", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "helpdesk/check-spam/", + "options": { + "rawBody": true + } + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [-1264, 80], + "id": "960d09d1-c2a3-4710-9c9a-07aa0cd3f795", + "name": "Webhook", + "webhookId": "da674dc0-4dc8-4ed6-a51d-59afff4272e2" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "99e8601c-e16b-4df6-9df9-f445ec130db8", + "leftValue": "={{ JSON.parse($json.message.content).is_spam }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [-688, 80], + "id": "982df316-3690-434c-85fa-c0c1da46dfdb", + "name": "If" + }, + { + "parameters": { + "modelId": { + "__rl": true, + "value": "gpt-4o-mini", + "mode": "list", + "cachedResultName": "GPT-4O-MINI" + }, + "messages": { + "values": [ + { + "content": "You are a strict Spam Detection Officer for an Odoo/IT Helpdesk.\n Your task is to analyze the incoming ticket description and determine whether it is spam.\nCRITERIA FOR SPAM — Return true when ANY of these is detected:\n1. SEO, marketing, backlink, or website ranking service offers.\n2. Unrelated service promotions (web design, mobile apps, logo design, copywriting, outsourcing not related to our project).\n3. Suspicious links, shortened URLs without context, or phishing-like messages.\n4. Bulk-generated outreach messages (generic, not referencing Odoo/IT context).\n5. Nonsense text, random characters, or obviously AI-generated gibberish.\n6. Messages unrelated to IT, Odoo, ERP, or our provided services.\nCRITERIA FOR HAM — Return false:\n1. Issues or questions related to Odoo, modules, customization, bugs, deployments, or integration.\n2. Questions about invoices, access, accounts, or contract-related matters.\n3. Complaints, feedback, or feature requests.\n4. Any message that resembles a genuine support request from a human.\nOUTPUT FORMAT:\nStrictly return a JSON object with this structure:\n{\n \"is_spam\": boolean,\n \"reason\": \"Short explanation why this is spam (max 1 sentence)\",\n \"confidence\": \"Estimation from 0% to 100% (e.g. '98%')\"\n}\nExample: {\"is_spam\": true, \"reason\": \"Mentions prohibited drugs (Viagra).\", \"confidence\": \"99%\"}", + "role": "system" + }, + { + "content": "={{ $json.body.record.display_name}} - {{ $json.body.record.description }}" + } + ] + }, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.openAi", + "typeVersion": 1.8, + "position": [-1056, 80], + "id": "e431b723-d2a4-4d3c-a8ae-420584da691c", + "name": "Message a model", + "credentials": { + "openAiApi": { + "id": "t7lm95Kw7wmoj4xt", + "name": "OpenAi account 2" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "={{$('Webhook').item.json.body._response_url}}", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{$json.message.content}}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [-432, -32], + "id": "a9e87afa-4907-436e-a241-f63c964af12b", + "name": "HTTP Request", + "alwaysOutputData": false + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Message a model", + "type": "main", + "index": 0 + } + ] + ] + }, + "Message a model": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + } + ] + ] + }, + "If": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "d7f051a5-c962-44b6-87b1-54441763efef", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "95483db1596026047067fce31863479f616aaf310d370d2a94b0e438c3495911" + }, + "id": "fWYne7mmDfzimDWd", + "tags": [] +} diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot new file mode 100644 index 0000000..42352ad --- /dev/null +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -0,0 +1,678 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_oca_bridge +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \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: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_kind +msgid "" +"\n" +" Defines how the result from the AI system is processed.\n" +" - 'Immediate': The result is processed immediately after the AI system responds.\n" +" - 'Asynchronous': The result is processed in the background.\n" +" It allows longer operations.\n" +" Odoo will provide a URL to the AI system where the response will be sent.\n" +" Users will receive a notification when the operation is started.\n" +" No notification will be sent when it is finished.\n" +" " +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s executed successfully." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s failed." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s is not active." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.module.category,name:ai_oca_bridge.module_category_ai +msgid "AI" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_act_window +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_menu +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_root_menu +msgid "AI Bridge" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Executed" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_execution_menu +msgid "AI Bridge Execution" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Failed" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Inactive" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_thread +msgid "AI Bridge Mixin" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window +msgid "AI Execution" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action +msgid "Action" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__active +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Active" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids +msgid "Activities" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_state +msgid "Activity State" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__ai_bridge_id +msgid "Ai Bridge" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge +msgid "Ai Bridge Configuration" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_discuss_channel__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_cc__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_main_attachment__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_phone__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_phone_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_partner__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_users__ai_bridge_info +msgid "Ai Bridge Info" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_execution +msgid "Ai Execution" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Archived" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "Async Timeout" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__async +msgid "Asynchronous" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_password +msgid "Auth Password" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_token +msgid "Auth Token" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_username +msgid "Auth Username" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_type +msgid "Authentication Type" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_base +msgid "Base" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic +msgid "Basic Authentication" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__company_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__company_id +msgid "Company" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_date +msgid "Created on" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage +msgid "" +"Defines how this bridge is used. If 'Thread', it will be used in the mail " +"thread context." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_type +msgid "Defines the type of result expected from the AI system." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__description +msgid "Description" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__display_name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__done +msgid "Done" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft +msgid "Draft" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_mail_thread +msgid "Email Thread" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__error +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__error +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Error" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids +msgid "Execution" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_count +msgid "Execution Count" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Execution Details" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution is expired." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution not found." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration Date" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration date for the async operation token." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Field" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Fields to include in the AI bridge." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__domain +msgid "Filter" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__group_ids +msgid "Group" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message +msgid "Has Message" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__id +msgid "ID" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__immediate +msgid "Immediate" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_ids +msgid "Messages" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__model_id +msgid "Model" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model +msgid "Model Name" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_required +msgid "Model Required" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__name +msgid "Name" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__none +msgid "No payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none +msgid "No processing" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__none +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__none +msgid "None" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create +msgid "On Record Created" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "On Record Deleted" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "On Record Updated" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload_txt +msgid "Payload Txt" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__payload_type +msgid "Payload Type" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__message +msgid "Post a Message" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record +msgid "Record" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Record Payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id +msgid "Res" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Response" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result +msgid "Result" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind +msgid "Result Kind" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_type +msgid "Result Type" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Sample" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "Sample Payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "" +"Sample payload to be sent to the AI system. This is used for testing and " +"debugging purposes." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sequence +msgid "Sequence" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state +msgid "State" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__url +msgid "The URL of the external AI system to which this bridge connects." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__model_id +msgid "The model to which this bridge is associated." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__auth_type +msgid "The type of authentication used to connect to the external AI system." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__user_id +msgid "The user that will be shown when executing this AI bridge." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread +msgid "Thread" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "" +"Timeout in seconds for asynchronous operations. If the operation does not " +"complete within this time, it will be considered failed." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token +msgid "Token Authentication" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Token is not allowed for this execution." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__url +msgid "URL" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 +msgid "Unsupported authentication type." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__usage +msgid "Usage" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id +msgid "User" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__group_ids +msgid "User groups allowed to use this AI bridge." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "" +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." +msgstr "" diff --git a/ai_oca_bridge/i18n/es.po b/ai_oca_bridge/i18n/es.po new file mode 100644 index 0000000..d15a5f2 --- /dev/null +++ b/ai_oca_bridge/i18n/es.po @@ -0,0 +1,750 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_oca_bridge +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-08-15 22:35+0000\n" +"Last-Translator: \"Leonardo J. Caballero G.\" \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_kind +msgid "" +"\n" +" Defines how the result from the AI system is processed.\n" +" - 'Immediate': The result is processed immediately after the AI " +"system responds.\n" +" - 'Asynchronous': The result is processed in the background.\n" +" It allows longer operations.\n" +" Odoo will provide a URL to the AI system where the response will " +"be sent.\n" +" Users will receive a notification when the operation is started.\n" +" No notification will be sent when it is finished.\n" +" " +msgstr "" +"\n" +" Define cómo se procesa el resultado del sistema de IA.\n" +" - 'Inmediato': el resultado se procesa inmediatamente después de que " +"el sistema de IA responda.\n" +" - 'Asíncrono': El resultado se procesa en segundo plano.\n" +" Permite operaciones más largas.\n" +" Odoo proporcionará una URL al sistema de IA donde se enviará la " +"respuesta.\n" +" Los usuarios recibirán una notificación cuando se inicie la " +"operación.\n" +" No se enviará ninguna notificación cuando finalice.\n" +" " + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s executed successfully." +msgstr "%s ejecutado correctamente." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s failed." +msgstr "%s falló." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s is not active." +msgstr "%s no está activo." + +#. module: ai_oca_bridge +#: model:ir.module.category,name:ai_oca_bridge.module_category_ai +msgid "AI" +msgstr "IA" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_act_window +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_menu +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_root_menu +msgid "AI Bridge" +msgstr "Puente de IA" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Executed" +msgstr "Puente de IA ejecutado" + +#. module: ai_oca_bridge +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_execution_menu +msgid "AI Bridge Execution" +msgstr "Ejecución de Puente de IA" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Failed" +msgstr "Puente de IA fallo" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Inactive" +msgstr "Puente de IA inactivo" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_thread +msgid "AI Bridge Mixin" +msgstr "Mezcla de puente de IA" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window +msgid "AI Execution" +msgstr "Ejecución de IA" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action +msgid "Action" +msgstr "Acción" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "Action Needed" +msgstr "Acción necesaria" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__active +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Active" +msgstr "Activo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids +msgid "Activities" +msgstr "Actividades" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decoración de excepción de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_state +msgid "Activity State" +msgstr "Estado de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icono de tipo de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__ai_bridge_id +msgid "Ai Bridge" +msgstr "Puente de IA" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge +msgid "Ai Bridge Configuration" +msgstr "Configuración de puente de IA" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_discuss_channel__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_cc__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_main_attachment__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_phone__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_phone_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_partner__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_users__ai_bridge_info +msgid "Ai Bridge Info" +msgstr "Información de puente de IA" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_execution +msgid "Ai Execution" +msgstr "Ejecución de IA" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Archived" +msgstr "Archivado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "Async Timeout" +msgstr "Tiempo de espera asíncrono" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__async +msgid "Asynchronous" +msgstr "Asincrónica" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_attachment_count +msgid "Attachment Count" +msgstr "Conteo de anexos" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_password +msgid "Auth Password" +msgstr "Contraseña de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_token +msgid "Auth Token" +msgstr "Token de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_username +msgid "Auth Username" +msgstr "Nombre de usuario de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_type +msgid "Authentication Type" +msgstr "Tipo de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_base +msgid "Base" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic +msgid "Basic Authentication" +msgstr "Autenticación básica" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__company_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__company_id +msgid "Company" +msgstr "Compañía" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage +msgid "" +"Defines how this bridge is used. If 'Thread', it will be used in the mail " +"thread context." +msgstr "" +"Define cómo se utiliza este puente. Si es 'Hilo', se usará en el contexto " +"del hilo de correo." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_type +msgid "Defines the type of result expected from the AI system." +msgstr "Define el tipo de resultado esperado del sistema de IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__description +msgid "Description" +msgstr "Descripción" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__display_name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__display_name +msgid "Display Name" +msgstr "Nombre a mostrar" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__done +msgid "Done" +msgstr "Hecho" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft +msgid "Draft" +msgstr "Borrador" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_mail_thread +msgid "Email Thread" +msgstr "Hilo de correo electrónico" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__error +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__error +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Error" +msgstr "Error" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids +msgid "Execution" +msgstr "Ejecución" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_count +msgid "Execution Count" +msgstr "Recuento de ejecuciones" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Execution Details" +msgstr "Detalles de ejecución" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution is expired." +msgstr "La ejecución ha expirado." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution not found." +msgstr "No se ha encontrado la ejecución." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration Date" +msgstr "Fecha de expiración" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration date for the async operation token." +msgstr "Fecha de expiración del token de operación asincrónica." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Field" +msgstr "Campo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Fields to include in the AI bridge." +msgstr "Campos que se incluirán en el puente de IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__domain +msgid "Filter" +msgstr "Filtrar" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores (Socios)" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icono Font awesome, por ejemplo fa-tasks" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__group_ids +msgid "Group" +msgstr "Grupo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message +msgid "Has Message" +msgstr "Tiene mensaje" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__id +msgid "ID" +msgstr "ID" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon" +msgstr "Icono" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icono para indicar una actividad de excepción." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Si está marcada, nuevos mensajes requieren su atención." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Si se activa, algunos mensajes tienen un error de envío." + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__immediate +msgid "Immediate" +msgstr "Inmediato" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower +msgid "Is Follower" +msgstr "Es seguidor" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "Message Delivery error" +msgstr "Error de entrega del mensaje" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__model_id +msgid "Model" +msgstr "Modelo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model +msgid "Model Name" +msgstr "Nombre del modelo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_required +msgid "Model Required" +msgstr "Modelo requerido" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Mi plazo de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__name +msgid "Name" +msgstr "Nombre" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Fecha límite para la próxima actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_summary +msgid "Next Activity Summary" +msgstr "Resumen de la próxima actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo de la próxima actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__none +msgid "No payload" +msgstr "Sin carga útil" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none +msgid "No processing" +msgstr "Sin procesamiento" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__none +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__none +msgid "None" +msgstr "Ninguno" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de acciones" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of errors" +msgstr "Número de errores" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Número de mensajes que requieren una acción" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Número de mensajes con error de entrega" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create +msgid "On Record Created" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "On Record Deleted" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "On Record Updated" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Payload" +msgstr "Carga útil" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload_txt +msgid "Payload Txt" +msgstr "Carga útil de texto" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__payload_type +msgid "Payload Type" +msgstr "Tipo de carga útil" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__message +msgid "Post a Message" +msgstr "Deja un mensaje" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record +msgid "Record" +msgstr "Registro" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Record Payload" +msgstr "Carga útil de registro" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id +msgid "Res" +msgstr "Respuesta" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Response" +msgstr "Respuesta" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_user_id +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result +msgid "Result" +msgstr "Resultado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind +msgid "Result Kind" +msgstr "Tipo de resultado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_type +msgid "Result Type" +msgstr "Tipo de resultado" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Sample" +msgstr "Ejemplo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "Sample Payload" +msgstr "Carga útil de muestra" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "" +"Sample payload to be sent to the AI system. This is used for testing and " +"debugging purposes." +msgstr "" +"Carga útil de muestra que se enviará al sistema de IA. Esto se utiliza con " +"fines de prueba y depuración." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state +msgid "State" +msgstr "Estado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Estado basado en actividades\n" +"Vencida: La fecha de vencimiento ya ha pasado\n" +"Hoy: La fecha de la actividad es hoy\n" +"Planificada: Actividades futuras." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__url +msgid "The URL of the external AI system to which this bridge connects." +msgstr "La URL del sistema de IA externo al que se conecta este puente." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__model_id +msgid "The model to which this bridge is associated." +msgstr "El modelo al que se asocia este puente." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__auth_type +msgid "The type of authentication used to connect to the external AI system." +msgstr "" +"El tipo de autenticación utilizado para conectarse al sistema de IA externo." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__user_id +msgid "The user that will be shown when executing this AI bridge." +msgstr "El usuario que se mostrará al ejecutar este puente de IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread +msgid "Thread" +msgstr "Hilo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "" +"Timeout in seconds for asynchronous operations. If the operation does not " +"complete within this time, it will be considered failed." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token +msgid "Token Authentication" +msgstr "Autenticación de token" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Token is not allowed for this execution." +msgstr "Token no está permitido para esta ejecución." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo de la actividad de excepción registrada." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__url +msgid "URL" +msgstr "Dirección URL" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 +msgid "Unsupported authentication type." +msgstr "Tipo de autenticación no admitido." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__usage +msgid "Usage" +msgstr "Uso" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id +msgid "User" +msgstr "Usuario" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__group_ids +msgid "User groups allowed to use this AI bridge." +msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "" +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." +msgstr "" + +#~ msgid "AI Thread Create" +#~ msgstr "Crear hilo de IA" + +#~ msgid "AI Thread Unlink" +#~ msgstr "Eliminar hilo de IA" + +#~ msgid "AI Thread Write" +#~ msgstr "Escribir hilo de IA" + +#~ msgid "Ai Usage" +#~ msgstr "Uso de IA" + +#~ msgid "Is Ai Bridge Thread" +#~ msgstr "Es un hilo del puente IA" + +#~ msgid "Models" +#~ msgstr "Modelos" + +#~ msgid "" +#~ "When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +#~ msgstr "" +#~ "Cuando el uso es \"Hilo Puente IA\", el tipo de carga útil debe ser \"Sin " +#~ "carga útil\"." + +#~ msgid "Last Modified on" +#~ msgstr "Última modificación el" + +#~ msgid "Main Attachment" +#~ msgstr "Adjunto principal" + +#~ msgid "Record v0" +#~ msgstr "Registro v0" + +#~ msgid "" +#~ "Timeout in seconds for asynchronous operations. If the operation does not " +#~ "complete within this time, it will be considered failed." +#~ msgstr "" +#~ "Tiempo de espera en segundos para operaciones asincrónicas. Si la " +#~ "operación no se completa dentro de este tiempo, se considerará fallida." + +#~ msgid "Website Messages" +#~ msgstr "Mensajes del sitio web" + +#~ msgid "Website communication history" +#~ msgstr "Historia de la comunicación en la web" diff --git a/ai_oca_bridge/i18n/es_VE.po b/ai_oca_bridge/i18n/es_VE.po new file mode 100644 index 0000000..2f0a7a7 --- /dev/null +++ b/ai_oca_bridge/i18n/es_VE.po @@ -0,0 +1,750 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_oca_bridge +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-08-16 01:25+0000\n" +"Last-Translator: \"Leonardo J. Caballero G.\" \n" +"Language-Team: none\n" +"Language: es_VE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_kind +msgid "" +"\n" +" Defines how the result from the AI system is processed.\n" +" - 'Immediate': The result is processed immediately after the AI " +"system responds.\n" +" - 'Asynchronous': The result is processed in the background.\n" +" It allows longer operations.\n" +" Odoo will provide a URL to the AI system where the response will " +"be sent.\n" +" Users will receive a notification when the operation is started.\n" +" No notification will be sent when it is finished.\n" +" " +msgstr "" +"\n" +" Define cómo se procesa el resultado del sistema de IA.\n" +" - 'Inmediato': el resultado se procesa inmediatamente después de que " +"el sistema de IA responda.\n" +" - 'Asíncrono': El resultado se procesa en segundo plano.\n" +" Permite operaciones más largas.\n" +" Odoo proporcionará una URL al sistema de IA donde se enviará la " +"respuesta.\n" +" Los usuarios recibirán una notificación cuando se inicie la " +"operación.\n" +" No se enviará ninguna notificación cuando finalice.\n" +" " + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s executed successfully." +msgstr "%s ejecutado correctamente." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s failed." +msgstr "%s falló." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s is not active." +msgstr "%s no está activo." + +#. module: ai_oca_bridge +#: model:ir.module.category,name:ai_oca_bridge.module_category_ai +msgid "AI" +msgstr "IA" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_act_window +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_menu +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_root_menu +msgid "AI Bridge" +msgstr "Puente de IA" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Executed" +msgstr "Puente de IA ejecutado" + +#. module: ai_oca_bridge +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_execution_menu +msgid "AI Bridge Execution" +msgstr "Ejecución de Puente de IA" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Failed" +msgstr "Puente de IA fallo" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Inactive" +msgstr "Puente de IA inactivo" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_thread +msgid "AI Bridge Mixin" +msgstr "Mezcla de puente de IA" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window +msgid "AI Execution" +msgstr "Ejecución de IA" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action +msgid "Action" +msgstr "Acción" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "Action Needed" +msgstr "Acción necesaria" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__active +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Active" +msgstr "Activo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids +msgid "Activities" +msgstr "Actividades" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decoración de excepción de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_state +msgid "Activity State" +msgstr "Estado de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icono de tipo de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__ai_bridge_id +msgid "Ai Bridge" +msgstr "Puente de IA" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge +msgid "Ai Bridge Configuration" +msgstr "Configuración de puente de IA" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_discuss_channel__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_cc__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_main_attachment__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_phone__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_phone_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_partner__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_users__ai_bridge_info +msgid "Ai Bridge Info" +msgstr "Información de puente de IA" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_execution +msgid "Ai Execution" +msgstr "Ejecución de IA" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Archived" +msgstr "Archivado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "Async Timeout" +msgstr "Tiempo de espera asíncrono" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__async +msgid "Asynchronous" +msgstr "Asincrónica" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_attachment_count +msgid "Attachment Count" +msgstr "Conteo de anexos" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_password +msgid "Auth Password" +msgstr "Contraseña de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_token +msgid "Auth Token" +msgstr "Token de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_username +msgid "Auth Username" +msgstr "Nombre de usuario de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_type +msgid "Authentication Type" +msgstr "Tipo de autenticación" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_base +msgid "Base" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic +msgid "Basic Authentication" +msgstr "Autenticación básica" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__company_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__company_id +msgid "Company" +msgstr "Compañía" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage +msgid "" +"Defines how this bridge is used. If 'Thread', it will be used in the mail " +"thread context." +msgstr "" +"Define cómo se utiliza este puente. Si es 'Hilo', se usará en el contexto " +"del hilo de correo." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_type +msgid "Defines the type of result expected from the AI system." +msgstr "Define el tipo de resultado esperado del sistema de IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__description +msgid "Description" +msgstr "Descripción" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__display_name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__display_name +msgid "Display Name" +msgstr "Nombre a mostrar" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__done +msgid "Done" +msgstr "Hecho" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft +msgid "Draft" +msgstr "Borrador" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_mail_thread +msgid "Email Thread" +msgstr "Hilo de correo electrónico" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__error +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__error +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Error" +msgstr "Error" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids +msgid "Execution" +msgstr "Ejecución" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_count +msgid "Execution Count" +msgstr "Recuento de ejecuciones" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Execution Details" +msgstr "Detalles de ejecución" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution is expired." +msgstr "La ejecución ha expirado." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution not found." +msgstr "No se ha encontrado la ejecución." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration Date" +msgstr "Fecha de expiración" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration date for the async operation token." +msgstr "Fecha de expiración del token de operación asincrónica." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Field" +msgstr "Campo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Fields to include in the AI bridge." +msgstr "Campos que se incluirán en el puente de IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__domain +msgid "Filter" +msgstr "Filtrar" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores (Socios)" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icono Font awesome, por ejemplo fa-tasks" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__group_ids +msgid "Group" +msgstr "Grupo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message +msgid "Has Message" +msgstr "Tiene mensaje" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__id +msgid "ID" +msgstr "ID" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon" +msgstr "Icono" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icono para indicar una actividad de excepción." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Si está marcada, nuevos mensajes requieren su atención." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Si se activa, algunos mensajes tienen un error de envío." + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__immediate +msgid "Immediate" +msgstr "Inmediato" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower +msgid "Is Follower" +msgstr "Es seguidor" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "Message Delivery error" +msgstr "Error de entrega del mensaje" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__model_id +msgid "Model" +msgstr "Modelo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model +msgid "Model Name" +msgstr "Nombre del modelo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_required +msgid "Model Required" +msgstr "Modelo requerido" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Mi plazo de actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__name +msgid "Name" +msgstr "Nombre" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Fecha límite para la próxima actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_summary +msgid "Next Activity Summary" +msgstr "Resumen de la próxima actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo de la próxima actividad" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__none +msgid "No payload" +msgstr "Sin carga útil" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none +msgid "No processing" +msgstr "Sin procesamiento" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__none +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__none +msgid "None" +msgstr "Ninguno" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de acciones" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of errors" +msgstr "Número de errores" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Número de mensajes que requieren una acción" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Número de mensajes con error de entrega" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create +msgid "On Record Created" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "On Record Deleted" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "On Record Updated" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Payload" +msgstr "Carga útil" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload_txt +msgid "Payload Txt" +msgstr "Carga útil de texto" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__payload_type +msgid "Payload Type" +msgstr "Tipo de carga útil" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__message +msgid "Post a Message" +msgstr "Deja un mensaje" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record +msgid "Record" +msgstr "Registro" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Record Payload" +msgstr "Carga útil de registro" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id +msgid "Res" +msgstr "Respuesta" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Response" +msgstr "Respuesta" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_user_id +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result +msgid "Result" +msgstr "Resultado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind +msgid "Result Kind" +msgstr "Tipo de resultado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_type +msgid "Result Type" +msgstr "Tipo de resultado" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Sample" +msgstr "Ejemplo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "Sample Payload" +msgstr "Carga útil de muestra" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "" +"Sample payload to be sent to the AI system. This is used for testing and " +"debugging purposes." +msgstr "" +"Carga útil de muestra que se enviará al sistema de IA. Esto se utiliza con " +"fines de prueba y depuración." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state +msgid "State" +msgstr "Estado" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Estado basado en actividades\n" +"Vencida: La fecha de vencimiento ya ha pasado\n" +"Hoy: La fecha de la actividad es hoy\n" +"Planificada: Actividades futuras." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__url +msgid "The URL of the external AI system to which this bridge connects." +msgstr "La URL del sistema de IA externo al que se conecta este puente." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__model_id +msgid "The model to which this bridge is associated." +msgstr "El modelo al que se asocia este puente." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__auth_type +msgid "The type of authentication used to connect to the external AI system." +msgstr "" +"El tipo de autenticación utilizado para conectarse al sistema de IA externo." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__user_id +msgid "The user that will be shown when executing this AI bridge." +msgstr "El usuario que se mostrará al ejecutar este puente de IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread +msgid "Thread" +msgstr "Hilo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "" +"Timeout in seconds for asynchronous operations. If the operation does not " +"complete within this time, it will be considered failed." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token +msgid "Token Authentication" +msgstr "Autenticación de token" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Token is not allowed for this execution." +msgstr "Token no está permitido para esta ejecución." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo de la actividad de excepción registrada." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__url +msgid "URL" +msgstr "Dirección URL" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 +msgid "Unsupported authentication type." +msgstr "Tipo de autenticación no admitido." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__usage +msgid "Usage" +msgstr "Uso" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id +msgid "User" +msgstr "Usuario" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__group_ids +msgid "User groups allowed to use this AI bridge." +msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "" +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." +msgstr "" + +#~ msgid "AI Thread Create" +#~ msgstr "Crear hilo de IA" + +#~ msgid "AI Thread Unlink" +#~ msgstr "Eliminar hilo de IA" + +#~ msgid "AI Thread Write" +#~ msgstr "Escribir hilo de IA" + +#~ msgid "Ai Usage" +#~ msgstr "Uso de IA" + +#~ msgid "Is Ai Bridge Thread" +#~ msgstr "Es un hilo del puente IA" + +#~ msgid "Models" +#~ msgstr "Modelos" + +#~ msgid "" +#~ "When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +#~ msgstr "" +#~ "Cuando el uso es \"Hilo Puente IA\", el tipo de carga útil debe ser \"Sin " +#~ "carga útil\"." + +#~ msgid "Last Modified on" +#~ msgstr "Última modificación el" + +#~ msgid "Main Attachment" +#~ msgstr "Adjunto principal" + +#~ msgid "Record v0" +#~ msgstr "Registro v0" + +#~ msgid "" +#~ "Timeout in seconds for asynchronous operations. If the operation does not " +#~ "complete within this time, it will be considered failed." +#~ msgstr "" +#~ "Tiempo de espera en segundos para operaciones asincrónicas. Si la " +#~ "operación no se completa dentro de este tiempo, se considerará fallida." + +#~ msgid "Website Messages" +#~ msgstr "Mensajes del sitio web" + +#~ msgid "Website communication history" +#~ msgstr "Historia de la comunicación en la web" diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po new file mode 100644 index 0000000..0669676 --- /dev/null +++ b/ai_oca_bridge/i18n/it.po @@ -0,0 +1,749 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_oca_bridge +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-12-11 10:42+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_kind +msgid "" +"\n" +" Defines how the result from the AI system is processed.\n" +" - 'Immediate': The result is processed immediately after the AI " +"system responds.\n" +" - 'Asynchronous': The result is processed in the background.\n" +" It allows longer operations.\n" +" Odoo will provide a URL to the AI system where the response will " +"be sent.\n" +" Users will receive a notification when the operation is started.\n" +" No notification will be sent when it is finished.\n" +" " +msgstr "" +"\n" +" Definisce come il risultato dal sistema IA è elaborato.\n" +" - 'Immediato': il risultato è elaborato immediatamente dopo la " +"risposta della IA.\n" +" - 'Asincrono': il risultato è elaborato in background.\n" +" Consente operazioni più lunghe.\n" +" Odoo fornirà un URL al sistema IA dove la risposta verrà inviata.\n" +" Gli utenti riceveranno una notifica quando si avvia l'operazione.\n" +" Non verrà inviata una notifica alla conclusione.\n" +" " + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s executed successfully." +msgstr "%s eseguito con successo." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s failed." +msgstr "%s fallito." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s is not active." +msgstr "%s non è attivo." + +#. module: ai_oca_bridge +#: model:ir.module.category,name:ai_oca_bridge.module_category_ai +msgid "AI" +msgstr "IA" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_act_window +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_menu +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_root_menu +msgid "AI Bridge" +msgstr "Collegamento IA" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Executed" +msgstr "Collegamento IA eseguito" + +#. module: ai_oca_bridge +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_execution_menu +msgid "AI Bridge Execution" +msgstr "Esecuzione collegamento IA" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Failed" +msgstr "Collegamento IA fallito" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Inactive" +msgstr "Collegamento IA inattivo" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_thread +msgid "AI Bridge Mixin" +msgstr "Mixin bridge IA" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window +msgid "AI Execution" +msgstr "Esecuzione IA" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action +msgid "Action" +msgstr "Azione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "Action Needed" +msgstr "Azione richiesta" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__active +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Active" +msgstr "Attivo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids +msgid "Activities" +msgstr "Attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decorazione eccezione attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_state +msgid "Activity State" +msgstr "Stato attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icona tipo attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__ai_bridge_id +msgid "Ai Bridge" +msgstr "Collegamento IA" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge +msgid "Ai Bridge Configuration" +msgstr "Configurazione collegamento IA" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_discuss_channel__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_cc__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_main_attachment__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_phone__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_phone_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_partner__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_users__ai_bridge_info +msgid "Ai Bridge Info" +msgstr "Informazioni collegamento IA" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_execution +msgid "Ai Execution" +msgstr "Esecuzione IA" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Archived" +msgstr "In archivio" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "Async Timeout" +msgstr "Timeout asincrono" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__async +msgid "Asynchronous" +msgstr "Asincrono" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_attachment_count +msgid "Attachment Count" +msgstr "Conteggio allegati" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_password +msgid "Auth Password" +msgstr "Password autenticazione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_token +msgid "Auth Token" +msgstr "Token autenticazione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_username +msgid "Auth Username" +msgstr "Nome utente autenticazione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_type +msgid "Authentication Type" +msgstr "Tipo autenticazione" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_base +msgid "Base" +msgstr "Base" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic +msgid "Basic Authentication" +msgstr "Autenticazione base" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__company_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__company_id +msgid "Company" +msgstr "Azienda" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage +msgid "" +"Defines how this bridge is used. If 'Thread', it will be used in the mail " +"thread context." +msgstr "" +"Definisce come viene usato questo collegamento. Se \"Discussione\", verrà " +"usato nel contesto della discussione e-mail." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_type +msgid "Defines the type of result expected from the AI system." +msgstr "Definisce il tipo di risultato che ci si aspetta dal sistema IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__description +msgid "Description" +msgstr "Descrizione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__display_name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__done +msgid "Done" +msgstr "Eseguito" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft +msgid "Draft" +msgstr "Bozza" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_mail_thread +msgid "Email Thread" +msgstr "Discussione e-mail" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__error +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__error +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Error" +msgstr "Errore" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids +msgid "Execution" +msgstr "Esecuzione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_count +msgid "Execution Count" +msgstr "Conteggio esecuzioni" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Execution Details" +msgstr "Dettagli esecuzione" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution is expired." +msgstr "L'esecuzione è scaduta." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution not found." +msgstr "Esecuzione non trovata." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration Date" +msgstr "Data di scadenza" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration date for the async operation token." +msgstr "Data scadenza per il token operazione asincrono." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Field" +msgstr "Campo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Fields to include in the AI bridge." +msgstr "Campi da includere nel collegamento IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__domain +msgid "Filter" +msgstr "Filtro" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids +msgid "Followers" +msgstr "Seguito da" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguito da (partner)" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icona Font Awesome es. fa-tasks" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__group_ids +msgid "Group" +msgstr "Gruppo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message +msgid "Has Message" +msgstr "Ha un messaggio" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__id +msgid "ID" +msgstr "ID" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon" +msgstr "Icona" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icona per indicare un'attività eccezione." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Se selezionata, nuovi messaggi richiedono attenzione." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna." + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__immediate +msgid "Immediate" +msgstr "Immediato" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower +msgid "Is Follower" +msgstr "Segue" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "Message Delivery error" +msgstr "Errore di consegna messaggio" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_ids +msgid "Messages" +msgstr "Messaggi" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__model_id +msgid "Model" +msgstr "Modello" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model +msgid "Model Name" +msgstr "Nome modello" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_required +msgid "Model Required" +msgstr "Modello richiesto" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Scadenza mia attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__name +msgid "Name" +msgstr "Nome" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Scadenza prossima attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_summary +msgid "Next Activity Summary" +msgstr "Riepilogo prossima attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo prossima attività" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__none +msgid "No payload" +msgstr "Nessun carico" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none +msgid "No processing" +msgstr "Nessuna elaborazione" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__none +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__none +msgid "None" +msgstr "Nessuno" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of Actions" +msgstr "Numero di azioni" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of errors" +msgstr "Numero di errori" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Numero di messaggi che richiedono un'azione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Numero di messaggi con errore di consegna" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create +msgid "On Record Created" +msgstr "Alla creazione del record" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "On Record Deleted" +msgstr "Alla cancellazione del record" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "On Record Updated" +msgstr "All'aggiornamento del record" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Payload" +msgstr "Carico" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload_txt +msgid "Payload Txt" +msgstr "Testo del carico" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__payload_type +msgid "Payload Type" +msgstr "Tipo carico" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__message +msgid "Post a Message" +msgstr "Invia un messaggio" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record +msgid "Record" +msgstr "Record" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Record Payload" +msgstr "Record carico" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id +msgid "Res" +msgstr "Res" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Response" +msgstr "Risposta" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_user_id +msgid "Responsible User" +msgstr "Utente responsabile" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result +msgid "Result" +msgstr "Risultato" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind +msgid "Result Kind" +msgstr "Genere risultato" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_type +msgid "Result Type" +msgstr "Tipo risultato" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Sample" +msgstr "Esempio" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "Sample Payload" +msgstr "Carico esempio" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "" +"Sample payload to be sent to the AI system. This is used for testing and " +"debugging purposes." +msgstr "" +"Carico esempio da inviare al sistema IA. Questo è usato per attività di test " +"e debug." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state +msgid "State" +msgstr "Stato" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Stato in base alle attività\n" +"Scaduto: la data richiesta è trascorsa\n" +"Oggi: la data attività è oggi\n" +"Pianificato: attività future." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__url +msgid "The URL of the external AI system to which this bridge connects." +msgstr "L'URL del sistema IA esterno a cui questo collegamento fa riferimento." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__model_id +msgid "The model to which this bridge is associated." +msgstr "Modello a cui è associato questo collegamento." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__auth_type +msgid "The type of authentication used to connect to the external AI system." +msgstr "" +"Il tipo di autenticazione utilizzato per connettere il sistema di IA esterno." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__user_id +msgid "The user that will be shown when executing this AI bridge." +msgstr "Utente da visualizzare quando si esegue questo collegamento IA." + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread +msgid "Thread" +msgstr "Discussione" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "" +"Timeout in seconds for asynchronous operations. If the operation does not " +"complete within this time, it will be considered failed." +msgstr "" +"Timeout in secondi per operazioni asincrone. Se l'operazione non si completa " +"entro questo tempo, verrà considerata fallita." + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token +msgid "Token Authentication" +msgstr "Autenticazione token" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Token is not allowed for this execution." +msgstr "Il token non è autorizzato per questa esecuzione." + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo di attività eccezione sul record." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__url +msgid "URL" +msgstr "URL" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 +msgid "Unsupported authentication type." +msgstr "Tipo autenticazione non supportato." + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__usage +msgid "Usage" +msgstr "Uilizzo" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id +msgid "User" +msgstr "Utente" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__group_ids +msgid "User groups allowed to use this AI bridge." +msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "" +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." +msgstr "" +"Quando si utilizza 'Alla cancellazione del record', il tipo contenuto deve " +"essere 'Nessun contenuto'." + +#~ msgid "AI Thread Create" +#~ msgstr "Creazione thread IA" + +#~ msgid "AI Thread Unlink" +#~ msgstr "Rilascio thread IA" + +#~ msgid "AI Thread Write" +#~ msgstr "Scrittura thread IA" + +#~ msgid "Ai Usage" +#~ msgstr "Utilizzo IA" + +#~ msgid "Is Ai Bridge Thread" +#~ msgstr "È un thread IA" + +#~ msgid "Models" +#~ msgstr "Modelli" + +#~ msgid "" +#~ "When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +#~ msgstr "" +#~ "Quando l'utilizzo è 'Rilascio thread IA', il tipo di carico deve essere " +#~ "'Nessun carico'." + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" + +#~ msgid "Main Attachment" +#~ msgstr "Allegato principale" + +#~ msgid "" +#~ "Timeout in seconds for asynchronous operations. If the operation does not " +#~ "complete within this time, it will be considered failed." +#~ msgstr "" +#~ "Timeout in secondi per operazioni asincrone. Se l'operazione non si " +#~ "completa entro questo tempo, verrà considerata fallita." + +#~ msgid "Website Messages" +#~ msgstr "Messaggi sito web" + +#~ msgid "Website communication history" +#~ msgstr "Cronologia comunicazioni sito web" diff --git a/ai_oca_bridge/i18n/pt_BR.po b/ai_oca_bridge/i18n/pt_BR.po new file mode 100644 index 0000000..288b399 --- /dev/null +++ b/ai_oca_bridge/i18n/pt_BR.po @@ -0,0 +1,681 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * ai_oca_bridge +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_kind +msgid "" +"\n" +" Defines how the result from the AI system is processed.\n" +" - 'Immediate': The result is processed immediately after the AI " +"system responds.\n" +" - 'Asynchronous': The result is processed in the background.\n" +" It allows longer operations.\n" +" Odoo will provide a URL to the AI system where the response will " +"be sent.\n" +" Users will receive a notification when the operation is started.\n" +" No notification will be sent when it is finished.\n" +" " +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s executed successfully." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s failed." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "%s is not active." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.module.category,name:ai_oca_bridge.module_category_ai +msgid "AI" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_act_window +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_menu +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_root_menu +msgid "AI Bridge" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Executed" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_execution_menu +msgid "AI Bridge Execution" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Failed" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "AI Bridge Inactive" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_thread +msgid "AI Bridge Mixin" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window +msgid "AI Execution" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action +msgid "Action" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__active +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Active" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids +msgid "Activities" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_state +msgid "Activity State" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__ai_bridge_id +msgid "Ai Bridge" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge +msgid "Ai Bridge Configuration" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_discuss_channel__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_cc__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_main_attachment__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread_phone__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_phone_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_partner__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_res_users__ai_bridge_info +msgid "Ai Bridge Info" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ai_bridge_execution +msgid "Ai Execution" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_search_view +msgid "Archived" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "Async Timeout" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__async +msgid "Asynchronous" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_password +msgid "Auth Password" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_token +msgid "Auth Token" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_username +msgid "Auth Username" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__auth_type +msgid "Authentication Type" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_base +msgid "Base" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic +msgid "Basic Authentication" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__company_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__company_id +msgid "Company" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_uid +msgid "Created by" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__create_date +msgid "Created on" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage +msgid "" +"Defines how this bridge is used. If 'Thread', it will be used in the mail " +"thread context." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__result_type +msgid "Defines the type of result expected from the AI system." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__description +msgid "Description" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__display_name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__display_name +msgid "Display Name" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__done +msgid "Done" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft +msgid "Draft" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_mail_thread +msgid "Email Thread" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__error +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__error +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Error" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids +msgid "Execution" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_count +msgid "Execution Count" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Execution Details" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution is expired." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Execution not found." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration Date" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge_execution__expiration_date +msgid "Expiration date for the async operation token." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Field" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__field_ids +msgid "Fields to include in the AI bridge." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__domain +msgid "Filter" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__group_ids +msgid "Group" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message +msgid "Has Message" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__id +msgid "ID" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_kind__immediate +msgid "Immediate" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_uid +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__write_date +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__write_date +msgid "Last Updated on" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_ids +msgid "Messages" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_id +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__model_id +msgid "Model" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model +msgid "Model Name" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model_required +msgid "Model Required" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__name +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__name +msgid "Name" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__none +msgid "No payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none +msgid "No processing" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__none +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__none +msgid "None" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create +msgid "On Record Created" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "On Record Deleted" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "On Record Updated" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload_txt +msgid "Payload Txt" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__payload_type +msgid "Payload Type" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__message +msgid "Post a Message" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record +msgid "Record" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Record Payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id +msgid "Res" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view +msgid "Response" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result +msgid "Result" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind +msgid "Result Kind" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_type +msgid "Result Type" +msgstr "" + +#. module: ai_oca_bridge +#: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view +msgid "Sample" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "Sample Payload" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload +msgid "" +"Sample payload to be sent to the AI system. This is used for testing and " +"debugging purposes." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sequence +msgid "Sequence" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state +msgid "State" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__url +msgid "The URL of the external AI system to which this bridge connects." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__model_id +msgid "The model to which this bridge is associated." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__auth_type +msgid "The type of authentication used to connect to the external AI system." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__user_id +msgid "The user that will be shown when executing this AI bridge." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread +msgid "Thread" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__async_timeout +msgid "" +"Timeout in seconds for asynchronous operations. If the operation does not " +"complete within this time, it will be considered failed." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token +msgid "Token Authentication" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +msgid "Token is not allowed for this execution." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__url +msgid "URL" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 +msgid "Unsupported authentication type." +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__usage +msgid "Usage" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id +msgid "User" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__group_ids +msgid "User groups allowed to use this AI bridge." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +msgid "" +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." +msgstr "" diff --git a/ai_oca_bridge/models/__init__.py b/ai_oca_bridge/models/__init__.py new file mode 100644 index 0000000..a2e52e2 --- /dev/null +++ b/ai_oca_bridge/models/__init__.py @@ -0,0 +1,5 @@ +from . import ai_bridge_thread +from . import ai_bridge +from . import ai_bridge_execution +from . import mail_thread +from . import base diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py new file mode 100644 index 0000000..954bd9b --- /dev/null +++ b/ai_oca_bridge/models/ai_bridge.py @@ -0,0 +1,336 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import json +import logging +from datetime import date, datetime + +from odoo import api, fields, models +from odoo.exceptions import ValidationError +from odoo.fields import Domain +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + + +class AiBridge(models.Model): + _name = "ai.bridge" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Ai Bridge Configuration" + _order = "sequence, id" + + sequence = fields.Integer( + default=10, + ) + company_id = fields.Many2one( + "res.company", + # We leave it empty to allow multiple companies to use the same bridge. + ) + usage = fields.Selection( + [ + ("none", "None"), + ("thread", "Thread"), + ("ai_thread_create", "On Record Created"), + ("ai_thread_write", "On Record Updated"), + ("ai_thread_unlink", "On Record Deleted"), + ], + default="none", + help="Defines how this bridge is used. " + "If 'Thread', it will be used in the mail thread context.", + ) + name = fields.Char(required=True, translate=True) + active = fields.Boolean(default=True) + description = fields.Html(translate=True) + user_id = fields.Many2one( + "res.users", + default=lambda self: self.env.user, + help="The user that will be shown when executing this AI bridge.", + ) + payload_type = fields.Selection( + [ + ("none", "No payload"), + ("record", "Record"), + ], + required=True, + default="record", + ) + result_type = fields.Selection( + [ + ("none", "No processing"), + ("message", "Post a Message"), + ("action", "Action"), + ("server_action", "Run a Server Action"), + ], + required=True, + default="none", + help="Defines the type of result expected from the AI system.", + ) + result_kind = fields.Selection( + [("immediate", "Immediate"), ("async", "Asynchronous")], + default="immediate", + help=""" + Defines how the result from the AI system is processed. + - 'Immediate': The result is processed immediately after the AI system responds. + - 'Asynchronous': The result is processed in the background. + It allows longer operations. + Odoo will provide a URL to the AI system where the response will be sent. + Users will receive a notification when the operation is started. + No notification will be sent when it is finished. + """, + ) + async_timeout = fields.Integer( + default=300, + help="Timeout in seconds for asynchronous operations. " + "If the operation does not complete within this time,\ + it will be considered failed.", + ) + execution_ids = fields.One2many("ai.bridge.execution", "ai_bridge_id") + execution_count = fields.Integer( + compute="_compute_execution_count", + ) + url = fields.Char( + string="URL", + help="The URL of the external AI system to which this bridge connects.", + ) + auth_type = fields.Selection( + selection=[ + ("none", "None"), + ("basic", "Basic Authentication"), + ("token", "Token Authentication"), + ], + default="none", + string="Authentication Type", + help="The type of authentication used to connect to the external AI system.", + ) + auth_username = fields.Char(groups="base.group_system") + auth_password = fields.Char(groups="base.group_system") + auth_token = fields.Char(groups="base.group_system") + group_ids = fields.Many2many( + "res.groups", + help="User groups allowed to use this AI bridge.", + ) + sample_payload = fields.Text( + help="Sample payload to be sent to the AI system. " + "This is used for testing and debugging purposes.", + compute="_compute_sample_payload", + ) + model_id = fields.Many2one( + "ir.model", + string="Model", + required=False, + ondelete="cascade", + help="The model to which this bridge is associated.", + ) + model_required = fields.Boolean(compute="_compute_model_fields") + server_action_id = fields.Many2one( + "ir.actions.server", + string="Server Action", + compute="_compute_server_action_id", + readonly=False, + store=True, + ) + trigger_field_ids = fields.Many2many( + "ir.model.fields", + relation="ai_bridge_trigger_ir_model_fields_rel", + column1="bridge_id", + column2="field_id", + compute="_compute_trigger_field_ids", + store=True, + readonly=False, + string="Trigger Fields", + help=( + "When set and usage is 'On Record Updated', the bridge will only " + "trigger if any of these fields are present in the write values." + ), + ) + + ####################################### + # Payload type 'record' specific fields + ####################################### + + field_ids = fields.Many2many( + "ir.model.fields", + help="Fields to include in the AI bridge.", + compute="_compute_field_ids", + store=True, + readonly=False, + ) + model = fields.Char( + related="model_id.model", + string="Model Name", + ) + domain = fields.Char( + string="Filter", compute="_compute_domain", readonly=False, store=True + ) + + @api.onchange("usage") + def _compute_payload_type(self): + for record in self: + if record.usage == "ai_thread_unlink": + record.payload_type = "none" + + @api.constrains("usage", "payload_type") + def _check_payload_type_usage_compatibility(self): + for record in self: + if record.usage == "ai_thread_unlink" and record.payload_type != "none": + raise ValidationError( + self.env._( + "When usage is 'On Record Deleted', " + "the Payload Type must be 'No payload'." + ) + ) + + @api.depends("usage") + def _compute_model_fields(self): + for record in self: + record.update(record._get_model_fields()) + + def _get_model_fields(self): + if self.usage == "thread": + return { + "model_required": True, + } + if self.usage in ["ai_thread_create", "ai_thread_write", "ai_thread_unlink"]: + return { + "model_required": True, + } + return { + "model_required": False, + } + + @api.depends("model_id") + def _compute_domain(self): + for record in self: + record.domain = "[]" + + @api.depends("model_id") + def _compute_field_ids(self): + for record in self: + record.field_ids = False + + @api.depends("model_id") + def _compute_trigger_field_ids(self): + for record in self: + record.trigger_field_ids = False + + @api.depends("result_type") + def _compute_server_action_id(self): + for record in self: + record.server_action_id = False + + @api.depends("field_ids", "model_id", "payload_type") + def _compute_sample_payload(self): + for record in self: + record.sample_payload = json.dumps( + record.with_context(sample_payload=True)._prepare_payload(), indent=4 + ) + + @api.depends("execution_ids") + def _compute_execution_count(self): + for record in self: + record.execution_count = len(record.execution_ids) + + def _get_info(self): + return {"id": self.id, "name": self.name, "description": self.description} + + def execute_ai_bridge(self, res_model, res_id): + self.ensure_one() + if not self.active or ( + self.group_ids and not self.env.user.group_ids & self.group_ids + ): + return { + "body": self.env._("%s is not active.", self.name), + "args": {"type": "warning", "title": self.env._("AI Bridge Inactive")}, + } + record = self.env[res_model].browse(res_id).exists() + if record: + execution = ( + self.env["ai.bridge.execution"] + .sudo() + .create( + { + "ai_bridge_id": self.id, + "model_id": self.sudo().env["ir.model"]._get_id(res_model), + "res_id": res_id, + } + ) + ) + result = execution._execute() + if result: + return result + if execution.state == "done": + return { + "notification": { + "body": self.env._("%s executed successfully.", self.name), + "args": { + "type": "success", + "title": self.env._("AI Bridge Executed"), + }, + } + } + return { + "notification": { + "body": self.env._("%s failed.", self.name), + "args": { + "type": "danger", + "title": self.env._("AI Bridge Failed"), + }, + } + } + + def _enabled_for(self, record): + """Check if the bridge is enabled for the given record.""" + self.ensure_one() + domain_list = safe_eval(self.domain or "[]") + domain = Domain(domain_list) + if self.group_ids and not self.env.user.group_ids & self.group_ids: + return False + if domain_list: + return bool(record.filtered_domain(domain)) + return True + + def _prepare_payload(self, **kwargs): + method = getattr(self, f"_prepare_payload_{self.payload_type}", None) + if not method: + raise ValueError( + f"Unsupported payload type: {self.payload_type}. " + "Please implement a method for this payload type." + ) + return method(**kwargs) + + def _prepare_payload_none(self, res_model=False, res_id=False, **kwargs): + return { + "_model": res_model, + "_id": res_id, + } + + def _prepare_payload_record(self, record=None, **kwargs): + """Prepare the payload to be sent to the AI system.""" + self.ensure_one() + if not self.model_id: + return {} + if record is None and self.env.context.get("sample_payload"): + record = self.env[self.model_id.model].search([], limit=1) + if not record: + return {} + vals = {} + if self.sudo().field_ids: + vals = record.read(self.sudo().field_ids.mapped("name"))[0] + return json.loads( + json.dumps( + { + "record": vals, + "_model": record._name, + "_id": record.id, + }, + default=self.custom_serializer, + ) + ) + + def custom_serializer(self, obj): + if isinstance(obj, datetime) or isinstance(obj, date): + return obj.isoformat() + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + raise TypeError(f"Type {type(obj)} not serializable") diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py new file mode 100644 index 0000000..d8074a5 --- /dev/null +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -0,0 +1,245 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +import traceback +from datetime import timedelta +from io import StringIO + +import requests + +from odoo import api, fields, models, tools +from odoo.tools import urls + + +class AiBridgeExecution(models.Model): + _name = "ai.bridge.execution" + _description = "Ai Execution" + _order = "id desc" + + name = fields.Char( + store=True, + compute="_compute_name", + ) + + ai_bridge_id = fields.Many2one( + "ai.bridge", + required=True, + ondelete="cascade", + ) + res_id = fields.Integer(required=False) + state = fields.Selection( + [ + ("draft", "Draft"), + ("done", "Done"), + ("error", "Error"), + ], + default="draft", + required=True, + ) + model_id = fields.Many2one( + "ir.model", + required=False, + ondelete="cascade", + ) + payload = fields.Json(readonly=True) + payload_txt = fields.Text( + compute="_compute_payload_txt", + ) + result = fields.Text(readonly=True) + error = fields.Text(readonly=True) + company_id = fields.Many2one( + "res.company", + compute="_compute_company_id", + store=True, + readonly=True, + ) + expiration_date = fields.Datetime( + readonly=True, + help="Expiration date for the async operation token.", + ) + + @api.depends("model_id", "res_id", "ai_bridge_id") + def _compute_name(self): + for record in self: + model = record.sudo().model_id.name or "Unknown Model" + related = self.env[record.sudo().model_id.model].browse(record.res_id) + record.name = ( + f"{model} - {related.display_name} - {record.ai_bridge_id.name}" + ) + + @api.depends("payload") + def _compute_payload_txt(self): + for record in self: + if record.payload: + try: + record.payload_txt = json.dumps(record.payload, indent=4) + except (TypeError, ValueError): + record.payload_txt = str(record.payload) + else: + record.payload_txt = "" + + @api.depends("ai_bridge_id") + def _compute_company_id(self): + for record in self: + record.company_id = record.ai_bridge_id.company_id + + def _add_extra_payload_fields(self, payload): + """Add extra fields to the payload if needed.""" + self.ensure_one() + if self.ai_bridge_id.result_kind == "async": + self.expiration_date = fields.Datetime.now() + timedelta( + seconds=self.ai_bridge_id.async_timeout + ) + token = self._generate_token() + payload["_response_url"] = urls.urljoin( + self.get_base_url(), f"/ai/response/{self.id}/{token}" + ) + IrParamSudo = self.env["ir.config_parameter"].sudo() + dbuuid = IrParamSudo.get_param("database.uuid") + db_create_date = IrParamSudo.get_param("database.create_date") + payload["_odoo"] = { + "db": dbuuid, + "db_name": self.env.cr.dbname, + "db_hash": tools.hmac( + self.env(su=True), + "database-hash", + (dbuuid, db_create_date, self.env.cr.dbname), + ), + "user_id": self.env.user.id, + } + return payload + + def _execute(self, **kwargs): + self.ensure_one() + record = None + if self.res_id and self.model_id: + record = self.env[self.sudo().model_id.model].browse(self.res_id) + payload = self.ai_bridge_id._prepare_payload( + record=record, + res_id=self.res_id, + model=self.sudo().model_id.model, + **kwargs, + ) + payload = self._add_extra_payload_fields(payload) + try: + response = requests.post( + self.ai_bridge_id.url, + json=payload, + auth=self._get_auth(), + headers=self._get_headers(), + timeout=30, # Default timeout, can be overridden by _execute_kwargs + **self._execute_kwargs(**kwargs), + ) + self.result = response.content + response.raise_for_status() + self.state = "done" + self.payload = payload + if self.ai_bridge_id.result_kind == "immediate": + return self._process_response(response.json()) + except Exception: + self.state = "error" + self.payload = payload + buff = StringIO() + traceback.print_exc(file=buff) + self.error = buff.getvalue() + buff.close() + + def _execute_kwargs(self, timeout=False, **kwargs): + self.ensure_one() + result = {} + if timeout: + result["timeout"] = timeout + return result + + def _get_auth(self): + """Return authentication for the request.""" + if self.ai_bridge_id.auth_type in ["none", "token"]: + # Token auth is handled in _get_headers + return None + elif self.ai_bridge_id.auth_type == "basic": + return ( + self.ai_bridge_id.sudo().auth_username, + self.ai_bridge_id.sudo().auth_password, + ) + else: + raise ValueError(self.env._("Unsupported authentication type.")) + + def _get_headers(self): + """Return headers for the request.""" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + if self.ai_bridge_id.auth_type == "token": + headers.update( + {"Authorization": f"Bearer {self.ai_bridge_id.sudo().auth_token}"} + ) + return headers + + def _generate_token(self): + """Generate a token for async operations.""" + self.ensure_one() + return tools.hmac( + self.env(su=True), + "ai_bridge-access_token", + ( + self.id, + self.expiration_date and self.expiration_date.isoformat() or "expired", + ), + ) + + def _process_response(self, response): + """Process the response from the AI bridge.""" + self.ensure_one() + self.expiration_date = None + return getattr( + self.with_user(self.ai_bridge_id.user_id.id), + f"_process_response_{self.ai_bridge_id.result_type}", + self._process_response_none, + )(response) + + def _process_response_none(self, response): + return {} + + def _process_response_message(self, response): + return {"id": self._get_channel().message_post(**response).id} + + def _process_response_action(self, response): + if response.get("action"): + action = self.env["ir.actions.actions"]._for_xml_id(response["action"]) + if response.get("context"): + action["context"] = response["context"] + if response.get("res_id"): + action["res_id"] = response["res_id"] + return {"action": action} + return {} + + def _process_response_server_action(self, response): + action = self.ai_bridge_id.server_action_id + + if not action: + return {"warning": "The Server Action to run has not been defined."} + + model_name = self.sudo().model_id.model + if not model_name: + return {"warning": "No model found for this execution."} + + ctx = dict( + self.env.context, + **{ + "active_id": self.res_id, + "active_ids": [self.res_id], + "active_model": model_name, + "ai_response": response, + }, + ) + + action.with_context(**ctx).run() + + return {"status": "success", "action": action.name} + + def _get_channel(self): + if self.model_id and self.res_id: + return self.env[self.model_id.model].browse(self.res_id) + return None diff --git a/ai_oca_bridge/models/ai_bridge_thread.py b/ai_oca_bridge/models/ai_bridge_thread.py new file mode 100644 index 0000000..8420df2 --- /dev/null +++ b/ai_oca_bridge/models/ai_bridge_thread.py @@ -0,0 +1,16 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AiBridgeThread(models.AbstractModel): + _name = "ai.bridge.thread" + _description = "AI Bridge Mixin" + + # This is an empty shell model maintained for backward compatibility. + # It prevents other modules from crashing, after improvements to the main + # AI bridge implementation made this model obsolete. + # + # This model can be safely removed once all dependent modules have been + # updated to use the new AI bridge architecture (e.g. v19+). diff --git a/ai_oca_bridge/models/base.py b/ai_oca_bridge/models/base.py new file mode 100644 index 0000000..d541b0d --- /dev/null +++ b/ai_oca_bridge/models/base.py @@ -0,0 +1,101 @@ +# Copyright 2025 Dixmit +# Copyright 2025 Binhex - Ariel Barreiros +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, models +from odoo.fields import Domain + +_logger = logging.getLogger(__name__) + + +class Base(models.AbstractModel): + _inherit = "base" + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + self._execute_ai_bridges_for_records(records, "ai_thread_create") + return records + + def write(self, values): + result = super().write(values) + self._execute_ai_bridges_for_records(self, "ai_thread_write", values=values) + return result + + def unlink(self): + executions = self.env["ai.bridge.execution"] + executions = self._prepare_execution_ai_bridges_unlink(self) + result = super().unlink() + + for execution in executions: + try: + execution._execute() + except Exception as e: + _logger.error( + f"Error executing AI bridge {execution.ai_bridge_id.name} " + f"for unlink on record ID {execution.res_id}: {e}" + ) + + return result + + def _execute_ai_bridges_for_records(self, records, usage, values=None): + if not records: + return + model_id = self.sudo().env["ir.model"]._get_id(records._name) + bridges = ( + self.env["ai.bridge"] + .sudo() + .search( + Domain.AND( + [Domain("model_id", "=", model_id), Domain("usage", "=", usage)] + ) + ) + ) + for bridge in bridges: + # If this is a write, and the bridge has trigger fields configured, + # only proceed when at least one of those fields is present in values + if usage == "ai_thread_write" and values is not None: + trigger_fields = bridge.sudo().trigger_field_ids + if trigger_fields: + trigger_names = set(trigger_fields.mapped("name")) + if trigger_names.isdisjoint(values.keys()): + continue + for record in records: + if bridge._enabled_for(record): + try: + bridge.execute_ai_bridge(record._name, record.id) + except Exception as e: + _logger.error( + f"Error executing AI bridge {bridge.name} " + f"for {usage} on {record}: {e}" + ) + + def _prepare_execution_ai_bridges_unlink(self, records): + if not records: + return self.env["ai.bridge.execution"] + + model_id = self.env["ir.model"]._get_id(records._name) + bridges = self.env["ai.bridge"].search( + Domain.AND( + [ + Domain("model_id", "=", model_id), + Domain("usage", "=", "ai_thread_unlink"), + ] + ) + ) + + executions = self.env["ai.bridge.execution"] + for bridge in bridges: + for record in records: + if bridge._enabled_for(record): + executions |= self.env["ai.bridge.execution"].create( + { + "ai_bridge_id": bridge.id, + "model_id": model_id, + "res_id": record.id, + } + ) + + return executions diff --git a/ai_oca_bridge/models/mail_thread.py b/ai_oca_bridge/models/mail_thread.py new file mode 100644 index 0000000..97a0716 --- /dev/null +++ b/ai_oca_bridge/models/mail_thread.py @@ -0,0 +1,75 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from lxml import etree + +from odoo import api, fields, models +from odoo.fields import Domain +from odoo.tools.misc import frozendict + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + ai_bridge_info = fields.Json(compute="_compute_ai_bridge_info", store=False) + + @api.depends() + def _compute_ai_bridge_info(self): + for record in self: + record.ai_bridge_info = [ + bridge._get_info() for bridge in record._get_ai_bridge_info() + ] + + def _get_ai_bridge_info(self): + self.ensure_one() + model_id = ( + self.env["ir.model"].sudo().search(Domain("model", "=", self._name)).id + ) + return ( + self.env["ai.bridge"] + .search( + Domain.AND( + [Domain("model_id", "=", model_id), Domain("usage", "=", "thread")] + ) + ) + .filtered(lambda r: r._enabled_for(self)) + ) + + @api.model + def get_view(self, view_id=None, view_type="form", **options): + res = super().get_view(view_id=view_id, view_type=view_type, **options) + if view_type == "form": + View = self.env["ir.ui.view"] + if view_id and res.get("base_model", self._name) != self._name: + View = View.with_context(base_model_name=res["base_model"]) + doc = etree.XML(res["arch"]) + + # We need to copy, because it is a frozen dict + all_models = res["models"].copy() + for node in doc.xpath("/form/chatter"): + # _add_tier_validation_label process + new_node = etree.fromstring( + "" + ) + new_arch, new_models = View.postprocess_and_fields(new_node, self._name) + new_node = etree.fromstring(new_arch) + for model in list(filter(lambda x: x not in all_models, new_models)): + if model not in res["models"]: + all_models[model] = new_models[model] + else: + all_models[model] = res["models"][model] + node.addprevious(new_node) + res["arch"] = etree.tostring(doc) + res["models"] = frozendict(all_models) + return res + + @api.model + def _get_view_fields(self, view_type, models): + """ + We need to add this in order to fix the usage of form opening from + lists inside a form + """ + result = super()._get_view_fields(view_type, models) + if view_type == "form": + result[self._name].add("ai_bridge_info") + return result diff --git a/ai_oca_bridge/pyproject.toml b/ai_oca_bridge/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/ai_oca_bridge/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/ai_oca_bridge/readme/CONFIGURE.md b/ai_oca_bridge/readme/CONFIGURE.md new file mode 100644 index 0000000..5a509b1 --- /dev/null +++ b/ai_oca_bridge/readme/CONFIGURE.md @@ -0,0 +1,63 @@ +As an administrator access `AI Bridge\AI Bridge`. + +Create a new bridge. +Define the name, model, url and configuration. + +In order to improve the view of the AI configuration, use groups and domain to set better filters. + +## Payload Configuration + +On the external system, you will receive a POST payload. The data included will be the following: + +### General + +- _odoo: Standard data to identify the Odoo Database +- _model: Model of the related object +- _id: Id of the related object +- _response_url: Url to call with the response in case of async calls + +### Record Payload + + +Adds a new item called record with all the fields. + +## Asynchronous and synchronous calls + +The new system allows asynchronous and synchronous calls. +Asynchronous calls makes sense when the task to be processed don't need to be immediate. +For example, reviewing an invoice and leave a comment with the result. +The same would happen with a chat message. +We expect that the system will leave time to the AI to answer and Odoo's user can do other things. + +Meanwhile, Synchronous calls will froze odoo system and wait for an answer. +This makes sense when we expect some feedback from odoo user. +It makes sense, when we open an action for example. + +In the synchronous call, the result is processed when the AI system answers on the webhook. +On the other hand, it will be processed automatically on the synchronous call. + +## Result processing + +With the answers of the system we expect to do something about it. +We have the following options: + +### No processing + +In this case, the result will do nothing + +### Post a Message + +We will post a message on the original thread of the system. +The thread is computed by a function, so it can be overriden in future modules. +It expects the keyword arguments of the `message_post` function. + +### Action + +It expects to launch an action on the user interface. +It only makes sense on synchronous calls. + +It expects an action item with the following parameters: + +- action: xmlid of the action +- context: Context to pass to the action (not required) +- res_id: Id of the resource (not required) diff --git a/ai_oca_bridge/readme/CONTEXT.md b/ai_oca_bridge/readme/CONTEXT.md new file mode 100644 index 0000000..84deea9 --- /dev/null +++ b/ai_oca_bridge/readme/CONTEXT.md @@ -0,0 +1,12 @@ +Right now, there are 2 different approaches for AI integration with Odoo: + +1. Make everything inside Odoo. +2. Make it using other tools and integrate Odoo with these tools. + +IMO, it would be better to make use of option 2 for different reasons: + +- Odoo server is intended as a transactional system. AI systems requires other kind of characteristics +- Everything changes too fast. I am not confident that Odoo can keep the pace in this topic +- There are OSS tools that fills the gap perfectly and are created just for this topic. + +Anyway, OCA is open to everyone and we don't intend to force an opinionated way of doing. For this reason, we have this module, that can be used as Bridge with AI systems. diff --git a/ai_oca_bridge/readme/CONTRIBUTORS.md b/ai_oca_bridge/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..12702fd --- /dev/null +++ b/ai_oca_bridge/readme/CONTRIBUTORS.md @@ -0,0 +1,12 @@ +- [Dixmit](https://www.dixmit.com) + + - Enric Tobella + +- [Sygel Technology](https://www.sygel.es) + + - Valentín Vinagre + +- [Binhex](https://www.binhex.cloud/) + + - Ariel Barreiros + - Adria Hortoneda diff --git a/ai_oca_bridge/readme/DESCRIPTION.md b/ai_oca_bridge/readme/DESCRIPTION.md new file mode 100644 index 0000000..0ebfc86 --- /dev/null +++ b/ai_oca_bridge/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module is used to create a bridge between Odoo and other AI systems like n8n. diff --git a/ai_oca_bridge/readme/ROADMAP.md b/ai_oca_bridge/readme/ROADMAP.md new file mode 100644 index 0000000..a95064f --- /dev/null +++ b/ai_oca_bridge/readme/ROADMAP.md @@ -0,0 +1,3 @@ +- Define examples to use and import +- Allow child fields. Right now, only first level fields are accepted. +- Information popover is not working properly when there is large data. diff --git a/ai_oca_bridge/readme/USAGE.md b/ai_oca_bridge/readme/USAGE.md new file mode 100644 index 0000000..8aa474f --- /dev/null +++ b/ai_oca_bridge/readme/USAGE.md @@ -0,0 +1,3 @@ +Use the bolt widget in the chatter to execute the different AI options. + +The options will be filtered according to the configuration. diff --git a/ai_oca_bridge/security/ir.model.access.csv b/ai_oca_bridge/security/ir.model.access.csv new file mode 100644 index 0000000..9e8a094 --- /dev/null +++ b/ai_oca_bridge/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ai_bridge_user,ai_bridge.user,model_ai_bridge,base.group_user,1,0,0,0 +access_ai_bridge_manager,ai_bridge.manager,model_ai_bridge,base.group_system,1,1,1,0 +access_ai_bridge_execution_user,ai_bridge.user,model_ai_bridge_execution,base.group_user,1,0,0,0 diff --git a/ai_oca_bridge/security/security.xml b/ai_oca_bridge/security/security.xml new file mode 100644 index 0000000..fa72c29 --- /dev/null +++ b/ai_oca_bridge/security/security.xml @@ -0,0 +1,19 @@ + + + + AI Bridge Multi Company Rule + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + + + AI Bridge Multi Company Rule + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + + diff --git a/ai_oca_bridge/static/description/icon.png b/ai_oca_bridge/static/description/icon.png new file mode 100644 index 0000000..4adee3c Binary files /dev/null and b/ai_oca_bridge/static/description/icon.png differ diff --git a/ai_oca_bridge/static/description/icon.svg b/ai_oca_bridge/static/description/icon.svg new file mode 100644 index 0000000..90cc269 --- /dev/null +++ b/ai_oca_bridge/static/description/icon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html new file mode 100644 index 0000000..fe694d1 --- /dev/null +++ b/ai_oca_bridge/static/description/index.html @@ -0,0 +1,560 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

AI OCA Bridge

+ +

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

+

This module is used to create a bridge between Odoo and other AI systems +like n8n.

+

Table of contents

+ +
+

Use Cases / Context

+

Right now, there are 2 different approaches for AI integration with +Odoo:

+
    +
  1. Make everything inside Odoo.
  2. +
  3. Make it using other tools and integrate Odoo with these tools.
  4. +
+

IMO, it would be better to make use of option 2 for different reasons:

+
    +
  • Odoo server is intended as a transactional system. AI systems requires +other kind of characteristics
  • +
  • Everything changes too fast. I am not confident that Odoo can keep the +pace in this topic
  • +
  • There are OSS tools that fills the gap perfectly and are created just +for this topic.
  • +
+

Anyway, OCA is open to everyone and we don’t intend to force an +opinionated way of doing. For this reason, we have this module, that can +be used as Bridge with AI systems.

+
+
+

Configuration

+

As an administrator access AI Bridge\AI Bridge.

+

Create a new bridge. Define the name, model, url and configuration.

+

In order to improve the view of the AI configuration, use groups and +domain to set better filters.

+
+

Payload Configuration

+

On the external system, you will receive a POST payload. The data +included will be the following:

+
+

General

+
    +
  • _odoo: Standard data to identify the Odoo Database
  • +
  • _model: Model of the related object
  • +
  • _id: Id of the related object
  • +
  • _response_url: Url to call with the response in case of async calls
  • +
+
+
+

Record Payload

+

Adds a new item called record with all the fields.

+
+
+
+

Asynchronous and synchronous calls

+

The new system allows asynchronous and synchronous calls. Asynchronous +calls makes sense when the task to be processed don’t need to be +immediate. For example, reviewing an invoice and leave a comment with +the result. The same would happen with a chat message. We expect that +the system will leave time to the AI to answer and Odoo’s user can do +other things.

+

Meanwhile, Synchronous calls will froze odoo system and wait for an +answer. This makes sense when we expect some feedback from odoo user. It +makes sense, when we open an action for example.

+

In the synchronous call, the result is processed when the AI system +answers on the webhook. On the other hand, it will be processed +automatically on the synchronous call.

+
+
+

Result processing

+

With the answers of the system we expect to do something about it. We +have the following options:

+
+

No processing

+

In this case, the result will do nothing

+
+
+

Post a Message

+

We will post a message on the original thread of the system. The thread +is computed by a function, so it can be overriden in future modules. It +expects the keyword arguments of the message_post function.

+
+
+

Action

+

It expects to launch an action on the user interface. It only makes +sense on synchronous calls.

+

It expects an action item with the following parameters:

+
    +
  • action: xmlid of the action
  • +
  • context: Context to pass to the action (not required)
  • +
  • res_id: Id of the resource (not required)
  • +
+
+
+
+
+

Usage

+

Use the bolt widget in the chatter to execute the different AI options.

+

The options will be filtered according to the configuration.

+
+
+

Known issues / Roadmap

+
    +
  • Define examples to use and import
  • +
  • Allow child fields. Right now, only first level fields are accepted.
  • +
  • Information popover is not working properly when there is large data.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/ai project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/ai_oca_bridge/static/src/chatter_ai_registry.esm.js b/ai_oca_bridge/static/src/chatter_ai_registry.esm.js new file mode 100644 index 0000000..a674114 --- /dev/null +++ b/ai_oca_bridge/static/src/chatter_ai_registry.esm.js @@ -0,0 +1,2 @@ +import "./components/chatter_topbar_ai/chatter_topbar_ai.esm"; +import "./components/chatter_topbar_ai_item/chatter_topbar_ai_item.esm"; diff --git a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js new file mode 100644 index 0000000..b1d6f65 --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js @@ -0,0 +1,43 @@ +import {Chatter} from "@mail/chatter/web_portal/chatter"; +import {patch} from "@web/core/utils/patch"; + +patch(Chatter.prototype, { + async onClickAiBridge(aiBridge) { + let saved = true; + + if (this.props.record && this.props.record.save) { + try { + await this.props.record.save(); + } catch (error) { + saved = false; + console.error("Error saving record:", error); + } + } + + if (!saved) { + return; + } + + const model = this.props.record.resModel; + const id = this.props.record.resId; + + const result = await this.env.services.orm.call( + "ai.bridge", + "execute_ai_bridge", + [[aiBridge.id], model, id] + ); + + if (result.action && this.env.services && this.env.services.action) { + this.env.services.action.doAction(result.action); + } else if ( + result.notification && + this.env.services && + this.env.services.notification + ) { + this.env.services.notification.add( + result.notification.body, + result.notification.args + ); + } + }, +}); diff --git a/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml b/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml new file mode 100644 index 0000000..ef2e1cd --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.esm.js b/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.esm.js new file mode 100644 index 0000000..056ce8c --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.esm.js @@ -0,0 +1,26 @@ +import {Chatter} from "@mail/chatter/web_portal/chatter"; +import {ChatterAIItem} from "../chatter_topbar_ai_item/chatter_topbar_ai_item.esm"; +import {Component} from "@odoo/owl"; +import {Dropdown} from "@web/core/dropdown/dropdown"; +import {DropdownItem} from "@web/core/dropdown/dropdown_item"; +import {patch} from "@web/core/utils/patch"; + +export class ChatterAITopbar extends Component { + static template = "ai_oca_bridge.ChatterAITopbar"; + static components = {Dropdown, DropdownItem, ChatterAIItem}; + static props = {record: Object}; + + /** + * @returns {ChatterAITopbar} + */ + get chatterTopbar() { + return this.props.record; + } +} + +patch(Chatter, { + components: { + ...Chatter.components, + ChatterAITopbar, + }, +}); diff --git a/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.xml b/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.xml new file mode 100644 index 0000000..02f69eb --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.esm.js b/ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.esm.js new file mode 100644 index 0000000..c1170ff --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.esm.js @@ -0,0 +1,36 @@ +import {Component, markup} from "@odoo/owl"; +import {usePopover} from "@web/core/popover/popover_hook"; + +export class ChatterAIItemPopover extends Component { + static template = "ai_oca_bridge.ChatterAIItemPopover"; + static props = { + help: String, + close: Function, + }; +} + +export class ChatterAIItem extends Component { + static template = "ai_oca_bridge.ChatterAIItem"; + static props = {bridge: Object}; + + setup() { + super.setup(); + this.popover = usePopover(ChatterAIItemPopover); + } + get tooltipInfo() { + return markup(this.props.bridge.description || ""); + } + onMouseEnter(ev) { + this.closeTooltip(); + const help = this.tooltipInfo; + this.popover.open(ev.currentTarget, {help}); + } + + onMouseLeave() { + this.closeTooltip(); + } + + closeTooltip() { + this.popover.close(); + } +} diff --git a/ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.xml b/ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.xml new file mode 100644 index 0000000..c876d9b --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/ai_oca_bridge/static/tests/mock_server/define_ai_models.esm.js b/ai_oca_bridge/static/tests/mock_server/define_ai_models.esm.js new file mode 100644 index 0000000..f7e5138 --- /dev/null +++ b/ai_oca_bridge/static/tests/mock_server/define_ai_models.esm.js @@ -0,0 +1,14 @@ +/* + Copyright 2025 Dixmit + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +*/ +import {AiBridge} from "./mock_models/ai_bridge.esm"; +import {defineModels} from "@web/../tests/web_test_helpers"; +import {mailModels} from "@mail/../tests/mail_test_helpers"; +export const baseAIModels = { + AiBridge, +}; + +export function defineBaseAIModels() { + return defineModels({...mailModels, ...baseAIModels}); +} diff --git a/ai_oca_bridge/static/tests/mock_server/mock_models/ai_bridge.esm.js b/ai_oca_bridge/static/tests/mock_server/mock_models/ai_bridge.esm.js new file mode 100644 index 0000000..a926326 --- /dev/null +++ b/ai_oca_bridge/static/tests/mock_server/mock_models/ai_bridge.esm.js @@ -0,0 +1,40 @@ +/* + Copyright 2025 Dixmit + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +*/ +import {fields, models} from "@web/../tests/web_test_helpers"; + +export class AiBridge extends models.ServerModel { + _name = "ai.bridge"; + result_type = fields.Selection({ + selection: [ + ["none", "None"], + ["action", "Action"], + ["notification", "Notification"], + ], + default: "none", + }); + name = fields.Char(); + _records = []; + execute_ai_bridge(ids) { + const record = this.browse(ids); + if (record && record[0].result_type === "action") { + return { + action: { + type: "ir.actions.act_window", + res_model: "res.partner", + views: [[false, "list"]], + }, + }; + } + return { + notification: { + body: "Mocked AI Bridge Response", + args: { + type: "info", + title: "AI Bridge Notification", + }, + }, + }; + } +} diff --git a/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.test.js b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.test.js new file mode 100644 index 0000000..6450eaa --- /dev/null +++ b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.test.js @@ -0,0 +1,134 @@ +/* + Copyright 2025 Dixmit + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +*/ +import {click, queryAll} from "@odoo/hoot-dom"; +import { + defineModels, + fields, + mountView, + webModels, +} from "@web/../tests/web_test_helpers"; +import {expect, test} from "@odoo/hoot"; +import {defineBaseAIModels} from "../mock_server/define_ai_models.esm"; +import {startServer} from "@mail/../tests/mail_test_helpers"; + +class ResPartner extends webModels.ResPartner { + _inherit = ["mail.thread"]; + ai_bridge_info = fields.Generic({default: []}); +} + +defineModels([ResPartner]); +defineBaseAIModels(); + +test("AI Notification", async () => { + const pyEnv = await startServer(); + const aiBridgeNotificationId = pyEnv["ai.bridge"].create({ + name: "AI 1", + result_type: "notification", + }); + const aiBridgeActionId = pyEnv["ai.bridge"].create({ + name: "AI 2", + result_type: "action", + }); + const partnerId = pyEnv["res.partner"].create({ + name: "Awesome partner", + ai_bridge_info: [ + { + id: aiBridgeNotificationId, + name: "AI 1", + description: "test1 description", + }, + {id: aiBridgeActionId, name: "AI 2"}, + ], + }); + await mountView({ + type: "form", + resId: partnerId, + resIds: [partnerId], + resModel: "res.partner", + arch: `
+ + + `, + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + await expect(queryAll(".ai_button_selection")).toHaveCount(1, { + message: "should have an AI button", + }); + + await click(`.ai_button_selection`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(`.o_ChatterTopbar_AIItem`).toHaveCount(2, { + message: "should have 2 AI Items", + }); + + await click(`.dropdown-item:first-child`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(`.o_notification_manager .o_notification`).toHaveCount(1, { + message: "should have 1 Notification after clicking on AI Item", + }); +}); + +test("AI Action", async () => { + const pyEnv = await startServer(); + const aiBridgeNotificationId = pyEnv["ai.bridge"].create({ + name: "AI 1", + result_type: "notification", + }); + const aiBridgeActionId = pyEnv["ai.bridge"].create({ + name: "AI 2", + result_type: "action", + }); + const partnerId = pyEnv["res.partner"].create({ + name: "Awesome partner", + ai_bridge_info: [ + { + id: aiBridgeNotificationId, + name: "AI 1", + description: "test1 description", + }, + {id: aiBridgeActionId, name: "AI 2"}, + ], + }); + await mountView({ + type: "form", + resId: partnerId, + resIds: [partnerId], + resModel: "res.partner", + arch: `
+ + + `, + }); + + // We load this view because we need it to be loaded later by the action. + await mountView({ + type: "list", + resId: false, + resIds: [], + resModel: "res.partner", + arch: ` + + + `, + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + await expect(`.ai_button_selection`).toHaveCount(1, { + message: "should have an AI button", + }); + + await click(`.ai_button_selection`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(`.o_ChatterTopbar_AIItem`).toHaveCount(2, { + message: "should have 2 AI Items", + }); + + await click(`.dropdown-item:nth-child(2)`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + expect(`.o_list_view`).toHaveCount(1, { + message: "should have 1 List View after clicking on AI Item with action", + }); +}); diff --git a/ai_oca_bridge/tests/__init__.py b/ai_oca_bridge/tests/__init__.py new file mode 100644 index 0000000..be5a273 --- /dev/null +++ b/ai_oca_bridge/tests/__init__.py @@ -0,0 +1,4 @@ +from . import test_bridge +from . import test_frontend +from . import test_connection +from . import test_mixin diff --git a/ai_oca_bridge/tests/common.py b/ai_oca_bridge/tests/common.py new file mode 100644 index 0000000..743be84 --- /dev/null +++ b/ai_oca_bridge/tests/common.py @@ -0,0 +1,8 @@ +class TrackingDisabledMixin: + """Disable tracking for the whole test environment.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Avoid chatter/metadata tracking interfering with assertions. + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) diff --git a/ai_oca_bridge/tests/fake_models.py b/ai_oca_bridge/tests/fake_models.py new file mode 100644 index 0000000..6c83df3 --- /dev/null +++ b/ai_oca_bridge/tests/fake_models.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class BridgeTest(models.Model): + _name = "bridge.test" + _description = "Test Model for AI Bridge" + + name = fields.Char() + description = fields.Char() diff --git a/ai_oca_bridge/tests/test_bridge.py b/ai_oca_bridge/tests/test_bridge.py new file mode 100644 index 0000000..53787e4 --- /dev/null +++ b/ai_oca_bridge/tests/test_bridge.py @@ -0,0 +1,371 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +from unittest import mock + +from odoo.fields import Domain +from odoo.tests.common import TransactionCase + +from .common import TrackingDisabledMixin + + +class TestBridge(TrackingDisabledMixin, TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.bridge = cls.env["ai.bridge"].create( + { + "name": "Test Bridge", + "model_id": cls.env.ref("base.model_res_partner").id, + "url": "https://example.com/api", + "auth_type": "none", + "usage": "thread", + } + ) + # We add this in order to simplify tests, as jsons will be filled. + cls.bridge_extra = cls.env["ai.bridge"].create( + { + "name": "Test Bridge Extra", + "model_id": cls.env.ref("base.model_res_partner").id, + "url": "https://example.com/api", + "auth_type": "none", + "usage": "thread", + } + ) + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Partner", + "email": "test@example.com", + } + ) + cls.group = cls.env["res.groups"].create( + { + "name": "Test Group", + } + ) + + def test_bridge_none_auth(self): + self.assertEqual(self.bridge.auth_type, "none") + self.assertTrue(self.partner.ai_bridge_info) + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + with mock.patch("requests.post") as mock_post: + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertTrue( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + execution = self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertEqual(execution.res_id, self.partner.id) + self.assertNotIn("name", execution.payload) + + def test_bridge_none_auth_fields_record(self): + self.bridge.write( + { + "payload_type": "record", + "auth_type": "none", + "field_ids": [ + (4, self.env.ref("base.field_res_partner__name").id), + (4, self.env.ref("base.field_res_partner__create_date").id), + (4, self.env.ref("base.field_res_partner__image_1920").id), + ], + } + ) + self.assertTrue(self.partner.ai_bridge_info) + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + with mock.patch("requests.post") as mock_post: + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertTrue( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + execution = self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertEqual(execution.res_id, self.partner.id) + self.assertIn("name", execution.payload["record"]) + self.assertEqual(execution.payload["record"]["name"], self.partner.name) + self.assertEqual(1, self.bridge.execution_count) + + def test_bridge_basic_auth(self): + self.bridge.write( + { + "auth_type": "basic", + "auth_username": "test_user", + "auth_password": "test_pass", + } + ) + self.assertTrue(self.partner.ai_bridge_info) + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + with mock.patch("requests.post") as mock_post: + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertTrue( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + + def test_bridge_token_auth(self): + self.bridge.write( + { + "auth_type": "token", + "auth_token": "test_token", + } + ) + self.assertTrue(self.partner.ai_bridge_info) + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + with mock.patch("requests.post") as mock_post: + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertTrue( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + + def test_bridge_error(self): + self.assertTrue(self.partner.ai_bridge_info) + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + execution = self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertTrue(execution) + self.assertTrue(execution.error) + + def test_bridge_unactive(self): + self.bridge.action_archive() + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + execution = self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertFalse(execution) + + def test_bridge_check_group(self): + self.bridge.group_ids = [(4, self.group.id)] + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + execution = self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertFalse(execution) + + def test_bridge_domain_filtering(self): + self.assertTrue(self.partner.ai_bridge_info) + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.bridge.write({"domain": f"[('id', '!=', {self.partner.id})]"}) + self.partner.invalidate_recordset() + self.assertNotIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + + def test_bridge_group_filtering(self): + self.assertTrue(self.partner.ai_bridge_info) + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.bridge.write({"group_ids": [(4, self.group.id)]}) + self.partner.invalidate_recordset() + self.assertNotIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + self.env.user.group_ids |= self.group + self.partner.invalidate_recordset() + self.assertIn( + self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] + ) + + def test_view_fields(self): + view = self.partner.get_view(view_type="form") + self.assertIn("ai_bridge_info", view["models"][self.partner._name]) + self.assertIn(b'name="ai_bridge_info"', view["arch"]) + + def test_sample(self): + self.assertTrue(self.bridge.sample_payload) + self.assertIn("_id", self.bridge.sample_payload) + + def test_bridge_result_message(self): + self.bridge.write({"result_type": "message"}) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + message_domain = Domain.AND( + [ + Domain("model", "=", self.partner._name), + Domain("res_id", "=", self.partner.id), + ] + ) + message_count = self.env["mail.message"].search_count(message_domain) + with mock.patch("requests.post") as mock_post: + mock_post.return_value = mock.Mock( + status_code=200, json=lambda: {"body": "My message"} + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertEqual( + self.env["mail.message"].search_count(message_domain), + message_count + 1, + ) + + def test_bridge_result_message_async(self): + self.bridge.write({"result_type": "message", "result_kind": "async"}) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + message_domain = Domain.AND( + [ + Domain("model", "=", self.partner._name), + Domain("res_id", "=", self.partner.id), + ] + ) + message_count = self.env["mail.message"].search_count(message_domain) + with mock.patch("requests.post") as mock_post: + mock_post.return_value = mock.Mock( + status_code=200, json=lambda: {"body": "My message"} + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertEqual( + self.env["mail.message"].search_count(message_domain), + message_count, + ) + execution = self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertTrue(execution.expiration_date) + execution._process_response({"body": "My message"}) + self.assertEqual( + self.env["mail.message"].search_count(message_domain), + message_count + 1, + ) + self.assertFalse(execution.expiration_date) + + def test_bridge_result_action_immediate(self): + self.bridge.write({"result_type": "action", "result_kind": "immediate"}) + self.assertFalse( + self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + ) + with mock.patch("requests.post") as mock_post: + mock_post.return_value = mock.Mock( + status_code=200, + json=lambda: { + "action": "ai_oca_bridge.ai_bridge_act_window", + "context": {"key": "value"}, + }, + ) + result = self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertIn("action", result) + self.assertEqual( + result["action"]["id"], + self.env.ref("ai_oca_bridge.ai_bridge_act_window").id, + ) + + def test_bridge_execute_computed_fields(self): + with mock.patch("requests.post") as mock_post: + mock_post.return_value = mock.Mock( + status_code=200, json=lambda: {"body": "My message"} + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + execution = self.env["ai.bridge.execution"].search( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertEqual( + execution.payload["_id"], json.loads(execution.payload_txt)["_id"] + ) + + def test_bridge_result_server_action_success(self): + # Create a server action and give it an external id + server_action = self.env["ir.actions.server"].create( + { + "name": "Test SA", + "model_id": self.env.ref("base.model_res_partner").id, + "state": "code", + "code": "records.ensure_one()\nresult = True", + } + ) + # Configure bridge to use server_action immediate + self.bridge.write( + { + "result_type": "server_action", + "result_kind": "immediate", + "server_action_id": server_action.id, + } + ) + self.env["ir.model.data"].create( + { + "name": "test_server_action_bridge", + "module": "ai_oca_bridge", + "model": "ir.actions.server", + "res_id": server_action.id, + "noupdate": True, + } + ) + with mock.patch("requests.post") as mock_post: + mock_post.return_value = mock.Mock( + status_code=200, + json=lambda: {"body": "My message"}, + ) + result = self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + mock_post.assert_called_once() + self.assertEqual(result, {"status": "success", "action": server_action.name}) diff --git a/ai_oca_bridge/tests/test_connection.py b/ai_oca_bridge/tests/test_connection.py new file mode 100644 index 0000000..a3c2f9f --- /dev/null +++ b/ai_oca_bridge/tests/test_connection.py @@ -0,0 +1,120 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo.tests.common import HttpCase, tagged +from odoo.tools import urls + +from .common import TrackingDisabledMixin + + +@tagged("post_install", "-at_install") +class TestAsyncConnection(TrackingDisabledMixin, HttpCase): + def setUp(self): + super().setUp() + self.bridge = self.env["ai.bridge"].create( + { + "name": "Test Bridge", + "model_id": self.env.ref("base.model_res_partner").id, + "url": "https://example.com/api", + "auth_type": "none", + "result_type": "message", + "result_kind": "async", + "usage": "thread", + } + ) + self.partner = self.env["res.partner"].create( + { + "name": "Test Partner", + "email": "test@example.com", + } + ) + + with mock.patch("requests.post") as mock_post: + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + self.url = mock_post.call_args[1]["json"]["_response_url"] + self.message_count = self.env["mail.message"].search_count( + [ + ("model", "=", self.partner._name), + ("res_id", "=", self.partner.id), + ] + ) + + def test_wrong_key(self): + result = self.opener.post(f"{self.url}1234", json={"body": "Test response"}) + self.assertEqual( + result.status_code, 404, "Should return 404 for wrong key in URL." + ) + self.assertEqual(result.text, "Token is not allowed for this execution.") + + def test_wrong_id(self): + result = self.opener.post( + f"{self.base_url()}/ai/response/-1/TOKEN", json={"body": "Test response"} + ) + self.assertEqual( + result.status_code, 404, "Should return 404 for wrong key in URL." + ) + self.assertEqual(result.text, "Execution not found.") + + def test_connection(self): + self.assertTrue( + self.env["ai.bridge.execution"].search( + [ + ("ai_bridge_id", "=", self.bridge.id), + ("expiration_date", "!=", False), + ] + ) + ) + result = self.opener.post(self.url, json={"body": "Test response"}) + self.assertIn('{"id":', result.text) + self.assertEqual( + self.env["mail.message"].search_count( + [ + ("model", "=", self.partner._name), + ("res_id", "=", self.partner.id), + ] + ), + self.message_count + 1, + "A new message should be created in the thread.", + ) + self.assertFalse( + self.env["ai.bridge.execution"].search( + [ + ("ai_bridge_id", "=", self.bridge.id), + ("expiration_date", "!=", False), + ] + ) + ) + # Key is wrong, so no message should be created + result = self.opener.post(self.url, json={"body": "Test response"}) + self.assertEqual( + result.status_code, 404, "Should return 404 for wrong key in URL." + ) + + def test_connection_expired(self): + self.assertTrue( + self.env["ai.bridge.execution"].search( + [ + ("ai_bridge_id", "=", self.bridge.id), + ("expiration_date", "!=", False), + ] + ) + ) + execution = self.env["ai.bridge.execution"].search( + [ + ("ai_bridge_id", "=", self.bridge.id), + ("expiration_date", "!=", False), + ] + ) + execution.expiration_date = "2020-01-01 00:00:00" + token = execution._generate_token() + result = self.opener.post( + urls.urljoin( + execution.get_base_url(), f"/ai/response/{execution.id}/{token}" + ), + json={"body": "Test response"}, + ) + self.assertEqual( + result.status_code, 404, "Should return 404 for expired execution." + ) diff --git a/ai_oca_bridge/tests/test_frontend.py b/ai_oca_bridge/tests/test_frontend.py new file mode 100644 index 0000000..2bbeece --- /dev/null +++ b/ai_oca_bridge/tests/test_frontend.py @@ -0,0 +1,17 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import odoo + +from odoo.addons.web.tests.test_js import WebSuite + + +@odoo.tests.tagged("post_install", "-at_install") +class TestAiOcaBridgeFrontend(WebSuite): + """Test Automation OCA""" + + def get_hoot_filters(self): + self._test_params = [("+", "@ai_oca_bridge")] + return super().get_hoot_filters() + + def test_ai_oca_bridge(self): + self.test_unit_desktop() diff --git a/ai_oca_bridge/tests/test_mixin.py b/ai_oca_bridge/tests/test_mixin.py new file mode 100644 index 0000000..a4b0e0e --- /dev/null +++ b/ai_oca_bridge/tests/test_mixin.py @@ -0,0 +1,306 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo.exceptions import ValidationError +from odoo.fields import Domain +from odoo.orm.model_classes import add_to_registry +from odoo.tests import Form, TransactionCase, new_test_user + +from .common import TrackingDisabledMixin + + +class TestBridge(TrackingDisabledMixin, TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + from .fake_models import BridgeTest + + add_to_registry(cls.registry, BridgeTest) + cls.registry._setup_models__(cls.env.cr, ["bridge.test"]) + cls.registry.init_models(cls.env.cr, ["bridge.test"], {"models_to_check": True}) + cls.addClassCleanup(cls.registry.__delitem__, "bridge.test") + + cls.bridge = cls.env["ai.bridge"].create( + { + "name": "Test Bridge", + "model_id": cls.env["ir.model"]._get_id("bridge.test"), + "url": "https://example.com/api", + "auth_type": "none", + "usage": "none", + } + ) + cls.user = new_test_user( + cls.env, + login="bridge_user", + groups="base.group_user", + ) + cls.portal_user = new_test_user( + cls.env, + login="bridge_portal_user", + groups="base.group_portal", + ) + cls.env["ir.model.access"].create( + { + "name": "ai.bridge.execution user access", + "model_id": cls.env["ir.model"]._get_id("bridge.test"), + "perm_create": True, + "perm_read": True, + "perm_write": True, + "perm_unlink": True, + } + ) + + def test_bridge_creation_user(self): + self.bridge.write({"usage": "ai_thread_create"}) + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + with self.with_user("bridge_user"), mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + # Create a test record + self.env["bridge.test"].create({"name": "Test Record"}) + mock_post.assert_called_once() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + + def test_bridge_creation_portal_user(self): + self.bridge.write({"usage": "ai_thread_create"}) + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + with ( + self.with_user("bridge_portal_user"), + mock.patch("requests.post") as mock_post, + ): + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + # Create a test record + self.env["bridge.test"].create({"name": "Test Record"}) + mock_post.assert_called_once() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + + def test_bridge_thread_creation(self): + self.bridge.write({"usage": "ai_thread_create"}) + with mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + # Create a test record + record = self.env["bridge.test"].create({"name": "Test Record"}) + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + mock_post.assert_called_once() + record.write({"name": "Updated Record"}) + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + record.unlink() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + mock_post.assert_called_once() + + def test_bridge_thread_write(self): + self.bridge.write({"usage": "ai_thread_write"}) + with mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + # Create a test record + record = self.env["bridge.test"].create({"name": "Test Record"}) + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + record.write({"name": "Updated Record"}) + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + record.unlink() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + mock_post.assert_called_once() + + def test_bridge_thread_unlink(self): + self.assertNotEqual(self.bridge.payload_type, "none") + with Form(self.bridge) as bridge_form: + bridge_form.usage = "ai_thread_unlink" + self.assertEqual(self.bridge.payload_type, "none") + with mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + # Create a test record + record = self.env["bridge.test"].create({"name": "Test Record"}) + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + record.write({"name": "Updated Record"}) + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + record.unlink() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ), + ) + mock_post.assert_called_once() + + def test_bridge_thread_unlink_constrains(self): + self.assertNotEqual(self.bridge.payload_type, "none") + with Form(self.bridge) as bridge_form: + bridge_form.usage = "ai_thread_unlink" + self.assertEqual(self.bridge.payload_type, "none") + with self.assertRaises(ValidationError): + self.bridge.payload_type = "record" + + def test_bridge_model_required(self): + self.assertFalse(self.bridge.model_required) + self.bridge.usage = "ai_thread_create" + self.assertTrue(self.bridge.model_required) + self.bridge.usage = "thread" + self.assertTrue(self.bridge.model_required) + + def test_bridge_enabled_for_conditions(self): + self.bridge.write({"usage": "ai_thread_create"}) + record = self.env["bridge.test"].create({"name": "Test Record"}) + with mock.patch.object( + type(self.bridge), "_enabled_for", return_value=False + ) as mock_enabled: + with mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + initial_count = self.env["ai.bridge.execution"].search_count([]) + self.env["bridge.test"].create({"name": "Test Record 2"}) + final_count = self.env["ai.bridge.execution"].search_count([]) + self.assertEqual(initial_count, final_count) + mock_enabled.assert_called() + mock_post.assert_not_called() + self.bridge.write({"usage": "ai_thread_unlink", "payload_type": "none"}) + with mock.patch.object( + type(self.bridge), "_enabled_for", return_value=False + ) as mock_enabled: + with mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + initial_count = self.env["ai.bridge.execution"].search_count([]) + record.unlink() + final_count = self.env["ai.bridge.execution"].search_count([]) + self.assertEqual(initial_count, final_count) + mock_enabled.assert_called() + mock_post.assert_not_called() + + def test_prepare_unlink_empty_records(self): + self.bridge.write({"usage": "ai_thread_unlink", "payload_type": "none"}) + empty_records = self.env["bridge.test"].browse([]) + initial_count = self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + result = empty_records._prepare_execution_ai_bridges_unlink(empty_records) + self.assertEqual(len(result), 0) + self.assertEqual(result._name, "ai.bridge.execution") + final_count = self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertEqual(initial_count, final_count) + + def test_execute_ai_bridges_empty_records(self): + self.bridge.write({"usage": "ai_thread_create"}) + empty_records = self.env["bridge.test"].browse([]) + with mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "success"} + empty_records._execute_ai_bridges_for_records( + empty_records, "ai_thread_create" + ) + mock_post.assert_not_called() + execution_count = self.env["ai.bridge.execution"].search_count( + Domain("ai_bridge_id", "=", self.bridge.id) + ) + self.assertEqual(execution_count, 0) + + def test_trigger_field_ids_write_filters(self): + self.bridge.write({"usage": "ai_thread_write"}) + # Only trigger when the 'name' field is present in values + name_field = self.env["ir.model.fields"].search( + [("model", "=", "bridge.test"), ("name", "=", "name")], limit=1 + ) + self.assertTrue(name_field, "Name field should exist for bridge.test") + self.bridge.trigger_field_ids = [(6, 0, [name_field.id])] + + record = self.env["bridge.test"].create({"name": "N", "description": "d"}) + with mock.patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"result": "ok"} + + # Write on non-configured field -> should NOT trigger + record.write({"description": "d2"}) + self.assertEqual(mock_post.call_count, 0) + + # Write on configured field -> should trigger once + record.write({"name": "N2"}) + self.assertEqual(mock_post.call_count, 1) + + # Write on both -> still triggers (presence of 'name') + record.write({"name": "N3", "description": "d3"}) + self.assertEqual(mock_post.call_count, 2) diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml new file mode 100644 index 0000000..7e00e38 --- /dev/null +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -0,0 +1,157 @@ + + + + + ai.bridge + +
+
+ + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + ai.bridge + + + + + + + + + + + + + + + ai.bridge + + + + + + + + + + + + + + AI Bridge + ai.bridge + list,form + [] + {} + + + + AI Bridge + + + + + diff --git a/ai_oca_bridge/views/ai_bridge_execution.xml b/ai_oca_bridge/views/ai_bridge_execution.xml new file mode 100644 index 0000000..5200ca7 --- /dev/null +++ b/ai_oca_bridge/views/ai_bridge_execution.xml @@ -0,0 +1,82 @@ + + + + + ai.bridge.execution + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + ai.bridge.execution + + + + + + + + + + + + + + + ai.bridge.execution + + + + + + + + + + + + + + AI Execution + ai.bridge.execution + list,form + [] + {} + + + + AI Bridge Execution + + + + +
diff --git a/ai_oca_bridge/views/menu.xml b/ai_oca_bridge/views/menu.xml new file mode 100644 index 0000000..70a6811 --- /dev/null +++ b/ai_oca_bridge/views/menu.xml @@ -0,0 +1,16 @@ + + + + +