From b56630f4f1c9f86e72d2d83c306c973b306be5eb Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 7 Jun 2025 14:36:17 +0200 Subject: [PATCH 01/51] [ADD] ai_oca_bridge --- ai_oca_bridge/README.rst | 138 +++++ ai_oca_bridge/__init__.py | 1 + ai_oca_bridge/__manifest__.py | 33 ++ ai_oca_bridge/models/__init__.py | 3 + ai_oca_bridge/models/ai_bridge.py | 181 +++++++ ai_oca_bridge/models/ai_bridge_execution.py | 134 +++++ ai_oca_bridge/models/mail_thread.py | 68 +++ ai_oca_bridge/readme/CONFIGURE.md | 11 + ai_oca_bridge/readme/CONTEXT.md | 12 + ai_oca_bridge/readme/CONTRIBUTORS.md | 8 + ai_oca_bridge/readme/DESCRIPTION.md | 1 + ai_oca_bridge/readme/ROADMAP.md | 4 + ai_oca_bridge/readme/USAGE.md | 3 + ai_oca_bridge/security/ir.model.access.csv | 4 + ai_oca_bridge/security/security.xml | 19 + ai_oca_bridge/static/description/icon.png | Bin 0 -> 72370 bytes ai_oca_bridge/static/description/icon.svg | 49 ++ ai_oca_bridge/static/description/index.html | 485 ++++++++++++++++++ .../src/components/chatter/chatter.esm.js | 28 + .../chatter_topbar/chatter_topbar.xml | 19 + .../chatter_topbar_ai.esm.js | 24 + .../chatter_topbar_ai/chatter_topbar_ai.xml | 27 + .../chatter_topbar_ai_item.esm.js | 46 ++ .../chatter_topbar_ai_item.xml | 22 + .../static/tests/helpers/mock_server.esm.js | 22 + .../tests/helpers/model_definition.esm.js | 7 + .../tests/web/test_ai_oca_bridge.esm.js | 45 ++ ai_oca_bridge/tests/__init__.py | 2 + ai_oca_bridge/tests/test_bridge.py | 233 +++++++++ ai_oca_bridge/tests/test_frontend.py | 12 + ai_oca_bridge/views/ai_bridge.xml | 126 +++++ ai_oca_bridge/views/ai_bridge_execution.xml | 88 ++++ ai_oca_bridge/views/menu.xml | 17 + 33 files changed, 1872 insertions(+) create mode 100644 ai_oca_bridge/README.rst create mode 100644 ai_oca_bridge/__init__.py create mode 100644 ai_oca_bridge/__manifest__.py create mode 100644 ai_oca_bridge/models/__init__.py create mode 100644 ai_oca_bridge/models/ai_bridge.py create mode 100644 ai_oca_bridge/models/ai_bridge_execution.py create mode 100644 ai_oca_bridge/models/mail_thread.py create mode 100644 ai_oca_bridge/readme/CONFIGURE.md create mode 100644 ai_oca_bridge/readme/CONTEXT.md create mode 100644 ai_oca_bridge/readme/CONTRIBUTORS.md create mode 100644 ai_oca_bridge/readme/DESCRIPTION.md create mode 100644 ai_oca_bridge/readme/ROADMAP.md create mode 100644 ai_oca_bridge/readme/USAGE.md create mode 100644 ai_oca_bridge/security/ir.model.access.csv create mode 100644 ai_oca_bridge/security/security.xml create mode 100644 ai_oca_bridge/static/description/icon.png create mode 100644 ai_oca_bridge/static/description/icon.svg create mode 100644 ai_oca_bridge/static/description/index.html create mode 100644 ai_oca_bridge/static/src/components/chatter/chatter.esm.js create mode 100644 ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml create mode 100644 ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.esm.js create mode 100644 ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.xml create mode 100644 ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.esm.js create mode 100644 ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.xml create mode 100644 ai_oca_bridge/static/tests/helpers/mock_server.esm.js create mode 100644 ai_oca_bridge/static/tests/helpers/model_definition.esm.js create mode 100644 ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js create mode 100644 ai_oca_bridge/tests/__init__.py create mode 100644 ai_oca_bridge/tests/test_bridge.py create mode 100644 ai_oca_bridge/tests/test_frontend.py create mode 100644 ai_oca_bridge/views/ai_bridge.xml create mode 100644 ai_oca_bridge/views/ai_bridge_execution.xml create mode 100644 ai_oca_bridge/views/menu.xml diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst new file mode 100644 index 0000000..3f98e3b --- /dev/null +++ b/ai_oca_bridge/README.rst @@ -0,0 +1,138 @@ +============= +AI OCA Bridge +============= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ec715e5128d445996bb14e4dddf88666bf0e7e42e2c7ec2061045eb8c9138b10 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-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/16.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-16-0/ai-16-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=16.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. + +On the external system, you will receive a POST payload with the +following information: + +- \_id +- \_model +- Fields selected + +In order to improve the view of the AI configuration, use groups and +domain to set better filters. + +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. +- Add an option to know when the AI task is asynchronous and comunicate + to the user once it has been finished. + +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 + +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..0650744 --- /dev/null +++ b/ai_oca_bridge/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/ai_oca_bridge/__manifest__.py b/ai_oca_bridge/__manifest__.py new file mode 100644 index 0000000..80c947c --- /dev/null +++ b/ai_oca_bridge/__manifest__.py @@ -0,0 +1,33 @@ +# 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": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/ai", + "development_status": "Beta", + "depends": ["mail"], + "data": [ + "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/**/*.xml", + "ai_oca_bridge/static/src/**/*.esm.js", + ], + "web.qunit_suite_tests": [ + "ai_oca_bridge/static/tests/web/**/*.esm.js", + ], + "web.tests_assets": [ + "ai_oca_bridge/static/tests/helpers/**/*.esm.js", + ], + }, + "applications": True, +} diff --git a/ai_oca_bridge/models/__init__.py b/ai_oca_bridge/models/__init__.py new file mode 100644 index 0000000..f432278 --- /dev/null +++ b/ai_oca_bridge/models/__init__.py @@ -0,0 +1,3 @@ +from . import ai_bridge +from . import ai_bridge_execution +from . import mail_thread diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py new file mode 100644 index 0000000..1b2bc74 --- /dev/null +++ b/ai_oca_bridge/models/ai_bridge.py @@ -0,0 +1,181 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import json +from datetime import date, datetime + +from odoo import _, api, fields, models +from odoo.tools.safe_eval import safe_eval + + +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")], + 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) + model_id = fields.Many2one( + "ir.model", + string="Model", + domain=[("transient", "=", False)], + required=True, + ondelete="cascade", + help="The model to which this bridge is associated.", + ) + model = fields.Char( + related="model_id.model", + string="Model Name", + ) + domain = fields.Char( + string="Filter", compute="_compute_domain", readonly=False, store=True + ) + 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.", + ) + group_ids = fields.Many2many( + "res.groups", + help="User groups allowed to use this AI bridge.", + ) + field_ids = fields.Many2many( + "ir.model.fields", + help="Fields to include in the AI bridge.", + compute="_compute_field_ids", + store=True, + readonly=False, + ) + auth_username = fields.Char(groups="base.group_system") + auth_password = fields.Char(groups="base.group_system") + auth_token = fields.Char(groups="base.group_system") + 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", + ) + + @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("field_ids", "model_id") + def _compute_sample_payload(self): + for record in self: + if not record.model_id: + record.sample_payload = json.dumps({}) + continue + item = record.env[record.model_id.model].search([], limit=1) + if item: + record.sample_payload = json.dumps( + record._prepare_payload(item), indent=4 + ) + else: + record.sample_payload = json.dumps({}) + + @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.groups_id & self.group_ids + ): + return { + "body": _("%s is not active.", self.name), + "args": {"type": "warning", "title": _("AI Bridge Inactive")}, + } + record = self.env[res_model].browse(res_id).exists() + if record: + execution = self.env["ai.bridge.execution"].create( + { + "ai_bridge_id": self.id, + "model_id": self.sudo().model_id.id, + "res_id": res_id, + } + ) + execution._execute() + if execution.state == "done": + return { + "body": _("%s executed successfully.", self.name), + "args": {"type": "success", "title": _("AI Bridge Executed")}, + } + return { + "body": _("%s failed.", self.name), + "args": {"type": "danger", "title": _("AI Bridge Failed")}, + } + + def _enabled_for(self, record): + """Check if the bridge is enabled for the given record.""" + self.ensure_one() + domain = safe_eval(self.domain) + if self.group_ids and not self.env.user.groups_id & self.group_ids: + return False + if domain: + return bool(record.filtered_domain(domain)) + return True + + def _prepare_payload(self, record, **kwargs): + """Prepare the payload to be sent to the AI system.""" + self.ensure_one() + vals = {} + if self.sudo().field_ids: + vals = record.read(self.sudo().field_ids.mapped("name"))[0] + return json.loads( + json.dumps( + { + **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..8dd4ca4 --- /dev/null +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -0,0 +1,134 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +import traceback +from io import StringIO + +import requests + +from odoo import _, api, fields, models + + +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=True) + state = fields.Selection( + [ + ("draft", "Draft"), + ("done", "Done"), + ("error", "Error"), + ], + default="draft", + required=True, + ) + model_id = fields.Many2one( + "ir.model", + required=True, + 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, + ) + + @api.depends() + 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 _execute(self, **kwargs): + self.ensure_one() + payload = self.ai_bridge_id._prepare_payload( + self.env[self.sudo().model_id.model].browse(self.res_id), **kwargs + ) + 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 + 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 == "none": + 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, + ) + elif self.ai_bridge_id.auth_type == "token": + return {"Authorization": f"Bearer {self.ai_bridge_id.sudo().auth_token}"} + else: + raise ValueError(_("Unsupported authentication type.")) + + def _get_headers(self): + """Return headers for the request.""" + return { + "Content-Type": "application/json", + "Accept": "application/json", + } diff --git a/ai_oca_bridge/models/mail_thread.py b/ai_oca_bridge/models/mail_thread.py new file mode 100644 index 0000000..f3a3f90 --- /dev/null +++ b/ai_oca_bridge/models/mail_thread.py @@ -0,0 +1,68 @@ +# 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.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([("model", "=", self._name)]).id + return ( + self.env["ai.bridge"] + .search([("model_id", "=", model_id), ("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/div[hasclass('oe_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 + trees 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/readme/CONFIGURE.md b/ai_oca_bridge/readme/CONFIGURE.md new file mode 100644 index 0000000..da5859e --- /dev/null +++ b/ai_oca_bridge/readme/CONFIGURE.md @@ -0,0 +1,11 @@ +As an administrator access `AI Bridge\AI Bridge`. + +Create a new bridge. +Define the name, model, url and configuration. + +On the external system, you will receive a POST payload with the following information: +- _id +- _model +- Fields selected + +In order to improve the view of the AI configuration, use groups and domain to set better filters. 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..7a0afbb --- /dev/null +++ b/ai_oca_bridge/readme/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +- [Dixmit](https://www.dixmit.com) + + - Enric Tobella + +- [Sygel Technology](https://www.sygel.es) + + - Valentín Vinagre + \ No newline at end of file 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..f3c96e6 --- /dev/null +++ b/ai_oca_bridge/readme/ROADMAP.md @@ -0,0 +1,4 @@ +- 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. +- Add an option to know when the AI task is asynchronous and comunicate to the user once it has been finished. 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..bb6f5ff --- /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,1,1,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 0000000000000000000000000000000000000000..4adee3cffbbe51372c56582e1a879ec08b5e0d94 GIT binary patch literal 72370 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 + + + + + + + + + diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html new file mode 100644 index 0000000..09a5ddd --- /dev/null +++ b/ai_oca_bridge/static/description/index.html @@ -0,0 +1,485 @@ + + + + + +AI OCA Bridge + + + +

+ + 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..ab45a61 --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js @@ -0,0 +1,28 @@ +/** @odoo-module **/ + +import {registerPatch} from "@mail/model/model_core"; + +registerPatch({ + name: "Chatter", + recordMethods: { + async onClickAiBridge(aiBridge) { + if (this.isTemporary) { + const saved = await this.doSaveRecord(); + if (!saved) { + return; + } + } + const notification = await this.env.services.orm.call( + "ai.bridge", + "execute_ai_bridge", + [[aiBridge.id], this.thread.model, this.thread.id] + ); + if (notification && this.env.services && this.env.services.notification) { + this.env.services.notification.add( + notification.body, + 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..01f7766 --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml @@ -0,0 +1,19 @@ + + + + + + + + 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..80ed03b --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.esm.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +import {ChatterAIItem} from "../chatter_topbar_ai_item/chatter_topbar_ai_item.esm"; +const {Component} = owl; +import {Dropdown} from "@web/core/dropdown/dropdown"; +import {DropdownItem} from "@web/core/dropdown/dropdown_item"; +import {registerMessagingComponent} from "@mail/utils/messaging_component"; + +export class ChatterAITopbar extends Component { + /** + * @returns {ChatterAITopbar} + */ + get chatterTopbar() { + return this.props.record; + } +} + +Object.assign(ChatterAITopbar, { + props: {record: Object}, + components: {Dropdown, DropdownItem, ChatterAIItem}, + template: "ai_oca_bridge.ChatterAITopbar", +}); + +registerMessagingComponent(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..f0a40e6 --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar_ai/chatter_topbar_ai.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + 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..b867db9 --- /dev/null +++ b/ai_oca_bridge/static/src/components/chatter_topbar_ai_item/chatter_topbar_ai_item.esm.js @@ -0,0 +1,46 @@ +/** @odoo-module **/ + +const {Component, markup} = owl; +import {usePopover} from "@web/core/popover/popover_hook"; + +export class ChatterAIItemPopover extends Component {} +ChatterAIItemPopover.template = "ai_oca_bridge.ChatterAIItemPopover"; + +export class ChatterAIItem extends Component { + setup() { + super.setup(); + this.popover = usePopover(); + this.tooltipPopover = null; + } + get tooltipInfo() { + return { + help: markup(this.props.bridge.description || ""), + }; + } + onMouseEnter(ev) { + this.closeTooltip(); + this.tooltipPopover = this.popover.add( + ev.currentTarget, + ChatterAIItemPopover, + this.tooltipInfo, + { + closeOnClickAway: true, + position: "top", + } + ); + } + + onMouseLeave() { + this.closeTooltip(); + } + + closeTooltip() { + if (this.tooltipPopover) { + this.tooltipPopover(); + this.tooltipPopover = null; + } + } +} + +ChatterAIItem.template = "ai_oca_bridge.ChatterAIItem"; +ChatterAIItem.props = {bridge: Object}; 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..9393a6f --- /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/helpers/mock_server.esm.js b/ai_oca_bridge/static/tests/helpers/mock_server.esm.js new file mode 100644 index 0000000..245ca97 --- /dev/null +++ b/ai_oca_bridge/static/tests/helpers/mock_server.esm.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ + +// ensure mail mock server is loaded first. +import "@mail/../tests/helpers/mock_server"; + +import {MockServer} from "@web/../tests/helpers/mock_server"; +import {patch} from "@web/core/utils/patch"; + +patch(MockServer.prototype, "ai_oca_bridge", { + async _performRPC(route, args) { + if (args.model === "ai.bridge" && args.method === "execute_ai_bridge") { + return { + body: "Mocked AI Bridge Response", + args: { + type: "info", + title: "AI Bridge Notification", + }, + }; + } + return this._super(...arguments); + }, +}); diff --git a/ai_oca_bridge/static/tests/helpers/model_definition.esm.js b/ai_oca_bridge/static/tests/helpers/model_definition.esm.js new file mode 100644 index 0000000..55720fd --- /dev/null +++ b/ai_oca_bridge/static/tests/helpers/model_definition.esm.js @@ -0,0 +1,7 @@ +/** @odoo-module **/ + +import {insertModelFields} from "@bus/../tests/helpers/model_definitions_helpers"; + +insertModelFields("res.partner", { + ai_bridge_info: {default: [], type: "json"}, +}); diff --git a/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js new file mode 100644 index 0000000..be185d5 --- /dev/null +++ b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js @@ -0,0 +1,45 @@ +/** @odoo-module */ +/* global QUnit */ +/* + Copyright 2025 Dixmit + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +*/ +import {start, startServer} from "@mail/../tests/helpers/test_utils"; +QUnit.module("ai_oca_bridge"); + +QUnit.test("Check Existance", async function (assert) { + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + ai_bridge_info: [ + {name: "AI 1", id: 1, description: "test1 description"}, + {name: "AI 2", id: 2}, + ], + }); + const views = { + "res.partner,false,form": `
+ +
+ + +
+ `, + }; + const {click, openView} = await start({serverData: {views}}); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_AIButton`).length, + 1, + "should have an AI button" + ); + await click(".o_ChatterTopbar_AIButton .ai_button_selection"); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_AIItem`).length, + 2, + "should have 2 AI Items" + ); + await click(".o_ChatterTopbar_AIItem"); +}); diff --git a/ai_oca_bridge/tests/__init__.py b/ai_oca_bridge/tests/__init__.py new file mode 100644 index 0000000..b369e8f --- /dev/null +++ b/ai_oca_bridge/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_bridge +from . import test_frontend diff --git a/ai_oca_bridge/tests/test_bridge.py b/ai_oca_bridge/tests/test_bridge.py new file mode 100644 index 0000000..4fd1c5b --- /dev/null +++ b/ai_oca_bridge/tests/test_bridge.py @@ -0,0 +1,233 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo.tests.common import TransactionCase + + +class TestBridge(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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + execution = self.env["ai.bridge.execution"].search( + [("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(self): + self.bridge.write( + { + "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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + execution = self.env["ai.bridge.execution"].search( + [("ai_bridge_id", "=", self.bridge.id)] + ) + self.assertEqual(execution.res_id, self.partner.id) + self.assertIn("name", execution.payload) + self.assertEqual(execution.payload["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( + [("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( + [("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( + [("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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + execution = self.env["ai.bridge.execution"].search( + [("ai_bridge_id", "=", self.bridge.id)] + ) + self.assertTrue(execution) + self.assertTrue(execution.error) + + def test_bridge_unactive(self): + self.bridge.toggle_active() + self.assertFalse( + self.env["ai.bridge.execution"].search( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + execution = self.env["ai.bridge.execution"].search( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) + execution = self.env["ai.bridge.execution"].search( + [("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.groups_id |= 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) diff --git a/ai_oca_bridge/tests/test_frontend.py b/ai_oca_bridge/tests/test_frontend.py new file mode 100644 index 0000000..74947d6 --- /dev/null +++ b/ai_oca_bridge/tests/test_frontend.py @@ -0,0 +1,12 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import common + + +@common.tagged("post_install", "-at_install") +class TestFrontend(common.HttpCase): + def test_javascript(self): + self.browser_js( + "/web/tests?module=ai_oca_bridge", "", login="admin", timeout=1800 + ) diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml new file mode 100644 index 0000000..2de2c96 --- /dev/null +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -0,0 +1,126 @@ + + + + + + ai.bridge + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + ai.bridge + + + + + + + + + + + + + + + ai.bridge + + + + + + + + + + + + + + AI Bridge + ai.bridge + tree,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..47c3e48 --- /dev/null +++ b/ai_oca_bridge/views/ai_bridge_execution.xml @@ -0,0 +1,88 @@ + + + + + + ai.bridge.execution + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + ai.bridge.execution + + + + + + + + + + + + + + + ai.bridge.execution + + + + + + + + + + + + + + AI Execution + ai.bridge.execution + tree,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..ecc9272 --- /dev/null +++ b/ai_oca_bridge/views/menu.xml @@ -0,0 +1,17 @@ + + + + + + From 717af48997f37010436b467e919a01ed052a6ed3 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 9 Jun 2025 19:51:45 +0000 Subject: [PATCH 02/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 528 +++++++++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 ai_oca_bridge/i18n/ai_oca_bridge.pot 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..9ca26b6 --- /dev/null +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -0,0 +1,528 @@ +# 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: \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 +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s executed successfully." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s failed." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s is not active." +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 +#, python-format +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 +#, python-format +msgid "AI Bridge Failed" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "AI Bridge Inactive" +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,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_mail_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_channel__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_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__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.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,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 +#: 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,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____last_update +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update +msgid "Last Modified on" +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_main_attachment_id +msgid "Main Attachment" +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__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__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,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_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_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.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread +msgid "Thread" +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 +#: 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 +#, python-format +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,help:ai_oca_bridge.field_ai_bridge__group_ids +msgid "User groups allowed to use this AI bridge." +msgstr "" From 989a79519d91d5126419fbb66ee3709e9679c538 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 9 Jun 2025 19:53:45 +0000 Subject: [PATCH 03/51] [BOT] post-merge updates --- ai_oca_bridge/README.rst | 8 +++-- ai_oca_bridge/static/description/index.html | 34 ++++++++++++--------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index 3f98e3b..1dce6b7 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,3 +1,7 @@ +.. 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 ============= @@ -7,13 +11,13 @@ AI OCA Bridge !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ec715e5128d445996bb14e4dddf88666bf0e7e42e2c7ec2061045eb8c9138b10 + !! source digest: sha256:31dfda372b3da67b0803b994d5e27a8e969232065831c946a09620ad3f3e7ba5 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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/licence-AGPL--3-blue.png +.. |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 diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 09a5ddd..758588f 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -AI OCA Bridge +README.rst -
-

AI OCA Bridge

+
+ + +Odoo Community Association + +
+

AI OCA Bridge

-

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

+

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

@@ -389,7 +394,7 @@

AI OCA Bridge

-

Use Cases / Context

+

Use Cases / Context

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

    @@ -410,7 +415,7 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

Configuration

As an administrator access AI Bridge\AI Bridge.

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

On the external system, you will receive a POST payload with the @@ -424,12 +429,12 @@

Configuration

domain to set better filters.

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -439,7 +444,7 @@

    Known issues / Roadmap

-

Bug Tracker

+

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 @@ -447,15 +452,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -481,5 +486,6 @@

Maintainers

+
From fc847deb0bd7e8b23117536500caa325f5d4e06a Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 10 Jun 2025 15:32:44 +0000 Subject: [PATCH 04/51] Added translation using Weblate (Italian) --- ai_oca_bridge/i18n/it.po | 529 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 529 insertions(+) create mode 100644 ai_oca_bridge/i18n/it.po diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po new file mode 100644 index 0000000..a162d98 --- /dev/null +++ b/ai_oca_bridge/i18n/it.po @@ -0,0 +1,529 @@ +# 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: 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" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s executed successfully." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s failed." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s is not active." +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 +#, python-format +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 +#, python-format +msgid "AI Bridge Failed" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "AI Bridge Inactive" +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,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_mail_blacklist__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_channel__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_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__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.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,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 +#: 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,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____last_update +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update +msgid "Last Modified on" +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_main_attachment_id +msgid "Main Attachment" +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__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__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,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_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_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.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread +msgid "Thread" +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 +#: 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 +#, python-format +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,help:ai_oca_bridge.field_ai_bridge__group_ids +msgid "User groups allowed to use this AI bridge." +msgstr "" From 63fac155a4f1521b49b20a2868c8d870dd8b9a38 Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 10 Jun 2025 15:34:41 +0000 Subject: [PATCH 05/51] Translated using Weblate (Italian) Currently translated at 33.3% (31 of 93 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 66 +++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index a162d98..c1337a4 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,113 +6,115 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2025-06-10 18:25+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 #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "%s executed successfully." -msgstr "" +msgstr "%s eseguito con successo." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "%s failed." -msgstr "" +msgstr "%s fallito." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "%s is not active." -msgstr "" +msgstr "%s non è attivo." #. 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 "" +msgstr "Collegamento AI" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Executed" -msgstr "" +msgstr "Collegamento AI eseguito" #. module: ai_oca_bridge #: model:ir.ui.menu,name:ai_oca_bridge.ai_bridge_execution_menu msgid "AI Bridge Execution" -msgstr "" +msgstr "Esecuzione collegamento AI" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Failed" -msgstr "" +msgstr "Collegamento AI fallito" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Inactive" -msgstr "" +msgstr "Collegamento AI inattivo" #. module: ai_oca_bridge #: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window msgid "AI Execution" -msgstr "" +msgstr "Esecuzione AI" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction msgid "Action Needed" -msgstr "" +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 "" +msgstr "Attivo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids msgid "Activities" -msgstr "" +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 "" +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 "" +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 "" +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 "" +msgstr "Collegamento AI" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ai_bridge msgid "Ai Bridge Configuration" -msgstr "" +msgstr "Configurazione collegamento AI" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__ai_bridge_info @@ -126,66 +128,66 @@ msgstr "" #: 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 "" +msgstr "Informazioni collegamento AI" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ai_bridge_execution msgid "Ai Execution" -msgstr "" +msgstr "Esecuzione AI" #. 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 "" +msgstr "In archivio" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_attachment_count msgid "Attachment Count" -msgstr "" +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 "" +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 "" +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 "" +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 "" +msgstr "Tipo autenticazione" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic msgid "Basic Authentication" -msgstr "" +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 "" +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 "" +msgstr "Creata 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 "" +msgstr "Creato il" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage @@ -193,11 +195,13 @@ 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,field_description:ai_oca_bridge.field_ai_bridge__description msgid "Description" -msgstr "" +msgstr "Descrizione" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__display_name From 1897e13b348ef73ee16dbf971715376ef924614b Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 11 Jun 2025 06:52:40 +0000 Subject: [PATCH 06/51] Translated using Weblate (Italian) Currently translated at 100.0% (93 of 93 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 127 +++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 60 deletions(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index c1337a4..64b33e5 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-06-10 18:25+0000\n" +"PO-Revision-Date: 2025-06-11 09:26+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -207,252 +207,252 @@ msgstr "Descrizione" #: 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 "" +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 "" +msgstr "Eseguito" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft msgid "Draft" -msgstr "" +msgstr "Bozza" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_mail_thread msgid "Email Thread" -msgstr "" +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 "" +msgstr "Errore" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids msgid "Execution" -msgstr "" +msgstr "Esecuzione" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_count msgid "Execution Count" -msgstr "" +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 "" +msgstr "Dettagli esecuzione" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__field_ids msgid "Field" -msgstr "" +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 "" +msgstr "Campi da includere nel collegamento AI." #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__domain msgid "Filter" -msgstr "" +msgstr "Filtro" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids msgid "Followers" -msgstr "" +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 "" +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 "" +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 "" +msgstr "Gruppo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message msgid "Has Message" -msgstr "" +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 "" +msgstr "ID" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon msgid "Icon" -msgstr "" +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 "" +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 "" +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 "" +msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna." #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" -msgstr "" +msgstr "Segue" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. 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 "" +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 "" +msgstr "Ultimo aggiornamento il" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Allegato principale" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" -msgstr "" +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 "" +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 "" +msgstr "Modello" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model msgid "Model Name" -msgstr "" +msgstr "Nome modello" #. 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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Tipo prossima attività" #. 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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Numero di messaggi con errore di consegna" #. 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 "" +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 "" +msgstr "Testo del carico" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" -msgstr "" +msgstr "Res" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view msgid "Response" -msgstr "" +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 "" +msgstr "Utente responsabile" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result msgid "Result" -msgstr "" +msgstr "Risultato" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view msgid "Sample" -msgstr "" +msgstr "Esempio" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload msgid "Sample Payload" -msgstr "" +msgstr "Carico esempio" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload @@ -460,16 +460,18 @@ 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 "" +msgstr "Sequenza" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state msgid "State" -msgstr "" +msgstr "Stato" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state @@ -479,55 +481,60 @@ msgid "" "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 "" +msgstr "L'URL del sistema AI 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 "" +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 AI esterno." #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__thread msgid "Thread" -msgstr "" +msgstr "Discussione" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token msgid "Token Authentication" -msgstr "" +msgstr "Autenticazione token" #. 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 "" +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 "" +msgstr "URL" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 #, python-format msgid "Unsupported authentication type." -msgstr "" +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 "" +msgstr "Uilizzo" #. 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 "" +msgstr "Gruppi utente autorizzati ad usare questo collegamento AI." From 33bbe690460b2cd3b4ad5bcf8516b6cd85941427 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 10 Jun 2025 22:00:06 +0200 Subject: [PATCH 07/51] [IMP] ai_oca_bridge Add payload_type Add result type Add result kind (immediate or async) Add controller for controlling asynchronous executions (with timeout) Add function for processing result (allows extension) Add a user that will execute the processing result function Add tests --- ai_oca_bridge/__init__.py | 1 + ai_oca_bridge/controllers/__init__.py | 1 + ai_oca_bridge/controllers/ai.py | 48 +++++++ ai_oca_bridge/models/ai_bridge.py | 132 +++++++++++++++----- ai_oca_bridge/models/ai_bridge_execution.py | 68 +++++++++- ai_oca_bridge/tests/__init__.py | 5 +- ai_oca_bridge/tests/test_bridge.py | 98 ++++++++++++++- ai_oca_bridge/tests/test_connection.py | 68 ++++++++++ ai_oca_bridge/views/ai_bridge.xml | 103 +++++++++------ 9 files changed, 446 insertions(+), 78 deletions(-) create mode 100644 ai_oca_bridge/controllers/__init__.py create mode 100644 ai_oca_bridge/controllers/ai.py create mode 100644 ai_oca_bridge/tests/test_connection.py diff --git a/ai_oca_bridge/__init__.py b/ai_oca_bridge/__init__.py index 0650744..91c5580 100644 --- a/ai_oca_bridge/__init__.py +++ b/ai_oca_bridge/__init__.py @@ -1 +1,2 @@ +from . import controllers from . import models 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..06668ec --- /dev/null +++ b/ai_oca_bridge/controllers/ai.py @@ -0,0 +1,48 @@ +# 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 +from odoo.tools.translate import _ + + +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(_("Execution not found."), status=404) + if not consteq(execution._generate_token(), token): + return request.make_response( + _("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(_("Execution is expired."), status=404) + return request.make_response( + json.dumps( + execution._process_response( + json.loads( + request.httprequest.get_data().decode( + request.httprequest.charset + ) + ) + ) + ), + headers=[ + ("Content-Type", "application/json"), + ], + ) diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index 1b2bc74..64d2e1b 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -3,11 +3,14 @@ import base64 import json +import logging from datetime import date, datetime from odoo import _, api, fields, models from odoo.tools.safe_eval import safe_eval +_logger = logging.getLogger(__name__) + class AiBridge(models.Model): @@ -32,20 +35,39 @@ class AiBridge(models.Model): name = fields.Char(required=True, translate=True) active = fields.Boolean(default=True) description = fields.Html(translate=True) - model_id = fields.Many2one( - "ir.model", - string="Model", - domain=[("transient", "=", False)], + 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( + [ + ("record", "Record"), + ("record_v0", "Record v0"), # Deprecated, use 'record' instead + ], required=True, - ondelete="cascade", - help="The model to which this bridge is associated.", + default="record", ) - model = fields.Char( - related="model_id.model", - string="Model Name", + result_type = fields.Selection( + [ + ("none", "None"), + ("message", "message"), + ], + required=True, + default="none", + help="Defines the type of result expected from the AI system.", ) - domain = fields.Char( - string="Filter", compute="_compute_domain", readonly=False, store=True + result_kind = fields.Selection( + [("immediate", "Immediate"), ("async", "Asynchronous")], + default="immediate", + help="Defines how the result from the AI system is processed. " + "If 'Asynchronous', the result will be processed in the background " + "and communicated later from the AI system.", + ) + 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( @@ -65,10 +87,23 @@ class AiBridge(models.Model): 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", + ) + + ####################################### + # Payload type 'record' specific fields + ####################################### + field_ids = fields.Many2many( "ir.model.fields", help="Fields to include in the AI bridge.", @@ -76,13 +111,20 @@ class AiBridge(models.Model): store=True, readonly=False, ) - auth_username = fields.Char(groups="base.group_system") - auth_password = fields.Char(groups="base.group_system") - auth_token = fields.Char(groups="base.group_system") - 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", + domain=[("transient", "=", False)], + required=False, + ondelete="cascade", + help="The model to which this bridge is associated.", + ) + model = fields.Char( + related="model_id.model", + string="Model Name", + ) + domain = fields.Char( + string="Filter", compute="_compute_domain", readonly=False, store=True ) @api.depends("model_id") @@ -95,19 +137,12 @@ def _compute_field_ids(self): for record in self: record.field_ids = False - @api.depends("field_ids", "model_id") + @api.depends("field_ids", "model_id", "payload_type") def _compute_sample_payload(self): for record in self: - if not record.model_id: - record.sample_payload = json.dumps({}) - continue - item = record.env[record.model_id.model].search([], limit=1) - if item: - record.sample_payload = json.dumps( - record._prepare_payload(item), indent=4 - ) - else: - record.sample_payload = json.dumps({}) + record.sample_payload = json.dumps( + record.with_context(sample_payload=True)._prepare_payload(), indent=4 + ) @api.depends("execution_ids") def _compute_execution_count(self): @@ -156,9 +191,46 @@ def _enabled_for(self, record): return bool(record.filtered_domain(domain)) return True - def _prepare_payload(self, record, **kwargs): + 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_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) + 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 _prepare_payload_record_v0(self, record=None, **kwargs): + """Prepare the payload to be sent to the AI system.""" + _logger.warning( + "The 'record_v0' payload type is deprecated. " "Use 'record' instead." + ) + 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) vals = {} if self.sudo().field_ids: vals = record.read(self.sudo().field_ids.mapped("name"))[0] diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py index 8dd4ca4..a3f3cab 100644 --- a/ai_oca_bridge/models/ai_bridge_execution.py +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -3,11 +3,13 @@ import json import traceback +from datetime import timedelta from io import StringIO import requests +from werkzeug import urls -from odoo import _, api, fields, models +from odoo import _, api, fields, models, tools class AiBridgeExecution(models.Model): @@ -26,7 +28,7 @@ class AiBridgeExecution(models.Model): required=True, ondelete="cascade", ) - res_id = fields.Integer(required=True) + res_id = fields.Integer(required=False) state = fields.Selection( [ ("draft", "Draft"), @@ -38,7 +40,7 @@ class AiBridgeExecution(models.Model): ) model_id = fields.Many2one( "ir.model", - required=True, + required=False, ondelete="cascade", ) payload = fields.Json(readonly=True) @@ -53,6 +55,10 @@ class AiBridgeExecution(models.Model): store=True, readonly=True, ) + expiration_date = fields.Datetime( + readonly=True, + help="Expiration date for the async operation token.", + ) @api.depends() def _compute_name(self): @@ -79,11 +85,26 @@ 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.url_join( + self.get_base_url(), f"/ai/response/{self.id}/{token}" + ) + return payload + def _execute(self, **kwargs): self.ensure_one() - payload = self.ai_bridge_id._prepare_payload( - self.env[self.sudo().model_id.model].browse(self.res_id), **kwargs - ) + 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, **kwargs) + payload = self._add_extra_payload_fields(payload) try: response = requests.post( self.ai_bridge_id.url, @@ -97,6 +118,8 @@ def _execute(self, **kwargs): response.raise_for_status() self.state = "done" self.payload = payload + if self.ai_bridge_id.result_kind == "immediate": + self._process_response(response.json()) except Exception: self.state = "error" self.payload = payload @@ -132,3 +155,36 @@ def _get_headers(self): "Content-Type": "application/json", "Accept": "application/json", } + + 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 _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/tests/__init__.py b/ai_oca_bridge/tests/__init__.py index b369e8f..b282372 100644 --- a/ai_oca_bridge/tests/__init__.py +++ b/ai_oca_bridge/tests/__init__.py @@ -1,2 +1,3 @@ -from . import test_bridge -from . import test_frontend +# from . import test_bridge +# from . import test_frontend +from . import test_connection diff --git a/ai_oca_bridge/tests/test_bridge.py b/ai_oca_bridge/tests/test_bridge.py index 4fd1c5b..200b6ac 100644 --- a/ai_oca_bridge/tests/test_bridge.py +++ b/ai_oca_bridge/tests/test_bridge.py @@ -66,9 +66,10 @@ def test_bridge_none_auth(self): self.assertEqual(execution.res_id, self.partner.id) self.assertNotIn("name", execution.payload) - def test_bridge_none_auth_fields(self): + def test_bridge_none_auth_fields_record_v0(self): self.bridge.write( { + "payload_type": "record_v0", "auth_type": "none", "field_ids": [ (4, self.env.ref("base.field_res_partner__name").id), @@ -102,6 +103,43 @@ def test_bridge_none_auth_fields(self): self.assertEqual(execution.payload["name"], self.partner.name) self.assertEqual(1, self.bridge.execution_count) + 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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + execution = self.env["ai.bridge.execution"].search( + [("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( { @@ -231,3 +269,61 @@ def test_view_fields(self): 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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + message_count = self.env["mail.message"].search_count( + [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] + ) + 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( + [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] + ), + 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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + ) + message_count = self.env["mail.message"].search_count( + [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] + ) + 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( + [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] + ), + message_count, + ) + execution = self.env["ai.bridge.execution"].search( + [("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( + [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] + ), + message_count + 1, + ) + self.assertFalse(execution.expiration_date) diff --git a/ai_oca_bridge/tests/test_connection.py b/ai_oca_bridge/tests/test_connection.py new file mode 100644 index 0000000..9d6f089 --- /dev/null +++ b/ai_oca_bridge/tests/test_connection.py @@ -0,0 +1,68 @@ +# 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 + + +@tagged("post_install", "-at_install") +class TestAsyncConnection(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_connection(self): + self.assertTrue( + self.env["ai.bridge.execution"].search( + [ + ("ai_bridge_id", "=", self.bridge.id), + ("expiration_date", "!=", False), + ] + ) + ) + self.opener.post(self.url, json={"body": "Test response"}) + 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), + ] + ) + ) diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml index 2de2c96..e85cdff 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -15,48 +15,73 @@ bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}" /> - +

- - - - - - - - - - - - - - - +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + From f582157b50fe78e201834071ebe999326177e8ba Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 12 Jun 2025 20:46:00 +0200 Subject: [PATCH 08/51] [IMP] ai_oca_bridge: Add action option --- ai_oca_bridge/README.rst | 87 +++++++++-- ai_oca_bridge/__manifest__.py | 4 +- ai_oca_bridge/data/ir_module_category.xml | 7 + ai_oca_bridge/models/ai_bridge.py | 39 +++-- ai_oca_bridge/models/ai_bridge_execution.py | 27 +++- ai_oca_bridge/readme/CONFIGURE.md | 67 ++++++++- ai_oca_bridge/readme/ROADMAP.md | 1 - ai_oca_bridge/static/description/index.html | 135 +++++++++++++----- .../src/components/chatter/chatter.esm.js | 14 +- .../static/tests/helpers/mock_server.esm.js | 22 ++- .../tests/helpers/model_definition.esm.js | 24 +++- .../tests/web/test_ai_oca_bridge.esm.js | 59 +++++++- ai_oca_bridge/tests/__init__.py | 4 +- ai_oca_bridge/tests/test_bridge.py | 38 +++++ ai_oca_bridge/tests/test_connection.py | 48 +++++++ 15 files changed, 492 insertions(+), 84 deletions(-) create mode 100644 ai_oca_bridge/data/ir_module_category.xml diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index 1dce6b7..e30b497 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,7 +1,3 @@ -.. 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 ============= @@ -17,7 +13,7 @@ AI OCA Bridge .. |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 +.. |badge2| image:: https://img.shields.io/badge/licence-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 @@ -69,16 +65,81 @@ As an administrator access ``AI Bridge\AI Bridge``. Create a new bridge. Define the name, model, url and configuration. -On the external system, you will receive a POST payload with the -following information: - -- \_id -- \_model -- Fields selected - 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. + +Record Payload (v0) +~~~~~~~~~~~~~~~~~~~ + +Adds all the fields directly on the payload. It will be removed on 17.0. + +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 ===== @@ -92,8 +153,6 @@ 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. -- Add an option to know when the AI task is asynchronous and comunicate - to the user once it has been finished. Bug Tracker =========== diff --git a/ai_oca_bridge/__manifest__.py b/ai_oca_bridge/__manifest__.py index 80c947c..dc0f12f 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -8,9 +8,11 @@ "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", + "category": "AI", "development_status": "Beta", "depends": ["mail"], "data": [ + "data/ir_module_category.xml", "security/ir.model.access.csv", "security/security.xml", "views/menu.xml", @@ -29,5 +31,5 @@ "ai_oca_bridge/static/tests/helpers/**/*.esm.js", ], }, - "applications": True, + "application": True, } 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/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index 64d2e1b..3cd5132 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -50,8 +50,9 @@ class AiBridge(models.Model): ) result_type = fields.Selection( [ - ("none", "None"), - ("message", "message"), + ("none", "No processing"), + ("message", "Post a Message"), + ("action", "Action"), ], required=True, default="none", @@ -60,9 +61,15 @@ class AiBridge(models.Model): result_kind = fields.Selection( [("immediate", "Immediate"), ("async", "Asynchronous")], default="immediate", - help="Defines how the result from the AI system is processed. " - "If 'Asynchronous', the result will be processed in the background " - "and communicated later from the AI system.", + 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, @@ -166,19 +173,25 @@ def execute_ai_bridge(self, res_model, res_id): execution = self.env["ai.bridge.execution"].create( { "ai_bridge_id": self.id, - "model_id": self.sudo().model_id.id, + "model_id": self.sudo().env["ir.model"]._get_id(res_model), "res_id": res_id, } ) - execution._execute() + result = execution._execute() + if result: + return result if execution.state == "done": return { - "body": _("%s executed successfully.", self.name), - "args": {"type": "success", "title": _("AI Bridge Executed")}, + "notification": { + "body": _("%s executed successfully.", self.name), + "args": {"type": "success", "title": _("AI Bridge Executed")}, + } } return { - "body": _("%s failed.", self.name), - "args": {"type": "danger", "title": _("AI Bridge Failed")}, + "notification": { + "body": _("%s failed.", self.name), + "args": {"type": "danger", "title": _("AI Bridge Failed")}, + } } def _enabled_for(self, record): @@ -207,6 +220,8 @@ def _prepare_payload_record(self, record=None, **kwargs): 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] @@ -231,6 +246,8 @@ def _prepare_payload_record_v0(self, record=None, **kwargs): 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] diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py index a3f3cab..8bf1e46 100644 --- a/ai_oca_bridge/models/ai_bridge_execution.py +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -60,7 +60,7 @@ class AiBridgeExecution(models.Model): help="Expiration date for the async operation token.", ) - @api.depends() + @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" @@ -96,6 +96,19 @@ def _add_extra_payload_fields(self, payload): payload["_response_url"] = urls.url_join( 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): @@ -119,7 +132,7 @@ def _execute(self, **kwargs): self.state = "done" self.payload = payload if self.ai_bridge_id.result_kind == "immediate": - self._process_response(response.json()) + return self._process_response(response.json()) except Exception: self.state = "error" self.payload = payload @@ -184,6 +197,16 @@ def _process_response_none(self, response): 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 _get_channel(self): if self.model_id and self.res_id: return self.env[self.model_id.model].browse(self.res_id) diff --git a/ai_oca_bridge/readme/CONFIGURE.md b/ai_oca_bridge/readme/CONFIGURE.md index da5859e..fe6bdb9 100644 --- a/ai_oca_bridge/readme/CONFIGURE.md +++ b/ai_oca_bridge/readme/CONFIGURE.md @@ -3,9 +3,66 @@ As an administrator access `AI Bridge\AI Bridge`. Create a new bridge. Define the name, model, url and configuration. -On the external system, you will receive a POST payload with the following information: -- _id -- _model -- Fields selected - 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. + +### Record Payload (v0) + +Adds all the fields directly on the payload. +It will be removed on 17.0. + +## 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) \ No newline at end of file diff --git a/ai_oca_bridge/readme/ROADMAP.md b/ai_oca_bridge/readme/ROADMAP.md index f3c96e6..a95064f 100644 --- a/ai_oca_bridge/readme/ROADMAP.md +++ b/ai_oca_bridge/readme/ROADMAP.md @@ -1,4 +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. -- Add an option to know when the AI task is asynchronous and comunicate to the user once it has been finished. diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 758588f..82c7b03 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +AI OCA Bridge -
+
+

AI OCA Bridge

- - -Odoo Community Association - -
-

AI OCA Bridge

-

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

+

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

+

Use Cases / Context

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

    @@ -415,36 +425,90 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

Configuration

As an administrator access AI Bridge\AI Bridge.

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

-

On the external system, you will receive a POST payload with the -following information:

-
    -
  • _id
  • -
  • _model
  • -
  • Fields selected
  • -

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.

+
+
+

Record Payload (v0)

+

Adds all the fields directly on the payload. It will be removed on 17.0.

+
+
+
+

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

+

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

+

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.
  • -
  • Add an option to know when the AI task is asynchronous and comunicate -to the user once it has been finished.
-

Bug Tracker

+

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 @@ -452,15 +516,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -486,6 +550,5 @@

Maintainers

-
diff --git a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js index ab45a61..7ec5189 100644 --- a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js +++ b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js @@ -12,15 +12,21 @@ registerPatch({ return; } } - const notification = await this.env.services.orm.call( + const result = await this.env.services.orm.call( "ai.bridge", "execute_ai_bridge", [[aiBridge.id], this.thread.model, this.thread.id] ); - if (notification && this.env.services && this.env.services.notification) { + 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( - notification.body, - notification.args + result.notification.body, + result.notification.args ); } }, diff --git a/ai_oca_bridge/static/tests/helpers/mock_server.esm.js b/ai_oca_bridge/static/tests/helpers/mock_server.esm.js index 245ca97..8a505e6 100644 --- a/ai_oca_bridge/static/tests/helpers/mock_server.esm.js +++ b/ai_oca_bridge/static/tests/helpers/mock_server.esm.js @@ -9,11 +9,25 @@ import {patch} from "@web/core/utils/patch"; patch(MockServer.prototype, "ai_oca_bridge", { async _performRPC(route, args) { if (args.model === "ai.bridge" && args.method === "execute_ai_bridge") { + const record = this.models["ai.bridge"].records.filter( + (record) => record.id === args.args[0][0] + ); + if (record && record[0].result_type === "action") { + return { + action: { + type: "ir.actions.act_window", + res_model: "res.partner", + views: [[false, "tree"]], + }, + }; + } return { - body: "Mocked AI Bridge Response", - args: { - type: "info", - title: "AI Bridge Notification", + notification: { + body: "Mocked AI Bridge Response", + args: { + type: "info", + title: "AI Bridge Notification", + }, }, }; } diff --git a/ai_oca_bridge/static/tests/helpers/model_definition.esm.js b/ai_oca_bridge/static/tests/helpers/model_definition.esm.js index 55720fd..ee1a109 100644 --- a/ai_oca_bridge/static/tests/helpers/model_definition.esm.js +++ b/ai_oca_bridge/static/tests/helpers/model_definition.esm.js @@ -1,7 +1,29 @@ /** @odoo-module **/ -import {insertModelFields} from "@bus/../tests/helpers/model_definitions_helpers"; +import { + addModelNamesToFetch, + insertModelFields, + insertRecords, +} from "@bus/../tests/helpers/model_definitions_helpers"; + +addModelNamesToFetch(["ai.bridge"]); insertModelFields("res.partner", { ai_bridge_info: {default: [], type: "json"}, }); +insertModelFields("ai.bridge", { + result_type: { + default: "none", + type: "selection", + selection: [ + ["none", "None"], + ["action", "Action"], + ["notification", "Notification"], + ], + }, + name: {string: "Name", type: "char"}, +}); +insertRecords("ai.bridge", [ + {id: 1, name: "Test AI Bridge", result_type: "none"}, + {id: 2, name: "Test AI Bridge Action", result_type: "action"}, +]); diff --git a/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js index be185d5..7aad6bc 100644 --- a/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js +++ b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js @@ -7,7 +7,7 @@ import {start, startServer} from "@mail/../tests/helpers/test_utils"; QUnit.module("ai_oca_bridge"); -QUnit.test("Check Existance", async function (assert) { +QUnit.test("AI Notification", async function (assert) { const pyEnv = await startServer(); const resPartnerId1 = pyEnv["res.partner"].create({ ai_bridge_info: [ @@ -30,8 +30,56 @@ QUnit.test("Check Existance", async function (assert) { res_model: "res.partner", views: [[false, "form"]], }); + await assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_AIButton .ai_button_selection`) + .length, + 1, + "should have an AI button" + ); + await click(".o_ChatterTopbar_AIButton .ai_button_selection"); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_AIItem`).length, + 2, + "should have 2 AI Items" + ); + await click(document.querySelectorAll(".o_ChatterTopbar_AIItem")[0]); assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_AIButton`).length, + document.querySelectorAll(`.o_notification_manager .o_notification`).length, + 1, + "should have 1 Notification after clicking on AI Item" + ); +}); + +QUnit.test("AI Action", async function (assert) { + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + ai_bridge_info: [ + {name: "AI 1", id: 1, description: "test1 description"}, + {name: "AI 2", id: 2}, + ], + }); + const views = { + "res.partner,false,form": `
+ +
+ + +
+ `, + "res.partner,false,tree": ` + + + `, + }; + const {click, openView} = await start({serverData: {views}}); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + await assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_AIButton .ai_button_selection`) + .length, 1, "should have an AI button" ); @@ -41,5 +89,10 @@ QUnit.test("Check Existance", async function (assert) { 2, "should have 2 AI Items" ); - await click(".o_ChatterTopbar_AIItem"); + await click(document.querySelectorAll(".o_ChatterTopbar_AIItem")[1]); + assert.strictEqual( + document.querySelectorAll(`.o_list_view`).length, + 1, + "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 index b282372..f1c4a81 100644 --- a/ai_oca_bridge/tests/__init__.py +++ b/ai_oca_bridge/tests/__init__.py @@ -1,3 +1,3 @@ -# from . import test_bridge -# from . import test_frontend +from . import test_bridge +from . import test_frontend from . import test_connection diff --git a/ai_oca_bridge/tests/test_bridge.py b/ai_oca_bridge/tests/test_bridge.py index 200b6ac..7a06239 100644 --- a/ai_oca_bridge/tests/test_bridge.py +++ b/ai_oca_bridge/tests/test_bridge.py @@ -1,6 +1,7 @@ # Copyright 2025 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import json from unittest import mock from odoo.tests.common import TransactionCase @@ -327,3 +328,40 @@ def test_bridge_result_message_async(self): 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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + self.assertEqual( + execution.payload["_id"], json.loads(execution.payload_txt)["_id"] + ) diff --git a/ai_oca_bridge/tests/test_connection.py b/ai_oca_bridge/tests/test_connection.py index 9d6f089..92ba5f9 100644 --- a/ai_oca_bridge/tests/test_connection.py +++ b/ai_oca_bridge/tests/test_connection.py @@ -3,6 +3,8 @@ from unittest import mock +from werkzeug import urls + from odoo.tests.common import HttpCase, tagged @@ -38,6 +40,20 @@ def setUp(self): ] ) + 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." + ) + + 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." + ) + def test_connection(self): self.assertTrue( self.env["ai.bridge.execution"].search( @@ -66,3 +82,35 @@ def test_connection(self): ] ) ) + # 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.url_join( + 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." + ) From d917fbd7976e6394b3e3f3c407af8b07ce408a70 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 17 Jun 2025 06:51:34 +0000 Subject: [PATCH 09/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 132 +++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index 9ca26b6..c391abe 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -13,6 +13,20 @@ msgstr "" "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 @@ -34,6 +48,11 @@ msgstr "" 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 @@ -72,6 +91,11 @@ msgstr "" 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" @@ -138,6 +162,16 @@ msgstr "" 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" @@ -193,6 +227,11 @@ msgid "" "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" @@ -241,6 +280,30 @@ msgstr "" msgid "Execution Details" msgstr "" +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +#, python-format +msgid "Execution is expired." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +#, python-format +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" @@ -307,6 +370,11 @@ msgstr "" 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" @@ -382,6 +450,11 @@ msgstr "" msgid "Next Activity Type" 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 @@ -419,6 +492,31 @@ msgstr "" 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.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 +msgid "Record v0" +msgstr "" + #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" @@ -439,6 +537,16 @@ msgstr "" 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" @@ -490,16 +598,35 @@ msgstr "" 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 +#, python-format +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." @@ -522,6 +649,11 @@ msgstr "" 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." From 286fa3b001e2da26501915f3b2027f6fe8dd7138 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 17 Jun 2025 06:53:33 +0000 Subject: [PATCH 10/51] [BOT] post-merge updates --- ai_oca_bridge/README.rst | 8 +++- ai_oca_bridge/__manifest__.py | 2 +- ai_oca_bridge/static/description/index.html | 52 ++++++++++++--------- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index e30b497..011aa11 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,3 +1,7 @@ +.. 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 ============= @@ -7,13 +11,13 @@ AI OCA Bridge !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:31dfda372b3da67b0803b994d5e27a8e969232065831c946a09620ad3f3e7ba5 + !! source digest: sha256:5aed2dd32687f302c9f2685670b2c67f99ae14c9579a96eab33454e0097934fb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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/licence-AGPL--3-blue.png +.. |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 diff --git a/ai_oca_bridge/__manifest__.py b/ai_oca_bridge/__manifest__.py index dc0f12f..86f1cc0 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -4,7 +4,7 @@ { "name": "AI OCA Bridge", "summary": """Makes a basic configuration to be used as bridge with external AI systems""", - "version": "16.0.1.0.0", + "version": "16.0.2.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 82c7b03..4fa6f51 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -AI OCA Bridge +README.rst -
-

AI OCA Bridge

+
+ + +Odoo Community Association + +
+

AI OCA Bridge

-

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

+

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

@@ -404,7 +409,7 @@

AI OCA Bridge

-

Use Cases / Context

+

Use Cases / Context

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

    @@ -425,17 +430,17 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

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

+

Payload Configuration

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

-

General

+

General

  • _odoo: Standard data to identify the Odoo Database
  • _model: Model of the related object
  • @@ -444,16 +449,16 @@

    General

-

Record Payload

+

Record Payload

Adds a new item called record with all the fields.

-

Record Payload (v0)

+

Record Payload (v0)

Adds all the fields directly on the payload. It will be removed on 17.0.

-

Asynchronous and synchronous calls

+

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 @@ -468,21 +473,21 @@

Asynchronous and synchronous call automatically on the synchronous call.

-

Result processing

+

Result processing

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

-

No processing

+

No processing

In this case, the result will do nothing

-

Post a Message

+

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

+

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:

@@ -495,12 +500,12 @@

Action

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -508,7 +513,7 @@

    Known issues / Roadmap

-

Bug Tracker

+

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 @@ -516,15 +521,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -550,5 +555,6 @@

Maintainers

+
From 06c4bd0cbd09d3c54f2c762a10ac20ae1cd4aad5 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 17 Jun 2025 06:53:41 +0000 Subject: [PATCH 11/51] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/ --- ai_oca_bridge/i18n/it.po | 134 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 64b33e5..32953dd 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -16,6 +16,22 @@ msgstr "" "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 "" + #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 @@ -37,6 +53,11 @@ msgstr "%s fallito." 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 "" + #. 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 @@ -75,6 +96,11 @@ msgstr "Collegamento AI inattivo" msgid "AI Execution" msgstr "Esecuzione AI" +#. 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" @@ -141,6 +167,16 @@ msgstr "Esecuzione AI" 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 "" + +#. 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" @@ -198,6 +234,11 @@ 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 "" + #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__description msgid "Description" @@ -246,6 +287,30 @@ msgstr "Conteggio esecuzioni" msgid "Execution Details" msgstr "Dettagli esecuzione" +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +#, python-format +msgid "Execution is expired." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +#, python-format +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" @@ -312,6 +377,11 @@ msgstr "Se selezionata, nuovi messaggi richiedono attenzione." 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 "" + #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -387,6 +457,11 @@ msgstr "Riepilogo prossima attività" msgid "Next Activity Type" msgstr "Tipo prossima attività" +#. 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 @@ -424,6 +499,31 @@ msgstr "Carico" 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 "" + +#. 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.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 +msgid "Record v0" +msgstr "" + #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" @@ -444,6 +544,16 @@ msgstr "Utente responsabile" 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 "" + +#. 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" @@ -502,16 +612,35 @@ msgid "The type of authentication used to connect to the external AI system." msgstr "" "Il tipo di autenticazione utilizzato per connettere il sistema di AI 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 "" + #. 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 "" + #. 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 +#, python-format +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." @@ -534,6 +663,11 @@ msgstr "Tipo autenticazione non supportato." 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 "" + #. 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." From e7fdd03e559e0cacda609bba35c93fa5d5b45029 Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 18 Jun 2025 06:41:19 +0000 Subject: [PATCH 12/51] Translated using Weblate (Italian) Currently translated at 100.0% (116 of 116 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 84 +++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 32953dd..163aafc 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-06-11 09:26+0000\n" +"PO-Revision-Date: 2025-06-18 10:25+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -31,6 +31,16 @@ msgid "" " 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 @@ -56,50 +66,50 @@ msgstr "%s non è attivo." #. module: ai_oca_bridge #: model:ir.module.category,name:ai_oca_bridge.module_category_ai msgid "AI" -msgstr "" +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 AI" +msgstr "Collegamento IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Executed" -msgstr "Collegamento AI eseguito" +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 AI" +msgstr "Esecuzione collegamento IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Failed" -msgstr "Collegamento AI fallito" +msgstr "Collegamento IA fallito" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Inactive" -msgstr "Collegamento AI inattivo" +msgstr "Collegamento IA inattivo" #. module: ai_oca_bridge #: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window msgid "AI Execution" -msgstr "Esecuzione AI" +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 "" +msgstr "Azione" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_needaction @@ -135,12 +145,12 @@ 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 AI" +msgstr "Collegamento IA" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ai_bridge msgid "Ai Bridge Configuration" -msgstr "Configurazione collegamento AI" +msgstr "Configurazione collegamento IA" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__ai_bridge_info @@ -154,12 +164,12 @@ msgstr "Configurazione collegamento AI" #: 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 AI" +msgstr "Informazioni collegamento IA" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ai_bridge_execution msgid "Ai Execution" -msgstr "Esecuzione AI" +msgstr "Esecuzione IA" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view @@ -170,12 +180,12 @@ 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 "" +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 "" +msgstr "Asincrono" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_attachment_count @@ -237,7 +247,7 @@ 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 "" +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 @@ -292,24 +302,24 @@ msgstr "Dettagli esecuzione" #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Execution is expired." -msgstr "" +msgstr "L'esecuzione è scaduta." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Execution not found." -msgstr "" +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 "" +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 "" +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 @@ -319,7 +329,7 @@ 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 AI." +msgstr "Campi da includere nel collegamento IA." #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__domain @@ -380,7 +390,7 @@ 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 "" +msgstr "Immediato" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower @@ -460,7 +470,7 @@ msgstr "Tipo prossima attività" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none msgid "No processing" -msgstr "" +msgstr "Nessuna elaborazione" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__none @@ -502,27 +512,27 @@ 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 "" +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 "" +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 "" +msgstr "Record" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view msgid "Record Payload" -msgstr "" +msgstr "Record carico" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 msgid "Record v0" -msgstr "" +msgstr "Record v0" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id @@ -547,12 +557,12 @@ msgstr "Risultato" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind msgid "Result Kind" -msgstr "" +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 "" +msgstr "Tipo risultato" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view @@ -599,7 +609,7 @@ 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 "L'URL del sistema AI esterno a cui questo collegamento fa riferimento." +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 @@ -610,12 +620,12 @@ msgstr "Modello a cui è associato questo collegamento." #: 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 AI esterno." +"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 "" +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 @@ -628,6 +638,8 @@ 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 @@ -639,7 +651,7 @@ msgstr "Autenticazione token" #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Token is not allowed for this execution." -msgstr "" +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 @@ -666,9 +678,9 @@ msgstr "Uilizzo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id msgid "User" -msgstr "" +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 AI." +msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." From 8e4f77294326c520671d27841d4f54141f8bd562 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 19 Jun 2025 15:24:30 +0200 Subject: [PATCH 13/51] [FIX] ai_oca_bridge: Enforce saving before executing it --- .../static/src/components/chatter/chatter.esm.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js index 7ec5189..418b53a 100644 --- a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js +++ b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js @@ -6,11 +6,9 @@ registerPatch({ name: "Chatter", recordMethods: { async onClickAiBridge(aiBridge) { - if (this.isTemporary) { - const saved = await this.doSaveRecord(); - if (!saved) { - return; - } + const saved = await this.doSaveRecord(); + if (!saved) { + return; } const result = await this.env.services.orm.call( "ai.bridge", From b04c102b70cca237aa7bdf4664bf6435fa0de2f7 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 19 Jun 2025 14:07:07 +0000 Subject: [PATCH 14/51] [BOT] post-merge updates --- ai_oca_bridge/README.rst | 2 +- ai_oca_bridge/__manifest__.py | 2 +- ai_oca_bridge/static/description/index.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index 011aa11..f167ca2 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -11,7 +11,7 @@ AI OCA Bridge !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:5aed2dd32687f302c9f2685670b2c67f99ae14c9579a96eab33454e0097934fb + !! source digest: sha256:95324eee7973f3fc8e883c34e3e72df6337726fe9eac34f04e68145eeaed22da !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/ai_oca_bridge/__manifest__.py b/ai_oca_bridge/__manifest__.py index 86f1cc0..cf8a635 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -4,7 +4,7 @@ { "name": "AI OCA Bridge", "summary": """Makes a basic configuration to be used as bridge with external AI systems""", - "version": "16.0.2.0.0", + "version": "16.0.2.0.1", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 4fa6f51..8aa403a 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -372,7 +372,7 @@

AI OCA Bridge

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:5aed2dd32687f302c9f2685670b2c67f99ae14c9579a96eab33454e0097934fb +!! source digest: sha256:95324eee7973f3fc8e883c34e3e72df6337726fe9eac34f04e68145eeaed22da !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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 From f7d8c5e95403f05963ea62dd2f562d2e2f5e6c6d Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 23 Jun 2025 17:00:36 +0200 Subject: [PATCH 15/51] [IMP] ai_oca_bridge: Allow to send files on Creation --- ai_oca_bridge/models/__init__.py | 2 + ai_oca_bridge/models/ai_bridge.py | 69 +++++++- ai_oca_bridge/models/ai_bridge_execution.py | 7 +- ai_oca_bridge/models/ai_bridge_thread.py | 77 +++++++++ ai_oca_bridge/models/ir_model.py | 28 ++++ ai_oca_bridge/tests/__init__.py | 1 + ai_oca_bridge/tests/fake_models.py | 9 + ai_oca_bridge/tests/test_mixin.py | 174 ++++++++++++++++++++ ai_oca_bridge/views/ai_bridge.xml | 12 +- 9 files changed, 362 insertions(+), 17 deletions(-) create mode 100644 ai_oca_bridge/models/ai_bridge_thread.py create mode 100644 ai_oca_bridge/models/ir_model.py create mode 100644 ai_oca_bridge/tests/fake_models.py create mode 100644 ai_oca_bridge/tests/test_mixin.py diff --git a/ai_oca_bridge/models/__init__.py b/ai_oca_bridge/models/__init__.py index f432278..226066a 100644 --- a/ai_oca_bridge/models/__init__.py +++ b/ai_oca_bridge/models/__init__.py @@ -1,3 +1,5 @@ +from . import ai_bridge_thread from . import ai_bridge from . import ai_bridge_execution from . import mail_thread +from . import ir_model diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index 3cd5132..e520a49 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -27,7 +27,13 @@ class AiBridge(models.Model): # We leave it empty to allow multiple companies to use the same bridge. ) usage = fields.Selection( - [("none", "None"), ("thread", "Thread")], + [ + ("none", "None"), + ("thread", "Thread"), + ("ai_thread_create", "AI Thread Create"), + ("ai_thread_write", "AI Thread Write"), + ("ai_thread_unlink", "AI Thread Unlink"), + ], default="none", help="Defines how this bridge is used. " "If 'Thread', it will be used in the mail thread context.", @@ -42,10 +48,14 @@ class AiBridge(models.Model): ) payload_type = fields.Selection( [ + ("none", "No payload"), ("record", "Record"), ("record_v0", "Record v0"), # Deprecated, use 'record' instead ], required=True, + store=True, + readonly=False, + compute="_compute_payload_type", default="record", ) result_type = fields.Selection( @@ -106,6 +116,14 @@ class AiBridge(models.Model): "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") ####################################### # Payload type 'record' specific fields @@ -118,14 +136,6 @@ class AiBridge(models.Model): store=True, readonly=False, ) - model_id = fields.Many2one( - "ir.model", - string="Model", - domain=[("transient", "=", False)], - required=False, - ondelete="cascade", - help="The model to which this bridge is associated.", - ) model = fields.Char( related="model_id.model", string="Model Name", @@ -134,6 +144,41 @@ class AiBridge(models.Model): 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 models.ValidationError( + _( + "When usage is 'AI Thread Unlink', " + "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: @@ -213,6 +258,12 @@ def _prepare_payload(self, **kwargs): ) 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() diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py index 8bf1e46..6f6247f 100644 --- a/ai_oca_bridge/models/ai_bridge_execution.py +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -116,7 +116,12 @@ def _execute(self, **kwargs): 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, **kwargs) + 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( 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..888dbb7 --- /dev/null +++ b/ai_oca_bridge/models/ai_bridge_thread.py @@ -0,0 +1,77 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, models + +_logger = logging.getLogger(__name__) + + +class AiBridgeThread(models.AbstractModel): + _name = "ai.bridge.thread" + _description = "AI Bridge Mixin" + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + model_id = self.sudo().env["ir.model"]._get_id(self._name) + for bridge in self.env["ai.bridge"].search( + [("model_id", "=", model_id), ("usage", "=", "ai_thread_create")] + ): + for record in records: + if bridge._enabled_for(record): + try: + bridge.execute_ai_bridge(record._name, record.id) + except Exception as e: + _logger.error( + "Error creating AI thread for creation on %s: %s", + record, + e, + ) + return records + + def write(self, values): + result = super().write(values) + model_id = self.sudo().env["ir.model"]._get_id(self._name) + for bridge in self.env["ai.bridge"].search( + [("model_id", "=", model_id), ("usage", "=", "ai_thread_write")] + ): + for record in self: + if bridge._enabled_for(record): + try: + bridge.execute_ai_bridge(record._name, record.id) + except Exception as e: + _logger.error( + "Error writing AI thread for writing on %s: %s", + record, + e, + ) + return result + + def unlink(self): + model_id = self.sudo().env["ir.model"]._get_id(self._name) + executions = self.env["ai.bridge.execution"] + for bridge in self.env["ai.bridge"].search( + [("model_id", "=", model_id), ("usage", "=", "ai_thread_unlink")] + ): + for record in self: + 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, + } + ) + result = super().unlink() + for execution in executions: + try: + execution._execute() + except Exception as e: + _logger.error( + "Error executing AI thread unlink for %s: %s", + self, + e, + ) + return result diff --git a/ai_oca_bridge/models/ir_model.py b/ai_oca_bridge/models/ir_model.py new file mode 100644 index 0000000..9b1ba97 --- /dev/null +++ b/ai_oca_bridge/models/ir_model.py @@ -0,0 +1,28 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrModel(models.Model): + + _inherit = "ir.model" + + is_ai_bridge_thread = fields.Boolean() + ai_usage = fields.Char(store=False, search="_search_ai_usage") + + def _reflect_model_params(self, model): + vals = super(IrModel, self)._reflect_model_params(model) + vals["is_ai_bridge_thread"] = ( + isinstance(model, self.pool["ai.bridge.thread"]) and not model._abstract + ) + return vals + + def _search_ai_usage(self, operator, value): + if operator not in ("="): + return [] + if value == "thread": + return [("is_mail_thread", "=", True), ("transient", "=", False)] + if value in ["ai_thread_create", "ai_thread_write", "ai_thread_unlink"]: + return [("is_ai_bridge_thread", "=", True), ("transient", "=", False)] + return [] diff --git a/ai_oca_bridge/tests/__init__.py b/ai_oca_bridge/tests/__init__.py index f1c4a81..be5a273 100644 --- a/ai_oca_bridge/tests/__init__.py +++ b/ai_oca_bridge/tests/__init__.py @@ -1,3 +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/fake_models.py b/ai_oca_bridge/tests/fake_models.py new file mode 100644 index 0000000..784b666 --- /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" + _inherit = "ai.bridge.thread" + _description = "Test Model for AI Bridge" + + name = fields.Char() diff --git a/ai_oca_bridge/tests/test_mixin.py b/ai_oca_bridge/tests/test_mixin.py new file mode 100644 index 0000000..afe76f5 --- /dev/null +++ b/ai_oca_bridge/tests/test_mixin.py @@ -0,0 +1,174 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo_test_helper import FakeModelLoader + +from odoo.exceptions import ValidationError +from odoo.tests.common import Form, TransactionCase + + +class TestBridge(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Load fake models ->/ + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .fake_models import BridgeTest + + cls.loader.update_registry((BridgeTest,)) + 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", + } + ) + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + 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( + [("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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ), + ) + record.unlink() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + [("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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ), + ) + record.write({"name": "Updated Record"}) + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + [("ai_bridge_id", "=", self.bridge.id)] + ), + ) + record.unlink() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + [("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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ), + ) + record.write({"name": "Updated Record"}) + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + [("ai_bridge_id", "=", self.bridge.id)] + ), + ) + record.unlink() + self.assertEqual( + 1, + self.env["ai.bridge.execution"].search_count( + [("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_search(self): + models = self.env["ir.model"].search([("ai_usage", "=", "thread")]) + model = self.env["ir.model"]._get_id("bridge.test") + self.assertTrue(models) + self.assertIn(self.env.ref("base.model_res_partner"), models) + self.assertNotIn(model, models.ids) + models = self.env["ir.model"].search([("ai_usage", "=", "ai_thread_create")]) + self.assertTrue(models) + self.assertNotIn(self.env.ref("base.model_res_partner"), models) + self.assertIn(model, models.ids) + models = self.env["ir.model"].search([("ai_usage", "=", "none")]) + self.assertTrue(models) + self.assertIn(self.env.ref("base.model_res_partner"), models) + self.assertIn(model, models.ids) + + 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) diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml index e85cdff..a60ef4a 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -30,9 +30,12 @@ /> + @@ -63,11 +66,6 @@ > - Date: Tue, 15 Jul 2025 13:39:08 +0000 Subject: [PATCH 16/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index c391abe..0f53661 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -86,11 +86,31 @@ msgstr "" 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__usage__ai_thread_create +msgid "AI Thread Create" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "AI Thread Unlink" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "AI Thread Write" +msgstr "" + #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" @@ -156,6 +176,11 @@ msgstr "" msgid "Ai Execution" msgstr "" +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage +msgid "Ai Usage" +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 @@ -375,6 +400,11 @@ msgstr "" msgid "Immediate" msgstr "" +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread +msgid "Is Ai Bridge Thread" +msgstr "" + #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -424,6 +454,16 @@ msgstr "" 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,name:ai_oca_bridge.model_ir_model +msgid "Models" +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" @@ -450,6 +490,11 @@ msgstr "" 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" @@ -658,3 +703,11 @@ msgstr "" #: 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 +#, python-format +msgid "" +"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +msgstr "" From 38671e705dcd24b7a4ce3ee876ef80924ffd316b Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 15 Jul 2025 13:41:21 +0000 Subject: [PATCH 17/51] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/ --- ai_oca_bridge/i18n/it.po | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 163aafc..7cc6c40 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -101,11 +101,31 @@ msgstr "Collegamento IA fallito" 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 "" + #. 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__usage__ai_thread_create +msgid "AI Thread Create" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "AI Thread Unlink" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "AI Thread Write" +msgstr "" + #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" @@ -171,6 +191,11 @@ msgstr "Informazioni collegamento IA" msgid "Ai Execution" msgstr "Esecuzione IA" +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage +msgid "Ai Usage" +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 @@ -392,6 +417,11 @@ msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna." msgid "Immediate" msgstr "Immediato" +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread +msgid "Is Ai Bridge Thread" +msgstr "" + #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -441,6 +471,16 @@ msgstr "Modello" 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 "" + +#. module: ai_oca_bridge +#: model:ir.model,name:ai_oca_bridge.model_ir_model +msgid "Models" +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" @@ -467,6 +507,11 @@ msgstr "Riepilogo prossima attività" 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 "" + #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none msgid "No processing" @@ -684,3 +729,11 @@ msgstr "Utente" #: 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 +#, python-format +msgid "" +"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +msgstr "" From 4f1c79123345b6a9ef7e5dbb8d2239129fecb5b4 Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 15 Jul 2025 14:16:24 +0000 Subject: [PATCH 18/51] Translated using Weblate (Italian) Currently translated at 100.0% (126 of 126 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 7cc6c40..d912fc2 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-06-18 10:25+0000\n" +"PO-Revision-Date: 2025-07-15 14:55+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -104,7 +104,7 @@ msgstr "Collegamento IA inattivo" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ai_bridge_thread msgid "AI Bridge Mixin" -msgstr "" +msgstr "Mixin bridge IA" #. module: ai_oca_bridge #: model:ir.actions.act_window,name:ai_oca_bridge.ai_bridge_execution_act_window @@ -114,17 +114,17 @@ msgstr "Esecuzione IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create msgid "AI Thread Create" -msgstr "" +msgstr "Creazione thread IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink msgid "AI Thread Unlink" -msgstr "" +msgstr "Rilascio thread IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write msgid "AI Thread Write" -msgstr "" +msgstr "Scrittura thread IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action @@ -194,7 +194,7 @@ msgstr "Esecuzione IA" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage msgid "Ai Usage" -msgstr "" +msgstr "Utilizzo IA" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view @@ -420,7 +420,7 @@ msgstr "Immediato" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread msgid "Is Ai Bridge Thread" -msgstr "" +msgstr "È un thread IA" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower @@ -474,12 +474,12 @@ 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 "" +msgstr "Modello richiesto" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ir_model msgid "Models" -msgstr "" +msgstr "Modelli" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline @@ -510,7 +510,7 @@ 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 "" +msgstr "Nessun carico" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__none @@ -737,3 +737,5 @@ msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." 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'." From 4df195fd374851faafa061b5504f342c82fb23c0 Mon Sep 17 00:00:00 2001 From: mymage Date: Tue, 22 Jul 2025 14:14:09 +0000 Subject: [PATCH 19/51] Translated using Weblate (Italian) Currently translated at 100.0% (126 of 126 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index d912fc2..ad491e7 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-07-15 14:55+0000\n" +"PO-Revision-Date: 2025-07-22 16:25+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -252,7 +252,7 @@ msgstr "Azienda" #: 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 "Creata da" +msgstr "Creato da" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__create_date From 6515aa824af3490a92a3d3856e082b9cc643ff9d Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 29 Jul 2025 04:08:59 +0000 Subject: [PATCH 20/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index 0f53661..51507b7 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -159,6 +159,7 @@ 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_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_channel__ai_bridge_info #: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info From 61fef7c795a82e94e0513d02d170c45e9c16ba84 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 29 Jul 2025 04:11:09 +0000 Subject: [PATCH 21/51] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/ --- ai_oca_bridge/i18n/it.po | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index ad491e7..fb8cc4e 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -174,6 +174,7 @@ 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_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_channel__ai_bridge_info #: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info From 5a703311c7d4ea3cce16502a7cdbb4e7febb9327 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 31 Jul 2025 09:41:01 +0000 Subject: [PATCH 22/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index 51507b7..ddbf4aa 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -160,6 +160,8 @@ 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_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__ai_bridge_info #: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info @@ -705,6 +707,16 @@ msgstr "" msgid "User groups allowed to use this AI bridge." msgstr "" +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website communication history" +msgstr "" + #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 From 71a5451482b8fdf61159713404bc01bb12447eb3 Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 31 Jul 2025 09:43:21 +0000 Subject: [PATCH 23/51] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/ --- ai_oca_bridge/i18n/it.po | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index fb8cc4e..51025bf 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -175,6 +175,8 @@ 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_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__ai_bridge_info #: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_thread__ai_bridge_info @@ -731,6 +733,16 @@ msgstr "Utente" msgid "User groups allowed to use this AI bridge." msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." +#. module: ai_oca_bridge +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website communication history" +msgstr "" + #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 From 7dbf4e4efb69da49f956e83222255e6515bb804e Mon Sep 17 00:00:00 2001 From: Ariel Barreiros <50119000+arielbarreiros96@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:34:09 +0100 Subject: [PATCH 24/51] [FIX] ai_oca_bridge token authentication --- ai_oca_bridge/models/ai_bridge_execution.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py index 6f6247f..1e2e342 100644 --- a/ai_oca_bridge/models/ai_bridge_execution.py +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -155,24 +155,28 @@ def _execute_kwargs(self, timeout=False, **kwargs): def _get_auth(self): """Return authentication for the request.""" - if self.ai_bridge_id.auth_type == "none": + 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, ) - elif self.ai_bridge_id.auth_type == "token": - return {"Authorization": f"Bearer {self.ai_bridge_id.sudo().auth_token}"} else: raise ValueError(_("Unsupported authentication type.")) def _get_headers(self): """Return headers for the request.""" - return { + 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.""" From 8c7df9b0ed41ca1f55bb1e3e3f327eca37a32930 Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 31 Jul 2025 10:02:22 +0000 Subject: [PATCH 25/51] Translated using Weblate (Italian) Currently translated at 100.0% (128 of 128 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 51025bf..207bdb4 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-07-22 16:25+0000\n" +"PO-Revision-Date: 2025-07-31 10:59+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -736,12 +736,12 @@ msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids msgid "Website Messages" -msgstr "" +msgstr "Messaggi sito web" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids msgid "Website communication history" -msgstr "" +msgstr "Cronologia comunicazioni sito web" #. module: ai_oca_bridge #. odoo-python From 2dc9b8e15292b631790e7b6d6d340e470d829176 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Thu, 14 Aug 2025 13:45:07 +0000 Subject: [PATCH 26/51] Added translation using Weblate (Spanish) --- ai_oca_bridge/i18n/es.po | 727 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 ai_oca_bridge/i18n/es.po diff --git a/ai_oca_bridge/i18n/es.po b/ai_oca_bridge/i18n/es.po new file mode 100644 index 0000000..307ee39 --- /dev/null +++ b/ai_oca_bridge/i18n/es.po @@ -0,0 +1,727 @@ +# 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: 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" + +#. 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 +#, python-format +msgid "%s executed successfully." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s failed." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +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 +#, python-format +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 +#, python-format +msgid "AI Bridge Failed" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +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__usage__ai_thread_create +msgid "AI Thread Create" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "AI Thread Unlink" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "AI Thread Write" +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_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__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_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:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage +msgid "Ai Usage" +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.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 +#, python-format +msgid "Execution is expired." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +#, python-format +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_ir_model__is_ai_bridge_thread +msgid "Is Ai Bridge Thread" +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____last_update +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update +msgid "Last Modified on" +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_main_attachment_id +msgid "Main Attachment" +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,name:ai_oca_bridge.model_ir_model +msgid "Models" +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,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.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 +msgid "Record v0" +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 +#, python-format +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 +#, python-format +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 +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "" +"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +msgstr "" From 0bd5810f189a432350049d8c9682e45d77ddad58 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Thu, 14 Aug 2025 13:55:09 +0000 Subject: [PATCH 27/51] Added translation using Weblate (Spanish (Venezuela)) --- ai_oca_bridge/i18n/es_VE.po | 727 ++++++++++++++++++++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 ai_oca_bridge/i18n/es_VE.po diff --git a/ai_oca_bridge/i18n/es_VE.po b/ai_oca_bridge/i18n/es_VE.po new file mode 100644 index 0000000..0aae7b9 --- /dev/null +++ b/ai_oca_bridge/i18n/es_VE.po @@ -0,0 +1,727 @@ +# 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: 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" + +#. 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 +#, python-format +msgid "%s executed successfully." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s failed." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +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 +#, python-format +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 +#, python-format +msgid "AI Bridge Failed" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +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__usage__ai_thread_create +msgid "AI Thread Create" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "AI Thread Unlink" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "AI Thread Write" +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_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__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_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:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage +msgid "Ai Usage" +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.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 +#, python-format +msgid "Execution is expired." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +#, python-format +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_ir_model__is_ai_bridge_thread +msgid "Is Ai Bridge Thread" +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____last_update +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update +msgid "Last Modified on" +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_main_attachment_id +msgid "Main Attachment" +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,name:ai_oca_bridge.model_ir_model +msgid "Models" +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,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.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 +msgid "Record v0" +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 +#, python-format +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 +#, python-format +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 +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "" +"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +msgstr "" From 06b8d3b4ae8930aea56999e624813a1f574e3002 Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Fri, 15 Aug 2025 22:31:16 +0000 Subject: [PATCH 28/51] Translated using Weblate (Spanish) Currently translated at 100.0% (128 of 128 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/es/ --- ai_oca_bridge/i18n/es.po | 271 +++++++++++++++++++++------------------ 1 file changed, 149 insertions(+), 122 deletions(-) diff --git a/ai_oca_bridge/i18n/es.po b/ai_oca_bridge/i18n/es.po index 307ee39..fbc7b5a 100644 --- a/ai_oca_bridge/i18n/es.po +++ b/ai_oca_bridge/i18n/es.po @@ -6,13 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\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 @@ -27,136 +29,148 @@ msgid "" " 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 #, python-format msgid "%s executed successfully." -msgstr "" +msgstr "%s ejecutado correctamente." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "%s failed." -msgstr "" +msgstr "%s falló." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "%s is not active." -msgstr "" +msgstr "%s no está activo." #. module: ai_oca_bridge #: model:ir.module.category,name:ai_oca_bridge.module_category_ai msgid "AI" -msgstr "" +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 "" +msgstr "Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Executed" -msgstr "" +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 "" +msgstr "Ejecución de Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Failed" -msgstr "" +msgstr "Puente de IA fallo" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Inactive" -msgstr "" +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 "" +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 "" +msgstr "Ejecución de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create msgid "AI Thread Create" -msgstr "" +msgstr "Crear hilo de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink msgid "AI Thread Unlink" -msgstr "" +msgstr "Eliminar hilo de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write msgid "AI Thread Write" -msgstr "" +msgstr "Escribir hilo de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" -msgstr "" +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 "" +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 "" +msgstr "Activo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids msgid "Activities" -msgstr "" +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 "" +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 "" +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 "" +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 "" +msgstr "Puente de IA" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ai_bridge msgid "Ai Bridge Configuration" -msgstr "" +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 @@ -173,81 +187,81 @@ msgstr "" #: 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 "" +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 "" +msgstr "Ejecución de IA" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage msgid "Ai Usage" -msgstr "" +msgstr "Uso 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 "" +msgstr "Archivado" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__async_timeout msgid "Async Timeout" -msgstr "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Tipo de autenticación" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic msgid "Basic Authentication" -msgstr "" +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 "" +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 "" +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 "" +msgstr "Creado el" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage @@ -255,356 +269,358 @@ 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 "" +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 "" +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 "" +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 "" +msgstr "Hecho" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft msgid "Draft" -msgstr "" +msgstr "Borrador" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_mail_thread msgid "Email Thread" -msgstr "" +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 "" +msgstr "Error" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids msgid "Execution" -msgstr "" +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 "" +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 "" +msgstr "Detalles de ejecución" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Execution is expired." -msgstr "" +msgstr "La ejecución ha expirado." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Execution not found." -msgstr "" +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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Filtrar" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids msgid "Followers" -msgstr "" +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 "" +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 "" +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 "" +msgstr "Grupo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message msgid "Has Message" -msgstr "" +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 "" +msgstr "ID" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon msgid "Icon" -msgstr "" +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 "" +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 "" +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 "" +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 "" +msgstr "Inmediato" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread msgid "Is Ai Bridge Thread" -msgstr "" +msgstr "Es un hilo del puente IA" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" -msgstr "" +msgstr "Es seguidor" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update msgid "Last Modified on" -msgstr "" +msgstr "Última modificación el" #. 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 "" +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 "" +msgstr "Última actualización el" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Adjunto principal" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" -msgstr "" +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 "" +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 "" +msgstr "Modelo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model msgid "Model Name" -msgstr "" +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 "" +msgstr "Modelo requerido" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ir_model msgid "Models" -msgstr "" +msgstr "Modelos" #. 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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Número de mensajes con error de entrega" #. 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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Registro" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view msgid "Record Payload" -msgstr "" +msgstr "Carga útil de registro" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 msgid "Record v0" -msgstr "" +msgstr "Registro v0" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" -msgstr "" +msgstr "Respuesta" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view msgid "Response" -msgstr "" +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 "" +msgstr "Usuario responsable" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result msgid "Result" -msgstr "" +msgstr "Resultado" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind msgid "Result Kind" -msgstr "" +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 "" +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 "" +msgstr "Ejemplo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload msgid "Sample Payload" -msgstr "" +msgstr "Carga útil de muestra" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload @@ -612,16 +628,18 @@ 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 "" +msgstr "Secuencia" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state msgid "State" -msgstr "" +msgstr "Estado" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state @@ -631,31 +649,36 @@ msgid "" "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 "" +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 "" +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 "" +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 "" +msgstr "Hilo" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__async_timeout @@ -663,60 +686,62 @@ 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." #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token msgid "Token Authentication" -msgstr "" +msgstr "Autenticación de token" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Token is not allowed for this execution." -msgstr "" +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 "" +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 "" +msgstr "Dirección URL" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 #, python-format msgid "Unsupported authentication type." -msgstr "" +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 "" +msgstr "Uso" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id msgid "User" -msgstr "" +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 "" +msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids msgid "Website Messages" -msgstr "" +msgstr "Mensajes del sitio web" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids msgid "Website communication history" -msgstr "" +msgstr "Historia de la comunicación en la web" #. module: ai_oca_bridge #. odoo-python @@ -725,3 +750,5 @@ msgstr "" 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\"." From 4b1f8bb57c9e3282aad4a062b565a8a55411044d Mon Sep 17 00:00:00 2001 From: "Leonardo J. Caballero G" Date: Fri, 15 Aug 2025 23:18:33 +0000 Subject: [PATCH 29/51] Translated using Weblate (Spanish (Venezuela)) Currently translated at 100.0% (128 of 128 strings) Translation: ai-16.0/ai-16.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge/es_VE/ --- ai_oca_bridge/i18n/es_VE.po | 271 ++++++++++++++++++++---------------- 1 file changed, 149 insertions(+), 122 deletions(-) diff --git a/ai_oca_bridge/i18n/es_VE.po b/ai_oca_bridge/i18n/es_VE.po index 0aae7b9..f217cf5 100644 --- a/ai_oca_bridge/i18n/es_VE.po +++ b/ai_oca_bridge/i18n/es_VE.po @@ -6,13 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\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 @@ -27,136 +29,148 @@ msgid "" " 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 #, python-format msgid "%s executed successfully." -msgstr "" +msgstr "%s ejecutado correctamente." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "%s failed." -msgstr "" +msgstr "%s falló." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "%s is not active." -msgstr "" +msgstr "%s no está activo." #. module: ai_oca_bridge #: model:ir.module.category,name:ai_oca_bridge.module_category_ai msgid "AI" -msgstr "" +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 "" +msgstr "Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Executed" -msgstr "" +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 "" +msgstr "Ejecución de Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Failed" -msgstr "" +msgstr "Puente de IA fallo" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 #, python-format msgid "AI Bridge Inactive" -msgstr "" +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 "" +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 "" +msgstr "Ejecución de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create msgid "AI Thread Create" -msgstr "" +msgstr "Crear hilo de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink msgid "AI Thread Unlink" -msgstr "" +msgstr "Eliminar hilo de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write msgid "AI Thread Write" -msgstr "" +msgstr "Escribir hilo de IA" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" -msgstr "" +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 "" +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 "" +msgstr "Activo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_ids msgid "Activities" -msgstr "" +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 "" +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 "" +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 "" +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 "" +msgstr "Puente de IA" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ai_bridge msgid "Ai Bridge Configuration" -msgstr "" +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 @@ -173,81 +187,81 @@ msgstr "" #: 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 "" +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 "" +msgstr "Ejecución de IA" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage msgid "Ai Usage" -msgstr "" +msgstr "Uso 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 "" +msgstr "Archivado" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__async_timeout msgid "Async Timeout" -msgstr "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Tipo de autenticación" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic msgid "Basic Authentication" -msgstr "" +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 "" +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 "" +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 "" +msgstr "Creado el" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__usage @@ -255,356 +269,358 @@ 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 "" +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 "" +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 "" +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 "" +msgstr "Hecho" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge_execution__state__draft msgid "Draft" -msgstr "" +msgstr "Borrador" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_mail_thread msgid "Email Thread" -msgstr "" +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 "" +msgstr "Error" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__execution_ids msgid "Execution" -msgstr "" +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 "" +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 "" +msgstr "Detalles de ejecución" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Execution is expired." -msgstr "" +msgstr "La ejecución ha expirado." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Execution not found." -msgstr "" +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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Filtrar" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_follower_ids msgid "Followers" -msgstr "" +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 "" +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 "" +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 "" +msgstr "Grupo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__has_message msgid "Has Message" -msgstr "" +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 "" +msgstr "ID" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__activity_exception_icon msgid "Icon" -msgstr "" +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 "" +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 "" +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 "" +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 "" +msgstr "Inmediato" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread msgid "Is Ai Bridge Thread" -msgstr "" +msgstr "Es un hilo del puente IA" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" -msgstr "" +msgstr "Es seguidor" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update msgid "Last Modified on" -msgstr "" +msgstr "Última modificación el" #. 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 "" +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 "" +msgstr "Última actualización el" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_main_attachment_id msgid "Main Attachment" -msgstr "" +msgstr "Adjunto principal" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" -msgstr "" +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 "" +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 "" +msgstr "Modelo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__model msgid "Model Name" -msgstr "" +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 "" +msgstr "Modelo requerido" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_ir_model msgid "Models" -msgstr "" +msgstr "Modelos" #. 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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Número de mensajes con error de entrega" #. 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 "" +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 "" +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 "" +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 "" +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 "" +msgstr "Registro" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_form_view msgid "Record Payload" -msgstr "" +msgstr "Carga útil de registro" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 msgid "Record v0" -msgstr "" +msgstr "Registro v0" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" -msgstr "" +msgstr "Respuesta" #. module: ai_oca_bridge #: model_terms:ir.ui.view,arch_db:ai_oca_bridge.ai_bridge_execution_form_view msgid "Response" -msgstr "" +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 "" +msgstr "Usuario responsable" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__result msgid "Result" -msgstr "" +msgstr "Resultado" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__result_kind msgid "Result Kind" -msgstr "" +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 "" +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 "" +msgstr "Ejemplo" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__sample_payload msgid "Sample Payload" -msgstr "" +msgstr "Carga útil de muestra" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__sample_payload @@ -612,16 +628,18 @@ 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 "" +msgstr "Secuencia" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__state msgid "State" -msgstr "" +msgstr "Estado" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__activity_state @@ -631,31 +649,36 @@ msgid "" "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 "" +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 "" +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 "" +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 "" +msgstr "Hilo" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__async_timeout @@ -663,60 +686,62 @@ 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." #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token msgid "Token Authentication" -msgstr "" +msgstr "Autenticación de token" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 #, python-format msgid "Token is not allowed for this execution." -msgstr "" +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 "" +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 "" +msgstr "Dirección URL" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 #, python-format msgid "Unsupported authentication type." -msgstr "" +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 "" +msgstr "Uso" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__user_id msgid "User" -msgstr "" +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 "" +msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids msgid "Website Messages" -msgstr "" +msgstr "Mensajes del sitio web" #. module: ai_oca_bridge #: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids msgid "Website communication history" -msgstr "" +msgstr "Historia de la comunicación en la web" #. module: ai_oca_bridge #. odoo-python @@ -725,3 +750,5 @@ msgstr "" 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\"." From 9f473a654549db7c93d262876f37905aec949465 Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Thu, 21 Aug 2025 03:40:48 +0000 Subject: [PATCH 30/51] Added translation using Weblate (Portuguese (Brazil)) --- ai_oca_bridge/i18n/pt_BR.po | 727 ++++++++++++++++++++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 ai_oca_bridge/i18n/pt_BR.po diff --git a/ai_oca_bridge/i18n/pt_BR.po b/ai_oca_bridge/i18n/pt_BR.po new file mode 100644 index 0000000..6631b26 --- /dev/null +++ b/ai_oca_bridge/i18n/pt_BR.po @@ -0,0 +1,727 @@ +# 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 +#, python-format +msgid "%s executed successfully." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "%s failed." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +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 +#, python-format +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 +#, python-format +msgid "AI Bridge Failed" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +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__usage__ai_thread_create +msgid "AI Thread Create" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink +msgid "AI Thread Unlink" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write +msgid "AI Thread Write" +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_document_page__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket__ai_bridge_info +#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__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_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:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage +msgid "Ai Usage" +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.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 +#, python-format +msgid "Execution is expired." +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/controllers/ai.py:0 +#, python-format +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_ir_model__is_ai_bridge_thread +msgid "Is Ai Bridge Thread" +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____last_update +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update +msgid "Last Modified on" +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_main_attachment_id +msgid "Main Attachment" +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,name:ai_oca_bridge.model_ir_model +msgid "Models" +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,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.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 +msgid "Record v0" +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 +#, python-format +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 +#, python-format +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 +#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: ai_oca_bridge +#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: ai_oca_bridge +#. odoo-python +#: code:addons/ai_oca_bridge/models/ai_bridge.py:0 +#, python-format +msgid "" +"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +msgstr "" From e4aa55e0e5c0720aa4cad3084b2f6e47cd09297b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 25 Aug 2025 10:51:04 +0000 Subject: [PATCH 31/51] [BOT] post-merge updates --- ai_oca_bridge/README.rst | 2 +- ai_oca_bridge/__manifest__.py | 2 +- ai_oca_bridge/static/description/index.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index f167ca2..b196324 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -11,7 +11,7 @@ AI OCA Bridge !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:95324eee7973f3fc8e883c34e3e72df6337726fe9eac34f04e68145eeaed22da + !! source digest: sha256:f8915001605720e33f96f0e2a995ad8ee969e822c1ac34978800c880ed7093bd !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/ai_oca_bridge/__manifest__.py b/ai_oca_bridge/__manifest__.py index cf8a635..f09cf72 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -4,7 +4,7 @@ { "name": "AI OCA Bridge", "summary": """Makes a basic configuration to be used as bridge with external AI systems""", - "version": "16.0.2.0.1", + "version": "16.0.2.0.2", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 8aa403a..255a869 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -372,7 +372,7 @@

AI OCA Bridge

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:95324eee7973f3fc8e883c34e3e72df6337726fe9eac34f04e68145eeaed22da +!! source digest: sha256:f8915001605720e33f96f0e2a995ad8ee969e822c1ac34978800c880ed7093bd !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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 From 7e27c06ea3ce07e6620cc961259855f11289e080 Mon Sep 17 00:00:00 2001 From: Adria Hortoenda Date: Mon, 25 Aug 2025 14:43:22 +0100 Subject: [PATCH 32/51] [IMP] ai_oca_bridge: pre-commit auto fixes --- ai_oca_bridge/models/ai_bridge.py | 1 - ai_oca_bridge/models/ai_bridge_execution.py | 1 - ai_oca_bridge/models/ir_model.py | 3 +-- ai_oca_bridge/pyproject.toml | 3 +++ 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 ai_oca_bridge/pyproject.toml diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index e520a49..9862a7d 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -13,7 +13,6 @@ class AiBridge(models.Model): - _name = "ai.bridge" _inherit = ["mail.thread", "mail.activity.mixin"] _description = "Ai Bridge Configuration" diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py index 1e2e342..6453b01 100644 --- a/ai_oca_bridge/models/ai_bridge_execution.py +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -13,7 +13,6 @@ class AiBridgeExecution(models.Model): - _name = "ai.bridge.execution" _description = "Ai Execution" _order = "id desc" diff --git a/ai_oca_bridge/models/ir_model.py b/ai_oca_bridge/models/ir_model.py index 9b1ba97..9783770 100644 --- a/ai_oca_bridge/models/ir_model.py +++ b/ai_oca_bridge/models/ir_model.py @@ -5,14 +5,13 @@ class IrModel(models.Model): - _inherit = "ir.model" is_ai_bridge_thread = fields.Boolean() ai_usage = fields.Char(store=False, search="_search_ai_usage") def _reflect_model_params(self, model): - vals = super(IrModel, self)._reflect_model_params(model) + vals = super()._reflect_model_params(model) vals["is_ai_bridge_thread"] = ( isinstance(model, self.pool["ai.bridge.thread"]) and not model._abstract ) 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" From 5178663b30b062cf55da8f3bc4bd38cf1dc25ff9 Mon Sep 17 00:00:00 2001 From: Adria Hortoneda Date: Wed, 13 Aug 2025 23:26:18 +0100 Subject: [PATCH 33/51] [MIG] ai_oca_bridge: Migration to 17.0 --- ai_oca_bridge/README.rst | 25 +++--- ai_oca_bridge/__manifest__.py | 14 +-- ai_oca_bridge/i18n/ai_oca_bridge.pot | 5 -- ai_oca_bridge/i18n/it.po | 5 -- ai_oca_bridge/models/ai_bridge.py | 33 +------ ai_oca_bridge/readme/CONFIGURE.md | 9 +- ai_oca_bridge/readme/CONTRIBUTORS.md | 5 +- ai_oca_bridge/static/description/index.html | 85 +++++++++---------- .../static/src/chatter_ai_registry.esm.js | 4 + .../src/components/chatter/chatter.esm.js | 63 ++++++++------ .../chatter_topbar/chatter_topbar.xml | 12 +-- .../chatter_topbar_ai.esm.js | 20 +++-- .../chatter_topbar_ai/chatter_topbar_ai.xml | 10 +-- .../chatter_topbar_ai_item.esm.js | 13 +-- .../chatter_topbar_ai_item.xml | 4 +- .../static/tests/helpers/mock_server.esm.js | 11 +-- .../tests/web/test_ai_oca_bridge.esm.js | 31 +++++-- ai_oca_bridge/tests/test_bridge.py | 37 -------- ai_oca_bridge/views/ai_bridge.xml | 29 ++++--- ai_oca_bridge/views/ai_bridge_execution.xml | 10 +-- 20 files changed, 186 insertions(+), 239 deletions(-) create mode 100644 ai_oca_bridge/static/src/chatter_ai_registry.esm.js diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index b196324..ce88e33 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,7 +1,3 @@ -.. 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 ============= @@ -17,17 +13,17 @@ AI OCA Bridge .. |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 +.. |badge2| image:: https://img.shields.io/badge/licence-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/16.0/ai_oca_bridge + :target: https://github.com/OCA/ai/tree/17.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-16-0/ai-16-0-ai_oca_bridge + :target: https://translation.odoo-community.org/projects/ai-17-0/ai-17-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=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -91,11 +87,6 @@ Record Payload Adds a new item called record with all the fields. -Record Payload (v0) -~~~~~~~~~~~~~~~~~~~ - -Adds all the fields directly on the payload. It will be removed on 17.0. - Asynchronous and synchronous calls ---------------------------------- @@ -164,7 +155,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -187,6 +178,10 @@ Contributors - Valentín Vinagre +- `Binhex `__ + + - Adria Hortoneda + Maintainers ----------- @@ -200,6 +195,6 @@ 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. +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/__manifest__.py b/ai_oca_bridge/__manifest__.py index f09cf72..1952d52 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -3,8 +3,10 @@ { "name": "AI OCA Bridge", - "summary": """Makes a basic configuration to be used as bridge with external AI systems""", - "version": "16.0.2.0.2", + "summary": """ + Makes a basic configuration to be used as bridge with external AI systems + """, + "version": "17.0.0.0.1", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", @@ -21,15 +23,17 @@ ], "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.qunit_suite_tests": [ - "ai_oca_bridge/static/tests/web/**/*.esm.js", - ], "web.tests_assets": [ "ai_oca_bridge/static/tests/helpers/**/*.esm.js", ], + "web.qunit_suite_tests": [ + "ai_oca_bridge/static/tests/**/*.esm.js", + ("remove", "ai_oca_bridge/static/tests/helpers/**/*.esm.js"), + ], }, "application": True, } diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index ddbf4aa..18322b3 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -560,11 +560,6 @@ msgstr "" msgid "Record Payload" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 -msgid "Record v0" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 207bdb4..96f85c5 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -577,11 +577,6 @@ msgstr "Record" msgid "Record Payload" msgstr "Record carico" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 -msgid "Record v0" -msgstr "Record v0" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index 9862a7d..d8b6bf8 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -49,12 +49,8 @@ class AiBridge(models.Model): [ ("none", "No payload"), ("record", "Record"), - ("record_v0", "Record v0"), # Deprecated, use 'record' instead ], required=True, - store=True, - readonly=False, - compute="_compute_payload_type", default="record", ) result_type = fields.Selection( @@ -83,7 +79,8 @@ class AiBridge(models.Model): 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.", + "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( @@ -286,32 +283,6 @@ def _prepare_payload_record(self, record=None, **kwargs): ) ) - def _prepare_payload_record_v0(self, record=None, **kwargs): - """Prepare the payload to be sent to the AI system.""" - _logger.warning( - "The 'record_v0' payload type is deprecated. " "Use 'record' instead." - ) - 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( - { - **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() diff --git a/ai_oca_bridge/readme/CONFIGURE.md b/ai_oca_bridge/readme/CONFIGURE.md index fe6bdb9..5a509b1 100644 --- a/ai_oca_bridge/readme/CONFIGURE.md +++ b/ai_oca_bridge/readme/CONFIGURE.md @@ -21,14 +21,9 @@ On the external system, you will receive a POST payload. The data included will Adds a new item called record with all the fields. -### Record Payload (v0) - -Adds all the fields directly on the payload. -It will be removed on 17.0. - ## Asynchronous and synchronous calls -The new system allows 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. @@ -65,4 +60,4 @@ 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) \ No newline at end of file +- res_id: Id of the resource (not required) diff --git a/ai_oca_bridge/readme/CONTRIBUTORS.md b/ai_oca_bridge/readme/CONTRIBUTORS.md index 7a0afbb..a12c579 100644 --- a/ai_oca_bridge/readme/CONTRIBUTORS.md +++ b/ai_oca_bridge/readme/CONTRIBUTORS.md @@ -5,4 +5,7 @@ - [Sygel Technology](https://www.sygel.es) - Valentín Vinagre - \ No newline at end of file + +- [Binhex](https://www.binhex.cloud/) + + - Adria Hortoneda diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 255a869..b0ebab5 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +AI OCA Bridge -

+
+

AI OCA Bridge

- - -Odoo Community Association - -
-

Use Cases / Context

+

Use Cases / Context

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

    @@ -430,17 +424,17 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

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

+

Payload Configuration

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

-

General

+

General

  • _odoo: Standard data to identify the Odoo Database
  • _model: Model of the related object
  • @@ -449,16 +443,12 @@

    General

-

Record Payload

+

Record Payload

Adds a new item called record with all the fields.

-
-

Record Payload (v0)

-

Adds all the fields directly on the payload. It will be removed on 17.0.

-
-

Asynchronous and synchronous calls

+

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 @@ -473,21 +463,21 @@

Asynchronous and synchronous call automatically on the synchronous call.

-

Result processing

+

Result processing

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

-

No processing

+

No processing

In this case, the result will do nothing

-

Post a Message

+

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

+

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:

@@ -500,12 +490,12 @@

Action

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -513,23 +503,23 @@

    Known issues / Roadmap

-

Bug Tracker

+

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.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -550,11 +544,10 @@

Maintainers

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.

+

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..7ebc021 --- /dev/null +++ b/ai_oca_bridge/static/src/chatter_ai_registry.esm.js @@ -0,0 +1,4 @@ +/** @odoo-module **/ + +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 index 418b53a..80529d6 100644 --- a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js +++ b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js @@ -1,32 +1,45 @@ /** @odoo-module **/ -import {registerPatch} from "@mail/model/model_core"; +import {Chatter} from "@mail/core/web/chatter"; +import {patch} from "@web/core/utils/patch"; -registerPatch({ - name: "Chatter", - recordMethods: { - async onClickAiBridge(aiBridge) { - const saved = await this.doSaveRecord(); - if (!saved) { - return; +patch(Chatter.prototype, { + async onClickAiBridge(aiBridge) { + let saved = true; + + if (this.props.webRecord && this.props.webRecord.save) { + try { + await this.props.webRecord.save(); + } catch (error) { + saved = false; + console.error("Error saving record:", error); } - const result = await this.env.services.orm.call( - "ai.bridge", - "execute_ai_bridge", - [[aiBridge.id], this.thread.model, this.thread.id] + } + + if (!saved) { + return; + } + + const model = this.props.webRecord.resModel; + const id = this.props.webRecord.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 ); - 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 index 01f7766..5e432ba 100644 --- a/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml +++ b/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml @@ -2,17 +2,13 @@ - + 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 index 80ed03b..b482e87 100644 --- 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 @@ -1,12 +1,17 @@ /** @odoo-module **/ +import {Chatter} from "@mail/core/web/chatter"; import {ChatterAIItem} from "../chatter_topbar_ai_item/chatter_topbar_ai_item.esm"; -const {Component} = owl; +import {Component} from "@odoo/owl"; import {Dropdown} from "@web/core/dropdown/dropdown"; import {DropdownItem} from "@web/core/dropdown/dropdown_item"; -import {registerMessagingComponent} from "@mail/utils/messaging_component"; +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} */ @@ -15,10 +20,9 @@ export class ChatterAITopbar extends Component { } } -Object.assign(ChatterAITopbar, { - props: {record: Object}, - components: {Dropdown, DropdownItem, ChatterAIItem}, - template: "ai_oca_bridge.ChatterAITopbar", +patch(Chatter, { + components: { + ...Chatter.components, + ChatterAITopbar, + }, }); - -registerMessagingComponent(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 index f0a40e6..b04a9c6 100644 --- 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 @@ -1,9 +1,9 @@ - + @@ -11,13 +11,13 @@ 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 index b867db9..2aa03fe 100644 --- 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 @@ -1,12 +1,16 @@ /** @odoo-module **/ -const {Component, markup} = owl; +import {Component, markup} from "@odoo/owl"; import {usePopover} from "@web/core/popover/popover_hook"; -export class ChatterAIItemPopover extends Component {} -ChatterAIItemPopover.template = "ai_oca_bridge.ChatterAIItemPopover"; +export class ChatterAIItemPopover extends Component { + static template = "ai_oca_bridge.ChatterAIItemPopover"; +} export class ChatterAIItem extends Component { + static template = "ai_oca_bridge.ChatterAIItem"; + static props = {bridge: Object}; + setup() { super.setup(); this.popover = usePopover(); @@ -41,6 +45,3 @@ export class ChatterAIItem extends Component { } } } - -ChatterAIItem.template = "ai_oca_bridge.ChatterAIItem"; -ChatterAIItem.props = {bridge: Object}; 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 index 9393a6f..c876d9b 100644 --- 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 @@ -1,6 +1,6 @@ - + - + `, }; - const {click, openView} = await start({serverData: {views}}); + + const {openView} = await start({serverData: {views}}); await openView({ res_id: resPartnerId1, res_model: "res.partner", views: [[false, "form"]], }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); await assert.strictEqual( document.querySelectorAll(`.o_ChatterTopbar_AIButton .ai_button_selection`) .length, 1, "should have an AI button" ); + await click(".o_ChatterTopbar_AIButton .ai_button_selection"); + await new Promise((resolve) => setTimeout(resolve, 1000)); assert.strictEqual( document.querySelectorAll(`.o_ChatterTopbar_AIItem`).length, 2, "should have 2 AI Items" ); - await click(document.querySelectorAll(".o_ChatterTopbar_AIItem")[0]); + + await click(".dropdown-item:first-child"); + await new Promise((resolve) => setTimeout(resolve, 1000)); assert.strictEqual( document.querySelectorAll(`.o_notification_manager .o_notification`).length, 1, @@ -50,7 +60,8 @@ QUnit.test("AI Notification", async function (assert) { ); }); -QUnit.test("AI Action", async function (assert) { +QUnit.test("AI Action", async (assert) => { + console.log("Starting AI Action test..."); const pyEnv = await startServer(); const resPartnerId1 = pyEnv["res.partner"].create({ ai_bridge_info: [ @@ -58,6 +69,7 @@ QUnit.test("AI Action", async function (assert) { {name: "AI 2", id: 2}, ], }); + const views = { "res.partner,false,form": `
@@ -71,25 +83,32 @@ QUnit.test("AI Action", async function (assert) { `, }; - const {click, openView} = await start({serverData: {views}}); + + const {openView} = await start({serverData: {views}}); await openView({ res_id: resPartnerId1, res_model: "res.partner", views: [[false, "form"]], }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); await assert.strictEqual( document.querySelectorAll(`.o_ChatterTopbar_AIButton .ai_button_selection`) .length, 1, "should have an AI button" ); + await click(".o_ChatterTopbar_AIButton .ai_button_selection"); + await new Promise((resolve) => setTimeout(resolve, 1000)); assert.strictEqual( document.querySelectorAll(`.o_ChatterTopbar_AIItem`).length, 2, "should have 2 AI Items" ); - await click(document.querySelectorAll(".o_ChatterTopbar_AIItem")[1]); + + await click(".dropdown-item:nth-child(2)"); + await new Promise((resolve) => setTimeout(resolve, 1000)); assert.strictEqual( document.querySelectorAll(`.o_list_view`).length, 1, diff --git a/ai_oca_bridge/tests/test_bridge.py b/ai_oca_bridge/tests/test_bridge.py index 7a06239..0e2797e 100644 --- a/ai_oca_bridge/tests/test_bridge.py +++ b/ai_oca_bridge/tests/test_bridge.py @@ -67,43 +67,6 @@ def test_bridge_none_auth(self): self.assertEqual(execution.res_id, self.partner.id) self.assertNotIn("name", execution.payload) - def test_bridge_none_auth_fields_record_v0(self): - self.bridge.write( - { - "payload_type": "record_v0", - "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( - [("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( - [("ai_bridge_id", "=", self.bridge.id)] - ) - ) - execution = self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] - ) - self.assertEqual(execution.res_id, self.partner.id) - self.assertIn("name", execution.payload) - self.assertEqual(execution.payload["name"], self.partner.name) - self.assertEqual(1, self.bridge.execution_count) - def test_bridge_none_auth_fields_record(self): self.bridge.write( { diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml index a60ef4a..90cc573 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -13,7 +13,7 @@ name="web_ribbon" title="Archived" bg_color="bg-danger" - attrs="{'invisible': [('active', '=', True)]}" + invisible="active" />

@@ -26,16 +26,16 @@ - + @@ -43,30 +43,33 @@ - + - + ai.bridge - + diff --git a/ai_oca_bridge/views/ai_bridge_execution.xml b/ai_oca_bridge/views/ai_bridge_execution.xml index 47c3e48..39a90c0 100644 --- a/ai_oca_bridge/views/ai_bridge_execution.xml +++ b/ai_oca_bridge/views/ai_bridge_execution.xml @@ -22,17 +22,13 @@ - + - + - + From d936cc574a6194f415dd652cf98da66f42923e20 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 26 Aug 2025 08:51:56 +0000 Subject: [PATCH 34/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 31 ++++------------------------ 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index 18322b3..02313c3 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" +"Project-Id-Version: Odoo Server 17.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -159,14 +159,12 @@ 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_document_page__ai_bridge_info -#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket__ai_bridge_info -#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_mail_blacklist__ai_bridge_info -#: model:ir.model.fields,field_description:ai_oca_bridge.field_mail_channel__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 @@ -413,12 +411,6 @@ msgstr "" msgid "Is Follower" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update -msgid "Last Modified on" -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 @@ -431,11 +423,6 @@ msgstr "" msgid "Last Updated on" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_main_attachment_id -msgid "Main Attachment" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" @@ -655,7 +642,7 @@ msgstr "" #: 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." +"complete within this time, it will be considered failed." msgstr "" #. module: ai_oca_bridge @@ -702,16 +689,6 @@ msgstr "" msgid "User groups allowed to use this AI bridge." msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website Messages" -msgstr "" - -#. module: ai_oca_bridge -#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website communication history" -msgstr "" - #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 From 9277195fadd7282c5c19626c4e00c566c5c5aa87 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 26 Aug 2025 08:55:29 +0000 Subject: [PATCH 35/51] [BOT] post-merge updates --- ai_oca_bridge/README.rst | 8 +++- ai_oca_bridge/static/description/index.html | 50 ++++++++++++--------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index ce88e33..0fe0f9f 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,3 +1,7 @@ +.. 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 ============= @@ -7,13 +11,13 @@ AI OCA Bridge !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:f8915001605720e33f96f0e2a995ad8ee969e822c1ac34978800c880ed7093bd + !! source digest: sha256:16a3e502d474d1134144ad2a4cdcba23e6404aee1a56acbf76c39541315eba45 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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/licence-AGPL--3-blue.png +.. |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 diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index b0ebab5..4e74b62 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -AI OCA Bridge +README.rst -
-

AI OCA Bridge

+
+ + +Odoo Community Association + +
+

AI OCA Bridge

-

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

+

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

@@ -403,7 +408,7 @@

AI OCA Bridge

-

Use Cases / Context

+

Use Cases / Context

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

    @@ -424,17 +429,17 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

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

+

Payload Configuration

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

-

General

+

General

  • _odoo: Standard data to identify the Odoo Database
  • _model: Model of the related object
  • @@ -443,12 +448,12 @@

    General

-

Record Payload

+

Record Payload

Adds a new item called record with all the fields.

-

Asynchronous and synchronous calls

+

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 @@ -463,21 +468,21 @@

Asynchronous and synchronous call automatically on the synchronous call.

-

Result processing

+

Result processing

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

-

No processing

+

No processing

In this case, the result will do nothing

-

Post a Message

+

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

+

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:

@@ -490,12 +495,12 @@

Action

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -503,7 +508,7 @@

    Known issues / Roadmap

-

Bug Tracker

+

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 @@ -511,15 +516,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -549,5 +554,6 @@

Maintainers

+
From 6ebf40984e498ad469f5dfee5e761e90ecbd4fd5 Mon Sep 17 00:00:00 2001 From: Luis Rodriguez Date: Wed, 3 Sep 2025 16:29:38 +0200 Subject: [PATCH 36/51] [MIG] ai_oca_bridge: Migration to 18.0 Co-authored-by: Enric Tobella --- ai_oca_bridge/README.rst | 16 +-- ai_oca_bridge/__manifest__.py | 10 +- ai_oca_bridge/models/mail_thread.py | 4 +- ai_oca_bridge/static/description/index.html | 52 ++++---- .../static/src/chatter_ai_registry.esm.js | 2 - .../src/components/chatter/chatter.esm.js | 4 +- .../chatter_topbar/chatter_topbar.xml | 2 +- .../chatter_topbar_ai.esm.js | 4 +- .../chatter_topbar_ai/chatter_topbar_ai.xml | 37 +++--- .../chatter_topbar_ai_item.esm.js | 2 - .../static/tests/helpers/mock_server.esm.js | 33 ----- .../tests/helpers/model_definition.esm.js | 29 ----- .../tests/mock_server/define_ai_models.esm.js | 14 +++ .../mock_server/mock_models/ai_bridge.esm.js | 43 +++++++ .../tests/web/test_ai_oca_bridge.esm.js | 117 ------------------ .../tests/web/test_ai_oca_bridge.test.js | 105 ++++++++++++++++ ai_oca_bridge/tests/test_frontend.py | 19 +-- ai_oca_bridge/tests/test_mixin.py | 2 +- ai_oca_bridge/views/ai_bridge.xml | 14 +-- ai_oca_bridge/views/ai_bridge_execution.xml | 8 +- ai_oca_bridge/views/menu.xml | 1 - 21 files changed, 238 insertions(+), 280 deletions(-) delete mode 100644 ai_oca_bridge/static/tests/helpers/mock_server.esm.js delete mode 100644 ai_oca_bridge/static/tests/helpers/model_definition.esm.js create mode 100644 ai_oca_bridge/static/tests/mock_server/define_ai_models.esm.js create mode 100644 ai_oca_bridge/static/tests/mock_server/mock_models/ai_bridge.esm.js delete mode 100644 ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js create mode 100644 ai_oca_bridge/static/tests/web/test_ai_oca_bridge.test.js diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index 0fe0f9f..1b3c72d 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,7 +1,3 @@ -.. 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 ============= @@ -17,17 +13,17 @@ AI OCA Bridge .. |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 +.. |badge2| image:: https://img.shields.io/badge/licence-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/17.0/ai_oca_bridge + :target: https://github.com/OCA/ai/tree/18.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-17-0/ai-17-0-ai_oca_bridge + :target: https://translation.odoo-community.org/projects/ai-18-0/ai-18-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=17.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -159,7 +155,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -199,6 +195,6 @@ 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. +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/__manifest__.py b/ai_oca_bridge/__manifest__.py index 1952d52..f0d8ea2 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ Makes a basic configuration to be used as bridge with external AI systems """, - "version": "17.0.0.0.1", + "version": "18.0.1.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", @@ -27,12 +27,8 @@ "ai_oca_bridge/static/src/**/*.xml", "ai_oca_bridge/static/src/**/*.esm.js", ], - "web.tests_assets": [ - "ai_oca_bridge/static/tests/helpers/**/*.esm.js", - ], - "web.qunit_suite_tests": [ - "ai_oca_bridge/static/tests/**/*.esm.js", - ("remove", "ai_oca_bridge/static/tests/helpers/**/*.esm.js"), + "web.assets_unit_tests": [ + "ai_oca_bridge/static/tests/**/*", ], }, "application": True, diff --git a/ai_oca_bridge/models/mail_thread.py b/ai_oca_bridge/models/mail_thread.py index f3a3f90..1bcc815 100644 --- a/ai_oca_bridge/models/mail_thread.py +++ b/ai_oca_bridge/models/mail_thread.py @@ -39,7 +39,7 @@ def get_view(self, view_id=None, view_type="form", **options): # We need to copy, because it is a frozen dict all_models = res["models"].copy() - for node in doc.xpath("/form/div[hasclass('oe_chatter')]"): + for node in doc.xpath("/form/chatter"): # _add_tier_validation_label process new_node = etree.fromstring( "" @@ -60,7 +60,7 @@ def get_view(self, view_id=None, view_type="form", **options): def _get_view_fields(self, view_type, models): """ We need to add this in order to fix the usage of form opening from - trees inside a form + lists inside a form """ result = super()._get_view_fields(view_type, models) if view_type == "form": diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 4e74b62..d4e8421 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +AI OCA Bridge -
+
+

AI OCA Bridge

- - -Odoo Community Association - -
-

AI OCA Bridge

-

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

+

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

@@ -408,7 +403,7 @@

AI OCA Bridge

-

Use Cases / Context

+

Use Cases / Context

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

    @@ -429,17 +424,17 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

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

+

Payload Configuration

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

-

General

+

General

  • _odoo: Standard data to identify the Odoo Database
  • _model: Model of the related object
  • @@ -448,12 +443,12 @@

    General

-

Record Payload

+

Record Payload

Adds a new item called record with all the fields.

-

Asynchronous and synchronous calls

+

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 @@ -468,21 +463,21 @@

Asynchronous and synchronous call automatically on the synchronous call.

-

Result processing

+

Result processing

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

-

No processing

+

No processing

In this case, the result will do nothing

-

Post a Message

+

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

+

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:

@@ -495,12 +490,12 @@

Action

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -508,23 +503,23 @@

    Known issues / Roadmap

-

Bug Tracker

+

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.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -549,11 +544,10 @@

Maintainers

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.

+

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 index 7ebc021..a674114 100644 --- a/ai_oca_bridge/static/src/chatter_ai_registry.esm.js +++ b/ai_oca_bridge/static/src/chatter_ai_registry.esm.js @@ -1,4 +1,2 @@ -/** @odoo-module **/ - 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 index 80529d6..e3aa2c2 100644 --- a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js +++ b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js @@ -1,6 +1,4 @@ -/** @odoo-module **/ - -import {Chatter} from "@mail/core/web/chatter"; +import {Chatter} from "@mail/chatter/web_portal/chatter"; import {patch} from "@web/core/utils/patch"; patch(Chatter.prototype, { 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 index 5e432ba..333bf88 100644 --- a/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml +++ b/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml @@ -5,7 +5,7 @@ t-inherit="mail.Chatter" t-inherit-mode="extension" > - + - - - - - + + + + - - + + + + 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 index 2aa03fe..b299780 100644 --- 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 @@ -1,5 +1,3 @@ -/** @odoo-module **/ - import {Component, markup} from "@odoo/owl"; import {usePopover} from "@web/core/popover/popover_hook"; diff --git a/ai_oca_bridge/static/tests/helpers/mock_server.esm.js b/ai_oca_bridge/static/tests/helpers/mock_server.esm.js deleted file mode 100644 index 42b00bb..0000000 --- a/ai_oca_bridge/static/tests/helpers/mock_server.esm.js +++ /dev/null @@ -1,33 +0,0 @@ -/** @odoo-module **/ - -import {MockServer} from "@web/../tests/helpers/mock_server"; -import {patch} from "@web/core/utils/patch"; - -patch(MockServer.prototype, { - async performRPC(route, args) { - if (args.model === "ai.bridge" && args.method === "execute_ai_bridge") { - const record = this.models["ai.bridge"].records.filter( - (r) => r.id === args.args[0][0] - ); - if (record && record[0].result_type === "action") { - return { - action: { - type: "ir.actions.act_window", - res_model: "res.partner", - views: [[false, "tree"]], - }, - }; - } - return { - notification: { - body: "Mocked AI Bridge Response", - args: { - type: "info", - title: "AI Bridge Notification", - }, - }, - }; - } - return await super.performRPC(...arguments); - }, -}); diff --git a/ai_oca_bridge/static/tests/helpers/model_definition.esm.js b/ai_oca_bridge/static/tests/helpers/model_definition.esm.js deleted file mode 100644 index ee1a109..0000000 --- a/ai_oca_bridge/static/tests/helpers/model_definition.esm.js +++ /dev/null @@ -1,29 +0,0 @@ -/** @odoo-module **/ - -import { - addModelNamesToFetch, - insertModelFields, - insertRecords, -} from "@bus/../tests/helpers/model_definitions_helpers"; - -addModelNamesToFetch(["ai.bridge"]); - -insertModelFields("res.partner", { - ai_bridge_info: {default: [], type: "json"}, -}); -insertModelFields("ai.bridge", { - result_type: { - default: "none", - type: "selection", - selection: [ - ["none", "None"], - ["action", "Action"], - ["notification", "Notification"], - ], - }, - name: {string: "Name", type: "char"}, -}); -insertRecords("ai.bridge", [ - {id: 1, name: "Test AI Bridge", result_type: "none"}, - {id: 2, name: "Test AI Bridge Action", result_type: "action"}, -]); 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..fb5e3e3 --- /dev/null +++ b/ai_oca_bridge/static/tests/mock_server/mock_models/ai_bridge.esm.js @@ -0,0 +1,43 @@ +/* + 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 = [ + {id: 1, name: "Test AI Bridge", result_type: "none"}, + {id: 2, name: "Test AI Bridge Action", result_type: "action"}, + ]; + 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.esm.js b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js deleted file mode 100644 index 4a1e627..0000000 --- a/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.esm.js +++ /dev/null @@ -1,117 +0,0 @@ -/** @odoo-module */ -/* global QUnit */ -/* - Copyright 2025 Dixmit - License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -*/ -import {click} from "@web/../tests/utils"; -import {start} from "@mail/../tests/helpers/test_utils"; -import {startServer} from "@bus/../tests/helpers/mock_python_environment"; - -QUnit.module("ai_oca_bridge"); - -QUnit.test("AI Notification", async function (assert) { - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv["res.partner"].create({ - ai_bridge_info: [ - {name: "AI 1", id: 1, description: "test1 description"}, - {name: "AI 2", id: 2}, - ], - }); - const views = { - "res.partner,false,form": ` - -
- - -
- `, - }; - - const {openView} = await start({serverData: {views}}); - await openView({ - res_id: resPartnerId1, - res_model: "res.partner", - views: [[false, "form"]], - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - await assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_AIButton .ai_button_selection`) - .length, - 1, - "should have an AI button" - ); - - await click(".o_ChatterTopbar_AIButton .ai_button_selection"); - await new Promise((resolve) => setTimeout(resolve, 1000)); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_AIItem`).length, - 2, - "should have 2 AI Items" - ); - - await click(".dropdown-item:first-child"); - await new Promise((resolve) => setTimeout(resolve, 1000)); - assert.strictEqual( - document.querySelectorAll(`.o_notification_manager .o_notification`).length, - 1, - "should have 1 Notification after clicking on AI Item" - ); -}); - -QUnit.test("AI Action", async (assert) => { - console.log("Starting AI Action test..."); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv["res.partner"].create({ - ai_bridge_info: [ - {name: "AI 1", id: 1, description: "test1 description"}, - {name: "AI 2", id: 2}, - ], - }); - - const views = { - "res.partner,false,form": `
- -
- - -
- `, - "res.partner,false,tree": ` - - - `, - }; - - const {openView} = await start({serverData: {views}}); - await openView({ - res_id: resPartnerId1, - res_model: "res.partner", - views: [[false, "form"]], - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - await assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_AIButton .ai_button_selection`) - .length, - 1, - "should have an AI button" - ); - - await click(".o_ChatterTopbar_AIButton .ai_button_selection"); - await new Promise((resolve) => setTimeout(resolve, 1000)); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_AIItem`).length, - 2, - "should have 2 AI Items" - ); - - await click(".dropdown-item:nth-child(2)"); - await new Promise((resolve) => setTimeout(resolve, 1000)); - assert.strictEqual( - document.querySelectorAll(`.o_list_view`).length, - 1, - "should have 1 List View after clicking on AI Item with action" - ); -}); 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..0c1a773 --- /dev/null +++ b/ai_oca_bridge/static/tests/web/test_ai_oca_bridge.test.js @@ -0,0 +1,105 @@ +/* + 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, models, mountView} 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 models.Model { + _name = "res.partner"; + ai_bridge_info = fields.Generic({default: []}); +} + +defineModels([ResPartner]); +defineBaseAIModels(); + +test("AI Notification", async () => { + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ + name: "Awesome partner", + ai_bridge_info: [ + {id: 1, name: "AI 1", description: "test1 description"}, + {id: 2, 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 partnerId = pyEnv["res.partner"].create({ + name: "Awesome partner", + ai_bridge_info: [ + {id: 1, name: "AI 1", description: "test1 description"}, + {id: 2, 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/test_frontend.py b/ai_oca_bridge/tests/test_frontend.py index 74947d6..2bbeece 100644 --- a/ai_oca_bridge/tests/test_frontend.py +++ b/ai_oca_bridge/tests/test_frontend.py @@ -1,12 +1,17 @@ # Copyright 2025 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import odoo -from odoo.tests import common +from odoo.addons.web.tests.test_js import WebSuite -@common.tagged("post_install", "-at_install") -class TestFrontend(common.HttpCase): - def test_javascript(self): - self.browser_js( - "/web/tests?module=ai_oca_bridge", "", login="admin", timeout=1800 - ) +@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 index afe76f5..f08ddb8 100644 --- a/ai_oca_bridge/tests/test_mixin.py +++ b/ai_oca_bridge/tests/test_mixin.py @@ -6,7 +6,7 @@ from odoo_test_helper import FakeModelLoader from odoo.exceptions import ValidationError -from odoo.tests.common import Form, TransactionCase +from odoo.tests import Form, TransactionCase class TestBridge(TransactionCase): diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml index 90cc573..5809f80 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -2,7 +2,6 @@ - ai.bridge @@ -88,11 +87,7 @@ -
- - - -
+
@@ -123,21 +118,21 @@ ai.bridge - + - + AI Bridge ai.bridge - tree,form + list,form [] {} @@ -148,5 +143,4 @@ -
diff --git a/ai_oca_bridge/views/ai_bridge_execution.xml b/ai_oca_bridge/views/ai_bridge_execution.xml index 39a90c0..5200ca7 100644 --- a/ai_oca_bridge/views/ai_bridge_execution.xml +++ b/ai_oca_bridge/views/ai_bridge_execution.xml @@ -2,7 +2,6 @@ - ai.bridge.execution @@ -55,21 +54,21 @@ ai.bridge.execution - + - + AI Execution ai.bridge.execution - tree,form + list,form [] {} @@ -80,5 +79,4 @@ - diff --git a/ai_oca_bridge/views/menu.xml b/ai_oca_bridge/views/menu.xml index ecc9272..70a6811 100644 --- a/ai_oca_bridge/views/menu.xml +++ b/ai_oca_bridge/views/menu.xml @@ -13,5 +13,4 @@ web_icon="ai_oca_bridge,static/description/icon.png" sequence="40" /> - From 17be27cdcb865aaedee9fbcced216aab8cd10fcf Mon Sep 17 00:00:00 2001 From: oca-ci Date: Wed, 10 Sep 2025 04:39:19 +0000 Subject: [PATCH 37/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index 02313c3..6e7432e 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 17.0\n" +"Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -30,21 +30,18 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s executed successfully." msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s failed." msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s is not active." msgstr "" @@ -63,7 +60,6 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Executed" msgstr "" @@ -75,14 +71,12 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Failed" msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Inactive" msgstr "" @@ -309,14 +303,12 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Execution is expired." msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Execution not found." msgstr "" @@ -653,7 +645,6 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Token is not allowed for this execution." msgstr "" @@ -670,7 +661,6 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 -#, python-format msgid "Unsupported authentication type." msgstr "" @@ -692,7 +682,6 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "" "When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." msgstr "" From 6f8dfd33aadef82906a39215266673e5765dbca9 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 10 Sep 2025 04:41:33 +0000 Subject: [PATCH 38/51] [BOT] post-merge updates --- ai_oca_bridge/README.rst | 8 +++- ai_oca_bridge/static/description/index.html | 50 ++++++++++++--------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index 1b3c72d..cfe05c8 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,3 +1,7 @@ +.. 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 ============= @@ -7,13 +11,13 @@ AI OCA Bridge !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:16a3e502d474d1134144ad2a4cdcba23e6404aee1a56acbf76c39541315eba45 + !! source digest: sha256:72deaa130a35eca64ce4bea745a612bb4d9a7271bdf7f9f9e016b0e7a47932a6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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/licence-AGPL--3-blue.png +.. |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 diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index d4e8421..d8b00b4 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -AI OCA Bridge +README.rst -
-

AI OCA Bridge

+
+ + +Odoo Community Association + +
+

AI OCA Bridge

-

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

+

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

@@ -403,7 +408,7 @@

AI OCA Bridge

-

Use Cases / Context

+

Use Cases / Context

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

    @@ -424,17 +429,17 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

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

+

Payload Configuration

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

-

General

+

General

  • _odoo: Standard data to identify the Odoo Database
  • _model: Model of the related object
  • @@ -443,12 +448,12 @@

    General

-

Record Payload

+

Record Payload

Adds a new item called record with all the fields.

-

Asynchronous and synchronous calls

+

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 @@ -463,21 +468,21 @@

Asynchronous and synchronous call automatically on the synchronous call.

-

Result processing

+

Result processing

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

-

No processing

+

No processing

In this case, the result will do nothing

-

Post a Message

+

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

+

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:

@@ -490,12 +495,12 @@

Action

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -503,7 +508,7 @@

    Known issues / Roadmap

-

Bug Tracker

+

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 @@ -511,15 +516,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -549,5 +554,6 @@

Maintainers

+
From 2b3a179d8cf383229c17f60e2fa0bf1d437bc3e3 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 18 Sep 2025 13:49:03 +0000 Subject: [PATCH 39/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index 6e7432e..3f25431 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -154,6 +154,7 @@ 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 From 222c31239b0f6fff55981fe824aa85f67fd3581d Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 18 Sep 2025 13:51:42 +0000 Subject: [PATCH 40/51] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: ai-18.0/ai-18.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_oca_bridge/ --- ai_oca_bridge/i18n/es.po | 78 +++++++++++++++---------------------- ai_oca_bridge/i18n/es_VE.po | 78 +++++++++++++++---------------------- ai_oca_bridge/i18n/it.po | 60 +++++++++++----------------- ai_oca_bridge/i18n/pt_BR.po | 50 ++++-------------------- 4 files changed, 91 insertions(+), 175 deletions(-) diff --git a/ai_oca_bridge/i18n/es.po b/ai_oca_bridge/i18n/es.po index fbc7b5a..aed3284 100644 --- a/ai_oca_bridge/i18n/es.po +++ b/ai_oca_bridge/i18n/es.po @@ -21,10 +21,12 @@ msgstr "" 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" +" - '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" +" 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" " " @@ -45,21 +47,18 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s executed successfully." msgstr "%s ejecutado correctamente." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s failed." msgstr "%s falló." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s is not active." msgstr "%s no está activo." @@ -78,7 +77,6 @@ msgstr "Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Executed" msgstr "Puente de IA ejecutado" @@ -90,14 +88,12 @@ msgstr "Ejecución de Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format 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 -#, python-format msgid "AI Bridge Inactive" msgstr "Puente de IA inactivo" @@ -174,14 +170,13 @@ 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_helpdesk_ticket__ai_bridge_info -#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__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 @@ -328,14 +323,12 @@ msgstr "Detalles de ejecución" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format 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 -#, python-format msgid "Execution not found." msgstr "No se ha encontrado la ejecución." @@ -430,12 +423,6 @@ msgstr "Es un hilo del puente IA" msgid "Is Follower" msgstr "Es seguidor" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update -msgid "Last Modified on" -msgstr "Última modificación el" - #. 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 @@ -448,11 +435,6 @@ msgstr "Última actualización por" 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_main_attachment_id -msgid "Main Attachment" -msgstr "Adjunto principal" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" @@ -577,11 +559,6 @@ msgstr "Registro" msgid "Record Payload" msgstr "Carga útil de registro" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 -msgid "Record v0" -msgstr "Registro v0" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" @@ -684,10 +661,8 @@ msgstr "Hilo" #: 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." +"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." #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token @@ -697,7 +672,6 @@ msgstr "Autenticación de token" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Token is not allowed for this execution." msgstr "Token no está permitido para esta ejecución." @@ -714,7 +688,6 @@ msgstr "Dirección URL" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 -#, python-format msgid "Unsupported authentication type." msgstr "Tipo de autenticación no admitido." @@ -733,22 +706,33 @@ msgstr "Usuario" msgid "User groups allowed to use this AI bridge." msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website Messages" -msgstr "Mensajes del sitio web" - -#. module: ai_oca_bridge -#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website communication history" -msgstr "Historia de la comunicación en la web" - #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format 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\"." +"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 index f217cf5..e1e0794 100644 --- a/ai_oca_bridge/i18n/es_VE.po +++ b/ai_oca_bridge/i18n/es_VE.po @@ -21,10 +21,12 @@ msgstr "" 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" +" - '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" +" 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" " " @@ -45,21 +47,18 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s executed successfully." msgstr "%s ejecutado correctamente." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s failed." msgstr "%s falló." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s is not active." msgstr "%s no está activo." @@ -78,7 +77,6 @@ msgstr "Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Executed" msgstr "Puente de IA ejecutado" @@ -90,14 +88,12 @@ msgstr "Ejecución de Puente de IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format 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 -#, python-format msgid "AI Bridge Inactive" msgstr "Puente de IA inactivo" @@ -174,14 +170,13 @@ 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_helpdesk_ticket__ai_bridge_info -#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__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 @@ -328,14 +323,12 @@ msgstr "Detalles de ejecución" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format 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 -#, python-format msgid "Execution not found." msgstr "No se ha encontrado la ejecución." @@ -430,12 +423,6 @@ msgstr "Es un hilo del puente IA" msgid "Is Follower" msgstr "Es seguidor" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update -msgid "Last Modified on" -msgstr "Última modificación el" - #. 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 @@ -448,11 +435,6 @@ msgstr "Última actualización por" 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_main_attachment_id -msgid "Main Attachment" -msgstr "Adjunto principal" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" @@ -577,11 +559,6 @@ msgstr "Registro" msgid "Record Payload" msgstr "Carga útil de registro" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 -msgid "Record v0" -msgstr "Registro v0" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" @@ -684,10 +661,8 @@ msgstr "Hilo" #: 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." +"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." #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__token @@ -697,7 +672,6 @@ msgstr "Autenticación de token" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Token is not allowed for this execution." msgstr "Token no está permitido para esta ejecución." @@ -714,7 +688,6 @@ msgstr "Dirección URL" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 -#, python-format msgid "Unsupported authentication type." msgstr "Tipo de autenticación no admitido." @@ -733,22 +706,33 @@ msgstr "Usuario" msgid "User groups allowed to use this AI bridge." msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website Messages" -msgstr "Mensajes del sitio web" - -#. module: ai_oca_bridge -#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website communication history" -msgstr "Historia de la comunicación en la web" - #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format 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\"." +"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 index 96f85c5..558a245 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -45,21 +45,18 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format 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 -#, python-format msgid "%s failed." msgstr "%s fallito." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s is not active." msgstr "%s non è attivo." @@ -78,7 +75,6 @@ msgstr "Collegamento IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Executed" msgstr "Collegamento IA eseguito" @@ -90,14 +86,12 @@ msgstr "Esecuzione collegamento IA" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Failed" msgstr "Collegamento IA fallito" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Inactive" msgstr "Collegamento IA inattivo" @@ -174,14 +168,13 @@ 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_helpdesk_ticket__ai_bridge_info -#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__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 @@ -328,14 +321,12 @@ msgstr "Dettagli esecuzione" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Execution is expired." msgstr "L'esecuzione è scaduta." #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Execution not found." msgstr "Esecuzione non trovata." @@ -430,12 +421,6 @@ msgstr "È un thread IA" msgid "Is Follower" msgstr "Segue" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update -msgid "Last Modified on" -msgstr "Ultima modifica il" - #. 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 @@ -448,11 +433,6 @@ msgstr "Ultimo aggiornamento di" 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_main_attachment_id -msgid "Main Attachment" -msgstr "Allegato principale" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" @@ -679,10 +659,8 @@ msgstr "Discussione" #: 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." +"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 @@ -692,7 +670,6 @@ msgstr "Autenticazione token" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Token is not allowed for this execution." msgstr "Il token non è autorizzato per questa esecuzione." @@ -709,7 +686,6 @@ msgstr "URL" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 -#, python-format msgid "Unsupported authentication type." msgstr "Tipo autenticazione non supportato." @@ -728,22 +704,30 @@ msgstr "Utente" msgid "User groups allowed to use this AI bridge." msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website Messages" -msgstr "Messaggi sito web" - -#. module: ai_oca_bridge -#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website communication history" -msgstr "Cronologia comunicazioni sito web" - #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format 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 index 6631b26..b14cc9a 100644 --- a/ai_oca_bridge/i18n/pt_BR.po +++ b/ai_oca_bridge/i18n/pt_BR.po @@ -19,10 +19,12 @@ msgstr "" 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" +" - '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" +" 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" " " @@ -31,21 +33,18 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s executed successfully." msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s failed." msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "%s is not active." msgstr "" @@ -64,7 +63,6 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Executed" msgstr "" @@ -76,14 +74,12 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Failed" msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "AI Bridge Inactive" msgstr "" @@ -160,14 +156,13 @@ 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_helpdesk_ticket__ai_bridge_info -#: model:ir.model.fields,field_description:ai_oca_bridge.field_helpdesk_ticket_team__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_channel__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 @@ -312,14 +307,12 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Execution is expired." msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Execution not found." msgstr "" @@ -414,12 +407,6 @@ msgstr "" msgid "Is Follower" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge____last_update -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution____last_update -msgid "Last Modified on" -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 @@ -432,11 +419,6 @@ msgstr "" msgid "Last Updated on" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_main_attachment_id -msgid "Main Attachment" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_has_error msgid "Message Delivery error" @@ -561,11 +543,6 @@ msgstr "" msgid "Record Payload" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__payload_type__record_v0 -msgid "Record v0" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__res_id msgid "Res" @@ -661,7 +638,7 @@ msgstr "" #: 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." +"complete within this time, it will be considered failed." msgstr "" #. module: ai_oca_bridge @@ -672,7 +649,6 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/controllers/ai.py:0 -#, python-format msgid "Token is not allowed for this execution." msgstr "" @@ -689,7 +665,6 @@ msgstr "" #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge_execution.py:0 -#, python-format msgid "Unsupported authentication type." msgstr "" @@ -708,20 +683,9 @@ msgstr "" msgid "User groups allowed to use this AI bridge." msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website Messages" -msgstr "" - -#. module: ai_oca_bridge -#: model:ir.model.fields,help:ai_oca_bridge.field_ai_bridge__website_message_ids -msgid "Website communication history" -msgstr "" - #. module: ai_oca_bridge #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 -#, python-format msgid "" "When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." msgstr "" From ca902b93291ae2619ca089a68fa9ebce85a93a22 Mon Sep 17 00:00:00 2001 From: mymage Date: Fri, 26 Sep 2025 16:14:21 +0000 Subject: [PATCH 41/51] Translated using Weblate (Italian) Currently translated at 100.0% (123 of 123 strings) Translation: ai-18.0/ai-18.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 558a245..76c5de1 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-07-31 10:59+0000\n" +"PO-Revision-Date: 2025-09-26 16:20+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -661,6 +661,8 @@ 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 From 7528974ec9d07a6d812b624727b5101774d222c6 Mon Sep 17 00:00:00 2001 From: Ariel Barreiros <50119000+arielbarreiros96@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:05:26 +0100 Subject: [PATCH 42/51] [IMP] ai_oca_bridge thread models selection --- ai_oca_bridge/README.rst | 7 +- ai_oca_bridge/models/__init__.py | 2 +- ai_oca_bridge/models/ai_bridge.py | 8 +- ai_oca_bridge/models/ai_bridge_thread.py | 75 ++----------------- ai_oca_bridge/models/base.py | 82 +++++++++++++++++++++ ai_oca_bridge/models/ir_model.py | 27 ------- ai_oca_bridge/readme/CONTRIBUTORS.md | 1 + ai_oca_bridge/static/description/index.html | 49 ++++++------ ai_oca_bridge/tests/fake_models.py | 1 - ai_oca_bridge/tests/test_mixin.py | 74 +++++++++++++++---- ai_oca_bridge/views/ai_bridge.xml | 1 - 11 files changed, 178 insertions(+), 149 deletions(-) create mode 100644 ai_oca_bridge/models/base.py delete mode 100644 ai_oca_bridge/models/ir_model.py diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index cfe05c8..d368c14 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,7 +1,3 @@ -.. 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 ============= @@ -17,7 +13,7 @@ AI OCA Bridge .. |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 +.. |badge2| image:: https://img.shields.io/badge/licence-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 @@ -184,6 +180,7 @@ Contributors - `Binhex `__ + - Ariel Barreiros - Adria Hortoneda Maintainers diff --git a/ai_oca_bridge/models/__init__.py b/ai_oca_bridge/models/__init__.py index 226066a..a2e52e2 100644 --- a/ai_oca_bridge/models/__init__.py +++ b/ai_oca_bridge/models/__init__.py @@ -2,4 +2,4 @@ from . import ai_bridge from . import ai_bridge_execution from . import mail_thread -from . import ir_model +from . import base diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index d8b6bf8..5da2806 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -29,9 +29,9 @@ class AiBridge(models.Model): [ ("none", "None"), ("thread", "Thread"), - ("ai_thread_create", "AI Thread Create"), - ("ai_thread_write", "AI Thread Write"), - ("ai_thread_unlink", "AI Thread Unlink"), + ("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. " @@ -152,7 +152,7 @@ def _check_payload_type_usage_compatibility(self): if record.usage == "ai_thread_unlink" and record.payload_type != "none": raise models.ValidationError( _( - "When usage is 'AI Thread Unlink', " + "When usage is 'On Record Deleted', " "the Payload Type must be 'No payload'." ) ) diff --git a/ai_oca_bridge/models/ai_bridge_thread.py b/ai_oca_bridge/models/ai_bridge_thread.py index 888dbb7..8420df2 100644 --- a/ai_oca_bridge/models/ai_bridge_thread.py +++ b/ai_oca_bridge/models/ai_bridge_thread.py @@ -1,77 +1,16 @@ # Copyright 2025 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -import logging - -from odoo import api, models - -_logger = logging.getLogger(__name__) +from odoo import models class AiBridgeThread(models.AbstractModel): _name = "ai.bridge.thread" _description = "AI Bridge Mixin" - @api.model_create_multi - def create(self, vals_list): - records = super().create(vals_list) - model_id = self.sudo().env["ir.model"]._get_id(self._name) - for bridge in self.env["ai.bridge"].search( - [("model_id", "=", model_id), ("usage", "=", "ai_thread_create")] - ): - for record in records: - if bridge._enabled_for(record): - try: - bridge.execute_ai_bridge(record._name, record.id) - except Exception as e: - _logger.error( - "Error creating AI thread for creation on %s: %s", - record, - e, - ) - return records - - def write(self, values): - result = super().write(values) - model_id = self.sudo().env["ir.model"]._get_id(self._name) - for bridge in self.env["ai.bridge"].search( - [("model_id", "=", model_id), ("usage", "=", "ai_thread_write")] - ): - for record in self: - if bridge._enabled_for(record): - try: - bridge.execute_ai_bridge(record._name, record.id) - except Exception as e: - _logger.error( - "Error writing AI thread for writing on %s: %s", - record, - e, - ) - return result - - def unlink(self): - model_id = self.sudo().env["ir.model"]._get_id(self._name) - executions = self.env["ai.bridge.execution"] - for bridge in self.env["ai.bridge"].search( - [("model_id", "=", model_id), ("usage", "=", "ai_thread_unlink")] - ): - for record in self: - 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, - } - ) - result = super().unlink() - for execution in executions: - try: - execution._execute() - except Exception as e: - _logger.error( - "Error executing AI thread unlink for %s: %s", - self, - e, - ) - return result + # 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..29271bf --- /dev/null +++ b/ai_oca_bridge/models/base.py @@ -0,0 +1,82 @@ +# 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 + +_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") + 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): + if not records: + return + + model_id = self.env["ir.model"]._get_id(records._name) + bridges = self.env["ai.bridge"].search( + [("model_id", "=", model_id), ("usage", "=", usage)] + ) + for bridge in bridges: + 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( + [("model_id", "=", model_id), ("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/ir_model.py b/ai_oca_bridge/models/ir_model.py deleted file mode 100644 index 9783770..0000000 --- a/ai_oca_bridge/models/ir_model.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025 Dixmit -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class IrModel(models.Model): - _inherit = "ir.model" - - is_ai_bridge_thread = fields.Boolean() - ai_usage = fields.Char(store=False, search="_search_ai_usage") - - def _reflect_model_params(self, model): - vals = super()._reflect_model_params(model) - vals["is_ai_bridge_thread"] = ( - isinstance(model, self.pool["ai.bridge.thread"]) and not model._abstract - ) - return vals - - def _search_ai_usage(self, operator, value): - if operator not in ("="): - return [] - if value == "thread": - return [("is_mail_thread", "=", True), ("transient", "=", False)] - if value in ["ai_thread_create", "ai_thread_write", "ai_thread_unlink"]: - return [("is_ai_bridge_thread", "=", True), ("transient", "=", False)] - return [] diff --git a/ai_oca_bridge/readme/CONTRIBUTORS.md b/ai_oca_bridge/readme/CONTRIBUTORS.md index a12c579..12702fd 100644 --- a/ai_oca_bridge/readme/CONTRIBUTORS.md +++ b/ai_oca_bridge/readme/CONTRIBUTORS.md @@ -8,4 +8,5 @@ - [Binhex](https://www.binhex.cloud/) + - Ariel Barreiros - Adria Hortoneda diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index d8b00b4..8d2b1a0 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +AI OCA Bridge -
+
+

AI OCA Bridge

- - -Odoo Community Association - -
-

AI OCA Bridge

-

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

+

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

@@ -408,7 +403,7 @@

AI OCA Bridge

-

Use Cases / Context

+

Use Cases / Context

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

    @@ -429,17 +424,17 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

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

+

Payload Configuration

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

-

General

+

General

  • _odoo: Standard data to identify the Odoo Database
  • _model: Model of the related object
  • @@ -448,12 +443,12 @@

    General

-

Record Payload

+

Record Payload

Adds a new item called record with all the fields.

-

Asynchronous and synchronous calls

+

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 @@ -468,21 +463,21 @@

Asynchronous and synchronous call automatically on the synchronous call.

-

Result processing

+

Result processing

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

-

No processing

+

No processing

In this case, the result will do nothing

-

Post a Message

+

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

+

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:

@@ -495,12 +490,12 @@

Action

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -508,7 +503,7 @@

    Known issues / Roadmap

-

Bug Tracker

+

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 @@ -516,15 +511,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -554,6 +550,5 @@

Maintainers

-
diff --git a/ai_oca_bridge/tests/fake_models.py b/ai_oca_bridge/tests/fake_models.py index 784b666..94ccfc1 100644 --- a/ai_oca_bridge/tests/fake_models.py +++ b/ai_oca_bridge/tests/fake_models.py @@ -3,7 +3,6 @@ class BridgeTest(models.Model): _name = "bridge.test" - _inherit = "ai.bridge.thread" _description = "Test Model for AI Bridge" name = fields.Char() diff --git a/ai_oca_bridge/tests/test_mixin.py b/ai_oca_bridge/tests/test_mixin.py index f08ddb8..77f4040 100644 --- a/ai_oca_bridge/tests/test_mixin.py +++ b/ai_oca_bridge/tests/test_mixin.py @@ -19,6 +19,7 @@ def setUpClass(cls): from .fake_models import BridgeTest cls.loader.update_registry((BridgeTest,)) + cls.bridge = cls.env["ai.bridge"].create( { "name": "Test Bridge", @@ -151,24 +152,67 @@ def test_bridge_thread_unlink_constrains(self): with self.assertRaises(ValidationError): self.bridge.payload_type = "record" - def test_bridge_model_search(self): - models = self.env["ir.model"].search([("ai_usage", "=", "thread")]) - model = self.env["ir.model"]._get_id("bridge.test") - self.assertTrue(models) - self.assertIn(self.env.ref("base.model_res_partner"), models) - self.assertNotIn(model, models.ids) - models = self.env["ir.model"].search([("ai_usage", "=", "ai_thread_create")]) - self.assertTrue(models) - self.assertNotIn(self.env.ref("base.model_res_partner"), models) - self.assertIn(model, models.ids) - models = self.env["ir.model"].search([("ai_usage", "=", "none")]) - self.assertTrue(models) - self.assertIn(self.env.ref("base.model_res_partner"), models) - self.assertIn(model, models.ids) - 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( + [("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( + [("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( + [("ai_bridge_id", "=", self.bridge.id)] + ) + self.assertEqual(execution_count, 0) diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml index 5809f80..7fc2349 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -31,7 +31,6 @@ From 4a833af9959044ace07c7919390903ac8a81305e Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 18 Nov 2025 15:09:07 +0100 Subject: [PATCH 43/51] [IMP] ai_oca_bridge: Allow access to portal users --- ai_oca_bridge/models/ai_bridge.py | 16 ++++-- ai_oca_bridge/models/base.py | 9 +-- ai_oca_bridge/security/ir.model.access.csv | 2 +- ai_oca_bridge/tests/test_mixin.py | 67 +++++++++++++++++++++- 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index 5da2806..cf4ed6b 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -211,12 +211,16 @@ def execute_ai_bridge(self, res_model, res_id): } record = self.env[res_model].browse(res_id).exists() if record: - execution = self.env["ai.bridge.execution"].create( - { - "ai_bridge_id": self.id, - "model_id": self.sudo().env["ir.model"]._get_id(res_model), - "res_id": res_id, - } + 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: diff --git a/ai_oca_bridge/models/base.py b/ai_oca_bridge/models/base.py index 29271bf..c206737 100644 --- a/ai_oca_bridge/models/base.py +++ b/ai_oca_bridge/models/base.py @@ -42,10 +42,11 @@ def unlink(self): def _execute_ai_bridges_for_records(self, records, usage): if not records: return - - model_id = self.env["ir.model"]._get_id(records._name) - bridges = self.env["ai.bridge"].search( - [("model_id", "=", model_id), ("usage", "=", usage)] + model_id = self.sudo().env["ir.model"]._get_id(records._name) + bridges = ( + self.env["ai.bridge"] + .sudo() + .search([("model_id", "=", model_id), ("usage", "=", usage)]) ) for bridge in bridges: for record in records: diff --git a/ai_oca_bridge/security/ir.model.access.csv b/ai_oca_bridge/security/ir.model.access.csv index bb6f5ff..9e8a094 100644 --- a/ai_oca_bridge/security/ir.model.access.csv +++ b/ai_oca_bridge/security/ir.model.access.csv @@ -1,4 +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,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/tests/test_mixin.py b/ai_oca_bridge/tests/test_mixin.py index 77f4040..8c31c1e 100644 --- a/ai_oca_bridge/tests/test_mixin.py +++ b/ai_oca_bridge/tests/test_mixin.py @@ -6,7 +6,7 @@ from odoo_test_helper import FakeModelLoader from odoo.exceptions import ValidationError -from odoo.tests import Form, TransactionCase +from odoo.tests import Form, TransactionCase, new_test_user class TestBridge(TransactionCase): @@ -29,12 +29,77 @@ def setUpClass(cls): "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, + } + ) @classmethod def tearDownClass(cls): cls.loader.restore_registry() super().tearDownClass() + def test_bridge_creation_user(self): + self.bridge.write({"usage": "ai_thread_create"}) + self.assertEqual( + 0, + self.env["ai.bridge.execution"].search_count( + [("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( + [("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( + [("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( + [("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: From adfb8761e1c42c0c1883f5b7ab405c0e963965a8 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 9 Dec 2025 11:15:32 +0000 Subject: [PATCH 44/51] [UPD] Update ai_oca_bridge.pot --- ai_oca_bridge/i18n/ai_oca_bridge.pot | 52 +++++++++++----------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/ai_oca_bridge/i18n/ai_oca_bridge.pot b/ai_oca_bridge/i18n/ai_oca_bridge.pot index 3f25431..42352ad 100644 --- a/ai_oca_bridge/i18n/ai_oca_bridge.pot +++ b/ai_oca_bridge/i18n/ai_oca_bridge.pot @@ -90,21 +90,6 @@ msgstr "" msgid "AI Execution" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create -msgid "AI Thread Create" -msgstr "" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink -msgid "AI Thread Unlink" -msgstr "" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write -msgid "AI Thread Write" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" @@ -172,11 +157,6 @@ msgstr "" msgid "Ai Execution" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage -msgid "Ai Usage" -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 @@ -218,6 +198,11 @@ msgstr "" 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" @@ -394,11 +379,6 @@ msgstr "" msgid "Immediate" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread -msgid "Is Ai Bridge Thread" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -442,11 +422,6 @@ msgstr "" msgid "Model Required" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model,name:ai_oca_bridge.model_ir_model -msgid "Models" -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" @@ -509,6 +484,21 @@ msgstr "" 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 @@ -684,5 +674,5 @@ msgstr "" #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 msgid "" -"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." msgstr "" From 142b50281c001fb90353577340d87fa27849c6a4 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 9 Dec 2025 11:18:08 +0000 Subject: [PATCH 45/51] [BOT] post-merge updates --- ai_oca_bridge/README.rst | 8 +++- ai_oca_bridge/__manifest__.py | 2 +- ai_oca_bridge/static/description/index.html | 50 ++++++++++++--------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index d368c14..b5e1c42 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -1,3 +1,7 @@ +.. 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 ============= @@ -7,13 +11,13 @@ AI OCA Bridge !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:72deaa130a35eca64ce4bea745a612bb4d9a7271bdf7f9f9e016b0e7a47932a6 + !! 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/licence-AGPL--3-blue.png +.. |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 diff --git a/ai_oca_bridge/__manifest__.py b/ai_oca_bridge/__manifest__.py index f0d8ea2..b044e68 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ Makes a basic configuration to be used as bridge with external AI systems """, - "version": "18.0.1.0.0", + "version": "18.0.2.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/ai", diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 8d2b1a0..679a8c0 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -3,7 +3,7 @@ -AI OCA Bridge +README.rst -
-

AI OCA Bridge

+
+ + +Odoo Community Association + +
+

AI OCA Bridge

-

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

+

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

@@ -403,7 +408,7 @@

AI OCA Bridge

-

Use Cases / Context

+

Use Cases / Context

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

    @@ -424,17 +429,17 @@

    Use Cases / Context

    be used as Bridge with AI systems.

-

Configuration

+

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

+

Payload Configuration

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

-

General

+

General

  • _odoo: Standard data to identify the Odoo Database
  • _model: Model of the related object
  • @@ -443,12 +448,12 @@

    General

-

Record Payload

+

Record Payload

Adds a new item called record with all the fields.

-

Asynchronous and synchronous calls

+

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 @@ -463,21 +468,21 @@

Asynchronous and synchronous call automatically on the synchronous call.

-

Result processing

+

Result processing

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

-

No processing

+

No processing

In this case, the result will do nothing

-

Post a Message

+

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

+

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:

@@ -490,12 +495,12 @@

Action

-

Usage

+

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

+

Known issues / Roadmap

  • Define examples to use and import
  • Allow child fields. Right now, only first level fields are accepted.
  • @@ -503,7 +508,7 @@

    Known issues / Roadmap

-

Bug Tracker

+

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 @@ -511,15 +516,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -550,5 +555,6 @@

Maintainers

+
From f95f33b866ddad57e0c9dcd83b95d431c37300f0 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 9 Dec 2025 11:18:19 +0000 Subject: [PATCH 46/51] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: ai-18.0/ai-18.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_oca_bridge/ --- ai_oca_bridge/i18n/es.po | 78 +++++++++++++++++++++---------------- ai_oca_bridge/i18n/es_VE.po | 78 +++++++++++++++++++++---------------- ai_oca_bridge/i18n/it.po | 78 +++++++++++++++++++++---------------- ai_oca_bridge/i18n/pt_BR.po | 52 ++++++++++--------------- 4 files changed, 156 insertions(+), 130 deletions(-) diff --git a/ai_oca_bridge/i18n/es.po b/ai_oca_bridge/i18n/es.po index aed3284..d15a5f2 100644 --- a/ai_oca_bridge/i18n/es.po +++ b/ai_oca_bridge/i18n/es.po @@ -107,21 +107,6 @@ msgstr "Mezcla de puente de IA" msgid "AI Execution" msgstr "Ejecución de IA" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create -msgid "AI Thread Create" -msgstr "Crear hilo de IA" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink -msgid "AI Thread Unlink" -msgstr "Eliminar hilo de IA" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write -msgid "AI Thread Write" -msgstr "Escribir hilo de IA" - #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" @@ -189,11 +174,6 @@ msgstr "Información de puente de IA" msgid "Ai Execution" msgstr "Ejecución de IA" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage -msgid "Ai Usage" -msgstr "Uso 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 @@ -235,6 +215,11 @@ msgstr "Nombre de usuario de autenticación" 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" @@ -413,11 +398,6 @@ msgstr "Si se activa, algunos mensajes tienen un error de envío." msgid "Immediate" msgstr "Inmediato" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread -msgid "Is Ai Bridge Thread" -msgstr "Es un hilo del puente IA" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -461,11 +441,6 @@ msgstr "Nombre del modelo" msgid "Model Required" msgstr "Modelo requerido" -#. module: ai_oca_bridge -#: model:ir.model,name:ai_oca_bridge.model_ir_model -msgid "Models" -msgstr "Modelos" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline msgid "My Activity Deadline" @@ -528,6 +503,21 @@ msgstr "Número de mensajes que requieren una acción" 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 @@ -710,10 +700,32 @@ msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 msgid "" -"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +"When usage is 'On Record Deleted', 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 "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" diff --git a/ai_oca_bridge/i18n/es_VE.po b/ai_oca_bridge/i18n/es_VE.po index e1e0794..2f0a7a7 100644 --- a/ai_oca_bridge/i18n/es_VE.po +++ b/ai_oca_bridge/i18n/es_VE.po @@ -107,21 +107,6 @@ msgstr "Mezcla de puente de IA" msgid "AI Execution" msgstr "Ejecución de IA" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create -msgid "AI Thread Create" -msgstr "Crear hilo de IA" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink -msgid "AI Thread Unlink" -msgstr "Eliminar hilo de IA" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write -msgid "AI Thread Write" -msgstr "Escribir hilo de IA" - #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" @@ -189,11 +174,6 @@ msgstr "Información de puente de IA" msgid "Ai Execution" msgstr "Ejecución de IA" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage -msgid "Ai Usage" -msgstr "Uso 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 @@ -235,6 +215,11 @@ msgstr "Nombre de usuario de autenticación" 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" @@ -413,11 +398,6 @@ msgstr "Si se activa, algunos mensajes tienen un error de envío." msgid "Immediate" msgstr "Inmediato" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread -msgid "Is Ai Bridge Thread" -msgstr "Es un hilo del puente IA" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -461,11 +441,6 @@ msgstr "Nombre del modelo" msgid "Model Required" msgstr "Modelo requerido" -#. module: ai_oca_bridge -#: model:ir.model,name:ai_oca_bridge.model_ir_model -msgid "Models" -msgstr "Modelos" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline msgid "My Activity Deadline" @@ -528,6 +503,21 @@ msgstr "Número de mensajes que requieren una acción" 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 @@ -710,10 +700,32 @@ msgstr "Grupos de usuarios autorizados a utilizar este puente de IA." #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 msgid "" -"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +"When usage is 'On Record Deleted', 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 "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" diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index 76c5de1..b539065 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -105,21 +105,6 @@ msgstr "Mixin bridge IA" msgid "AI Execution" msgstr "Esecuzione IA" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create -msgid "AI Thread Create" -msgstr "Creazione thread IA" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink -msgid "AI Thread Unlink" -msgstr "Rilascio thread IA" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write -msgid "AI Thread Write" -msgstr "Scrittura thread IA" - #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" @@ -187,11 +172,6 @@ msgstr "Informazioni collegamento IA" msgid "Ai Execution" msgstr "Esecuzione IA" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage -msgid "Ai Usage" -msgstr "Utilizzo 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 @@ -233,6 +213,11 @@ msgstr "Nome utente autenticazione" msgid "Authentication Type" msgstr "Tipo autenticazione" +#. 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" @@ -411,11 +396,6 @@ msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna." msgid "Immediate" msgstr "Immediato" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread -msgid "Is Ai Bridge Thread" -msgstr "È un thread IA" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -459,11 +439,6 @@ msgstr "Nome modello" msgid "Model Required" msgstr "Modello richiesto" -#. module: ai_oca_bridge -#: model:ir.model,name:ai_oca_bridge.model_ir_model -msgid "Models" -msgstr "Modelli" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__my_activity_date_deadline msgid "My Activity Deadline" @@ -526,6 +501,21 @@ msgstr "Numero di messaggi che richiedono un'azione" 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 "" + +#. 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 @@ -710,10 +700,32 @@ msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 msgid "" -"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." msgstr "" -"Quando l'utilizzo è 'Rilascio thread IA', il tipo di carico deve essere " -"'Nessun carico'." + +#~ 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" diff --git a/ai_oca_bridge/i18n/pt_BR.po b/ai_oca_bridge/i18n/pt_BR.po index b14cc9a..288b399 100644 --- a/ai_oca_bridge/i18n/pt_BR.po +++ b/ai_oca_bridge/i18n/pt_BR.po @@ -93,21 +93,6 @@ msgstr "" msgid "AI Execution" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_create -msgid "AI Thread Create" -msgstr "" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_unlink -msgid "AI Thread Unlink" -msgstr "" - -#. module: ai_oca_bridge -#: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__usage__ai_thread_write -msgid "AI Thread Write" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__result_type__action msgid "Action" @@ -175,11 +160,6 @@ msgstr "" msgid "Ai Execution" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__ai_usage -msgid "Ai Usage" -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 @@ -221,6 +201,11 @@ msgstr "" 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" @@ -397,11 +382,6 @@ msgstr "" msgid "Immediate" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model.fields,field_description:ai_oca_bridge.field_ir_model__is_ai_bridge_thread -msgid "Is Ai Bridge Thread" -msgstr "" - #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge__message_is_follower msgid "Is Follower" @@ -445,11 +425,6 @@ msgstr "" msgid "Model Required" msgstr "" -#. module: ai_oca_bridge -#: model:ir.model,name:ai_oca_bridge.model_ir_model -msgid "Models" -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" @@ -512,6 +487,21 @@ msgstr "" 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 @@ -687,5 +677,5 @@ msgstr "" #. odoo-python #: code:addons/ai_oca_bridge/models/ai_bridge.py:0 msgid "" -"When usage is 'AI Thread Unlink', the Payload Type must be 'No payload'." +"When usage is 'On Record Deleted', the Payload Type must be 'No payload'." msgstr "" From 48f415ed43cbe352ab4414fb489bb2e06e3139d8 Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 11 Dec 2025 07:59:09 +0000 Subject: [PATCH 47/51] Translated using Weblate (Italian) Currently translated at 100.0% (121 of 121 strings) Translation: ai-18.0/ai-18.0-ai_oca_bridge Translate-URL: https://translation.odoo-community.org/projects/ai-18-0/ai-18-0-ai_oca_bridge/it/ --- ai_oca_bridge/i18n/it.po | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ai_oca_bridge/i18n/it.po b/ai_oca_bridge/i18n/it.po index b539065..0669676 100644 --- a/ai_oca_bridge/i18n/it.po +++ b/ai_oca_bridge/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-09-26 16:20+0000\n" +"PO-Revision-Date: 2025-12-11 10:42+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -216,7 +216,7 @@ msgstr "Tipo autenticazione" #. module: ai_oca_bridge #: model:ir.model,name:ai_oca_bridge.model_base msgid "Base" -msgstr "" +msgstr "Base" #. module: ai_oca_bridge #: model:ir.model.fields.selection,name:ai_oca_bridge.selection__ai_bridge__auth_type__basic @@ -504,17 +504,17 @@ 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 "" +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 "" +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 "" +msgstr "All'aggiornamento del record" #. module: ai_oca_bridge #: model:ir.model.fields,field_description:ai_oca_bridge.field_ai_bridge_execution__payload @@ -702,6 +702,8 @@ msgstr "Gruppi utente autorizzati ad usare questo collegamento IA." 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" From aaee0bfd4176c02c4baf7191ea16ecc426bd26e1 Mon Sep 17 00:00:00 2001 From: thienvh Date: Tue, 16 Dec 2025 09:36:33 +0700 Subject: [PATCH 48/51] [IMP] ai_oca_bridge: support server_action result_type --- ai_oca_bridge/models/ai_bridge.py | 13 ++++++++ ai_oca_bridge/models/ai_bridge_execution.py | 24 ++++++++++++++ ai_oca_bridge/tests/test_bridge.py | 36 +++++++++++++++++++++ ai_oca_bridge/views/ai_bridge.xml | 5 +++ 4 files changed, 78 insertions(+) diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index cf4ed6b..145435a 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -58,6 +58,7 @@ class AiBridge(models.Model): ("none", "No processing"), ("message", "Post a Message"), ("action", "Action"), + ("server_action", "Run a Server Action"), ], required=True, default="none", @@ -120,6 +121,13 @@ class AiBridge(models.Model): 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, + ) ####################################### # Payload type 'record' specific fields @@ -185,6 +193,11 @@ def _compute_field_ids(self): for record in self: record.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: diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py index 6453b01..f7af8b7 100644 --- a/ai_oca_bridge/models/ai_bridge_execution.py +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -215,6 +215,30 @@ def _process_response_action(self, response): 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) diff --git a/ai_oca_bridge/tests/test_bridge.py b/ai_oca_bridge/tests/test_bridge.py index 0e2797e..2080271 100644 --- a/ai_oca_bridge/tests/test_bridge.py +++ b/ai_oca_bridge/tests/test_bridge.py @@ -328,3 +328,39 @@ def test_bridge_execute_computed_fields(self): 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/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml index 7fc2349..83b67a7 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -34,6 +34,11 @@ required="model_required" /> + From fcc9c249077d27dea2d2b9838c8f4f8a49059356 Mon Sep 17 00:00:00 2001 From: thienvh Date: Tue, 16 Dec 2025 09:38:26 +0700 Subject: [PATCH 49/51] [IMP] ai_oca_bridge: support field-based trigger for ai_thread_update --- ai_oca_bridge/models/ai_bridge.py | 19 +++++++++++++++++++ ai_oca_bridge/models/base.py | 12 ++++++++++-- ai_oca_bridge/tests/fake_models.py | 1 + ai_oca_bridge/tests/test_mixin.py | 26 ++++++++++++++++++++++++++ ai_oca_bridge/views/ai_bridge.xml | 6 ++++++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index 145435a..55b8e9d 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -128,6 +128,20 @@ class AiBridge(models.Model): 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 @@ -193,6 +207,11 @@ 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: diff --git a/ai_oca_bridge/models/base.py b/ai_oca_bridge/models/base.py index c206737..456d3cc 100644 --- a/ai_oca_bridge/models/base.py +++ b/ai_oca_bridge/models/base.py @@ -20,7 +20,7 @@ def create(self, vals_list): def write(self, values): result = super().write(values) - self._execute_ai_bridges_for_records(self, "ai_thread_write") + self._execute_ai_bridges_for_records(self, "ai_thread_write", values=values) return result def unlink(self): @@ -39,7 +39,7 @@ def unlink(self): return result - def _execute_ai_bridges_for_records(self, records, usage): + 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) @@ -49,6 +49,14 @@ def _execute_ai_bridges_for_records(self, records, usage): .search([("model_id", "=", model_id), ("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: diff --git a/ai_oca_bridge/tests/fake_models.py b/ai_oca_bridge/tests/fake_models.py index 94ccfc1..6c83df3 100644 --- a/ai_oca_bridge/tests/fake_models.py +++ b/ai_oca_bridge/tests/fake_models.py @@ -6,3 +6,4 @@ class BridgeTest(models.Model): _description = "Test Model for AI Bridge" name = fields.Char() + description = fields.Char() diff --git a/ai_oca_bridge/tests/test_mixin.py b/ai_oca_bridge/tests/test_mixin.py index 8c31c1e..fe97847 100644 --- a/ai_oca_bridge/tests/test_mixin.py +++ b/ai_oca_bridge/tests/test_mixin.py @@ -281,3 +281,29 @@ def test_execute_ai_bridges_empty_records(self): [("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 index 83b67a7..8b87352 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -39,6 +39,12 @@ domain="[('model_id', '=', model_id)]" attrs="{'invisible': [('result_type', '!=', 'server_action')]}" /> + From b4f3aa213bc3d07c9c383de41b70a402668d5799 Mon Sep 17 00:00:00 2001 From: thienvh Date: Wed, 17 Dec 2025 13:07:38 +0700 Subject: [PATCH 50/51] [IMP] ai_oca_bridge: add examples data for AI bridge configuration with server_action --- .../examples/filter_spam_tickets.xml | 42 +++++ .../examples/filter_spam_tickets_n8n.json | 147 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 ai_oca_bridge/examples/filter_spam_tickets.xml create mode 100644 ai_oca_bridge/examples/filter_spam_tickets_n8n.json 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": [] +} From a622199e8bc19069870bfb2789d316ee858914d3 Mon Sep 17 00:00:00 2001 From: Hai Lang Date: Wed, 7 Jan 2026 14:34:55 +0700 Subject: [PATCH 51/51] [MIG] ai_oca_bridge: Migration to 19.0 - Domain handling migrated to `odoo.fields.Domain` (AND helpers, safe empty defaults) and user group access checks updated to new `group_ids` API. - Async response URLs now built with `odoo.tools.urls.urljoin` and chatter tracking disabled in tests via a new mixin to avoid noisy metadata. - JS/web tests now create bridge records dynamically (no hardcoded mock data) and fake test model registration updated for Odoo 19 registry APIs; inactive bridge uses `action_archive`. - Message/action tests now share computed domains for clarity and resilience under the new Domain API. * Change _ to self.env._, more info at https://github.com/odoo/odoo/pull/174844 * Work around charget AttributeError * Import ValidationError from odoo.exceptions instead of models * Rename webRecord to record * Use new way of popover * Test for response content --- ai_oca_bridge/README.rst | 10 +-- ai_oca_bridge/__manifest__.py | 8 +- ai_oca_bridge/controllers/ai.py | 20 ++--- ai_oca_bridge/models/ai_bridge.py | 35 +++++--- ai_oca_bridge/models/ai_bridge_execution.py | 8 +- ai_oca_bridge/models/base.py | 14 +++- ai_oca_bridge/models/mail_thread.py | 11 ++- ai_oca_bridge/static/description/index.html | 6 +- .../src/components/chatter/chatter.esm.js | 8 +- .../chatter_topbar/chatter_topbar.xml | 2 +- .../chatter_topbar_ai/chatter_topbar_ai.xml | 2 +- .../chatter_topbar_ai_item.esm.js | 27 +++---- .../mock_server/mock_models/ai_bridge.esm.js | 5 +- .../tests/web/test_ai_oca_bridge.test.js | 43 ++++++++-- ai_oca_bridge/tests/common.py | 8 ++ ai_oca_bridge/tests/test_bridge.py | 79 ++++++++++--------- ai_oca_bridge/tests/test_connection.py | 14 ++-- ai_oca_bridge/tests/test_mixin.py | 59 +++++++------- ai_oca_bridge/views/ai_bridge.xml | 5 +- 19 files changed, 214 insertions(+), 150 deletions(-) create mode 100644 ai_oca_bridge/tests/common.py diff --git a/ai_oca_bridge/README.rst b/ai_oca_bridge/README.rst index b5e1c42..cfa1415 100644 --- a/ai_oca_bridge/README.rst +++ b/ai_oca_bridge/README.rst @@ -21,13 +21,13 @@ AI OCA Bridge :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/18.0/ai_oca_bridge + :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-18-0/ai-18-0-ai_oca_bridge + :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=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -159,7 +159,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -200,6 +200,6 @@ 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. +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/__manifest__.py b/ai_oca_bridge/__manifest__.py index b044e68..8f8a92d 100644 --- a/ai_oca_bridge/__manifest__.py +++ b/ai_oca_bridge/__manifest__.py @@ -6,13 +6,17 @@ "summary": """ Makes a basic configuration to be used as bridge with external AI systems """, - "version": "18.0.2.0.0", + "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": ["mail"], + "depends": [ + "base", + "mail", + "web", + ], "data": [ "data/ir_module_category.xml", "security/ir.model.access.csv", diff --git a/ai_oca_bridge/controllers/ai.py b/ai_oca_bridge/controllers/ai.py index 06668ec..76929a0 100644 --- a/ai_oca_bridge/controllers/ai.py +++ b/ai_oca_bridge/controllers/ai.py @@ -6,7 +6,6 @@ from odoo import fields, http from odoo.http import request from odoo.tools import consteq -from odoo.tools.translate import _ class AIController(http.Controller): @@ -22,24 +21,27 @@ class AIController(http.Controller): 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(_("Execution not found."), status=404) + return request.make_response(self.env._("Execution not found."), status=404) if not consteq(execution._generate_token(), token): return request.make_response( - _("Token is not allowed for this execution."), status=404 + 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(_("Execution is expired."), status=404) + 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( - request.httprequest.charset - ) - ) + json.loads(request.httprequest.get_data().decode(charset)) ) ), headers=[ diff --git a/ai_oca_bridge/models/ai_bridge.py b/ai_oca_bridge/models/ai_bridge.py index 55b8e9d..954bd9b 100644 --- a/ai_oca_bridge/models/ai_bridge.py +++ b/ai_oca_bridge/models/ai_bridge.py @@ -6,7 +6,9 @@ import logging from datetime import date, datetime -from odoo import _, api, fields, models +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__) @@ -172,8 +174,8 @@ def _compute_payload_type(self): def _check_payload_type_usage_compatibility(self): for record in self: if record.usage == "ai_thread_unlink" and record.payload_type != "none": - raise models.ValidationError( - _( + raise ValidationError( + self.env._( "When usage is 'On Record Deleted', " "the Payload Type must be 'No payload'." ) @@ -235,11 +237,11 @@ def _get_info(self): 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.groups_id & self.group_ids + self.group_ids and not self.env.user.group_ids & self.group_ids ): return { - "body": _("%s is not active.", self.name), - "args": {"type": "warning", "title": _("AI Bridge Inactive")}, + "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: @@ -260,24 +262,31 @@ def execute_ai_bridge(self, res_model, res_id): if execution.state == "done": return { "notification": { - "body": _("%s executed successfully.", self.name), - "args": {"type": "success", "title": _("AI Bridge Executed")}, + "body": self.env._("%s executed successfully.", self.name), + "args": { + "type": "success", + "title": self.env._("AI Bridge Executed"), + }, } } return { "notification": { - "body": _("%s failed.", self.name), - "args": {"type": "danger", "title": _("AI Bridge Failed")}, + "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 = safe_eval(self.domain) - if self.group_ids and not self.env.user.groups_id & self.group_ids: + 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: + if domain_list: return bool(record.filtered_domain(domain)) return True diff --git a/ai_oca_bridge/models/ai_bridge_execution.py b/ai_oca_bridge/models/ai_bridge_execution.py index f7af8b7..d8074a5 100644 --- a/ai_oca_bridge/models/ai_bridge_execution.py +++ b/ai_oca_bridge/models/ai_bridge_execution.py @@ -7,9 +7,9 @@ from io import StringIO import requests -from werkzeug import urls -from odoo import _, api, fields, models, tools +from odoo import api, fields, models, tools +from odoo.tools import urls class AiBridgeExecution(models.Model): @@ -92,7 +92,7 @@ def _add_extra_payload_fields(self, payload): seconds=self.ai_bridge_id.async_timeout ) token = self._generate_token() - payload["_response_url"] = urls.url_join( + payload["_response_url"] = urls.urljoin( self.get_base_url(), f"/ai/response/{self.id}/{token}" ) IrParamSudo = self.env["ir.config_parameter"].sudo() @@ -163,7 +163,7 @@ def _get_auth(self): self.ai_bridge_id.sudo().auth_password, ) else: - raise ValueError(_("Unsupported authentication type.")) + raise ValueError(self.env._("Unsupported authentication type.")) def _get_headers(self): """Return headers for the request.""" diff --git a/ai_oca_bridge/models/base.py b/ai_oca_bridge/models/base.py index 456d3cc..d541b0d 100644 --- a/ai_oca_bridge/models/base.py +++ b/ai_oca_bridge/models/base.py @@ -5,6 +5,7 @@ import logging from odoo import api, models +from odoo.fields import Domain _logger = logging.getLogger(__name__) @@ -46,7 +47,11 @@ def _execute_ai_bridges_for_records(self, records, usage, values=None): bridges = ( self.env["ai.bridge"] .sudo() - .search([("model_id", "=", model_id), ("usage", "=", usage)]) + .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, @@ -73,7 +78,12 @@ def _prepare_execution_ai_bridges_unlink(self, records): model_id = self.env["ir.model"]._get_id(records._name) bridges = self.env["ai.bridge"].search( - [("model_id", "=", model_id), ("usage", "=", "ai_thread_unlink")] + Domain.AND( + [ + Domain("model_id", "=", model_id), + Domain("usage", "=", "ai_thread_unlink"), + ] + ) ) executions = self.env["ai.bridge.execution"] diff --git a/ai_oca_bridge/models/mail_thread.py b/ai_oca_bridge/models/mail_thread.py index 1bcc815..97a0716 100644 --- a/ai_oca_bridge/models/mail_thread.py +++ b/ai_oca_bridge/models/mail_thread.py @@ -4,6 +4,7 @@ from lxml import etree from odoo import api, fields, models +from odoo.fields import Domain from odoo.tools.misc import frozendict @@ -21,10 +22,16 @@ def _compute_ai_bridge_info(self): def _get_ai_bridge_info(self): self.ensure_one() - model_id = self.env["ir.model"].sudo().search([("model", "=", self._name)]).id + model_id = ( + self.env["ir.model"].sudo().search(Domain("model", "=", self._name)).id + ) return ( self.env["ai.bridge"] - .search([("model_id", "=", model_id), ("usage", "=", "thread")]) + .search( + Domain.AND( + [Domain("model_id", "=", model_id), Domain("usage", "=", "thread")] + ) + ) .filtered(lambda r: r._enabled_for(self)) ) diff --git a/ai_oca_bridge/static/description/index.html b/ai_oca_bridge/static/description/index.html index 679a8c0..fe694d1 100644 --- a/ai_oca_bridge/static/description/index.html +++ b/ai_oca_bridge/static/description/index.html @@ -374,7 +374,7 @@

AI OCA Bridge

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:c702d03816331f686ec7f037a5bbef08e513f54645bfdb51d11b15bcd5db77f2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

+

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

@@ -512,7 +512,7 @@

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.

+feedback.

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

@@ -550,7 +550,7 @@

Maintainers

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.

+

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/components/chatter/chatter.esm.js b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js index e3aa2c2..b1d6f65 100644 --- a/ai_oca_bridge/static/src/components/chatter/chatter.esm.js +++ b/ai_oca_bridge/static/src/components/chatter/chatter.esm.js @@ -5,9 +5,9 @@ patch(Chatter.prototype, { async onClickAiBridge(aiBridge) { let saved = true; - if (this.props.webRecord && this.props.webRecord.save) { + if (this.props.record && this.props.record.save) { try { - await this.props.webRecord.save(); + await this.props.record.save(); } catch (error) { saved = false; console.error("Error saving record:", error); @@ -18,8 +18,8 @@ patch(Chatter.prototype, { return; } - const model = this.props.webRecord.resModel; - const id = this.props.webRecord.resId; + const model = this.props.record.resModel; + const id = this.props.record.resId; const result = await this.env.services.orm.call( "ai.bridge", 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 index 333bf88..ef2e1cd 100644 --- a/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml +++ b/ai_oca_bridge/static/src/components/chatter_topbar/chatter_topbar.xml @@ -8,7 +8,7 @@ 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 index 7394c81..02f69eb 100644 --- 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 @@ -11,7 +11,7 @@ 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 index b299780..c1170ff 100644 --- 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 @@ -3,6 +3,10 @@ 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 { @@ -11,25 +15,15 @@ export class ChatterAIItem extends Component { setup() { super.setup(); - this.popover = usePopover(); - this.tooltipPopover = null; + this.popover = usePopover(ChatterAIItemPopover); } get tooltipInfo() { - return { - help: markup(this.props.bridge.description || ""), - }; + return markup(this.props.bridge.description || ""); } onMouseEnter(ev) { this.closeTooltip(); - this.tooltipPopover = this.popover.add( - ev.currentTarget, - ChatterAIItemPopover, - this.tooltipInfo, - { - closeOnClickAway: true, - position: "top", - } - ); + const help = this.tooltipInfo; + this.popover.open(ev.currentTarget, {help}); } onMouseLeave() { @@ -37,9 +31,6 @@ export class ChatterAIItem extends Component { } closeTooltip() { - if (this.tooltipPopover) { - this.tooltipPopover(); - this.tooltipPopover = null; - } + this.popover.close(); } } 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 index fb5e3e3..a926326 100644 --- 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 @@ -15,10 +15,7 @@ export class AiBridge extends models.ServerModel { default: "none", }); name = fields.Char(); - _records = [ - {id: 1, name: "Test AI Bridge", result_type: "none"}, - {id: 2, name: "Test AI Bridge Action", result_type: "action"}, - ]; + _records = []; execute_ai_bridge(ids) { const record = this.browse(ids); if (record && record[0].result_type === "action") { 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 index 0c1a773..6450eaa 100644 --- 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 @@ -3,13 +3,18 @@ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ import {click, queryAll} from "@odoo/hoot-dom"; -import {defineModels, fields, models, mountView} from "@web/../tests/web_test_helpers"; +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 models.Model { - _name = "res.partner"; +class ResPartner extends webModels.ResPartner { + _inherit = ["mail.thread"]; ai_bridge_info = fields.Generic({default: []}); } @@ -18,11 +23,23 @@ 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: 1, name: "AI 1", description: "test1 description"}, - {id: 2, name: "AI 2"}, + { + id: aiBridgeNotificationId, + name: "AI 1", + description: "test1 description", + }, + {id: aiBridgeActionId, name: "AI 2"}, ], }); await mountView({ @@ -56,11 +73,23 @@ test("AI Notification", async () => { 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: 1, name: "AI 1", description: "test1 description"}, - {id: 2, name: "AI 2"}, + { + id: aiBridgeNotificationId, + name: "AI 1", + description: "test1 description", + }, + {id: aiBridgeActionId, name: "AI 2"}, ], }); await mountView({ 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/test_bridge.py b/ai_oca_bridge/tests/test_bridge.py index 2080271..53787e4 100644 --- a/ai_oca_bridge/tests/test_bridge.py +++ b/ai_oca_bridge/tests/test_bridge.py @@ -4,10 +4,13 @@ import json from unittest import mock +from odoo.fields import Domain from odoo.tests.common import TransactionCase +from .common import TrackingDisabledMixin -class TestBridge(TransactionCase): + +class TestBridge(TrackingDisabledMixin, TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -50,7 +53,7 @@ def test_bridge_none_auth(self): ) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) with mock.patch("requests.post") as mock_post: @@ -58,11 +61,11 @@ def test_bridge_none_auth(self): mock_post.assert_called_once() self.assertTrue( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) execution = self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertEqual(execution.res_id, self.partner.id) self.assertNotIn("name", execution.payload) @@ -85,7 +88,7 @@ def test_bridge_none_auth_fields_record(self): ) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) with mock.patch("requests.post") as mock_post: @@ -93,11 +96,11 @@ def test_bridge_none_auth_fields_record(self): mock_post.assert_called_once() self.assertTrue( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) execution = self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertEqual(execution.res_id, self.partner.id) self.assertIn("name", execution.payload["record"]) @@ -118,7 +121,7 @@ def test_bridge_basic_auth(self): ) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) with mock.patch("requests.post") as mock_post: @@ -126,7 +129,7 @@ def test_bridge_basic_auth(self): mock_post.assert_called_once() self.assertTrue( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) @@ -143,7 +146,7 @@ def test_bridge_token_auth(self): ) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) with mock.patch("requests.post") as mock_post: @@ -151,7 +154,7 @@ def test_bridge_token_auth(self): mock_post.assert_called_once() self.assertTrue( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) @@ -162,26 +165,26 @@ def test_bridge_error(self): ) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + 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( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertTrue(execution) self.assertTrue(execution.error) def test_bridge_unactive(self): - self.bridge.toggle_active() + self.bridge.action_archive() self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + 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( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertFalse(execution) @@ -189,12 +192,12 @@ def test_bridge_check_group(self): self.bridge.group_ids = [(4, self.group.id)] self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + 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( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertFalse(execution) @@ -219,7 +222,7 @@ def test_bridge_group_filtering(self): self.assertNotIn( self.bridge.id, [bridge["id"] for bridge in self.partner.ai_bridge_info] ) - self.env.user.groups_id |= self.group + 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] @@ -238,12 +241,16 @@ def test_bridge_result_message(self): self.bridge.write({"result_type": "message"}) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) - message_count = self.env["mail.message"].search_count( - [("model", "=", self.partner._name), ("res_id", "=", self.partner.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"} @@ -251,9 +258,7 @@ def test_bridge_result_message(self): self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) mock_post.assert_called_once() self.assertEqual( - self.env["mail.message"].search_count( - [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] - ), + self.env["mail.message"].search_count(message_domain), message_count + 1, ) @@ -261,12 +266,16 @@ def test_bridge_result_message_async(self): self.bridge.write({"result_type": "message", "result_kind": "async"}) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) - message_count = self.env["mail.message"].search_count( - [("model", "=", self.partner._name), ("res_id", "=", self.partner.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"} @@ -274,20 +283,16 @@ def test_bridge_result_message_async(self): self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) mock_post.assert_called_once() self.assertEqual( - self.env["mail.message"].search_count( - [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] - ), + self.env["mail.message"].search_count(message_domain), message_count, ) execution = self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + 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( - [("model", "=", self.partner._name), ("res_id", "=", self.partner.id)] - ), + self.env["mail.message"].search_count(message_domain), message_count + 1, ) self.assertFalse(execution.expiration_date) @@ -296,7 +301,7 @@ def test_bridge_result_action_immediate(self): self.bridge.write({"result_type": "action", "result_kind": "immediate"}) self.assertFalse( self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) ) with mock.patch("requests.post") as mock_post: @@ -323,7 +328,7 @@ def test_bridge_execute_computed_fields(self): self.bridge.execute_ai_bridge(self.partner._name, self.partner.id) mock_post.assert_called_once() execution = self.env["ai.bridge.execution"].search( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertEqual( execution.payload["_id"], json.loads(execution.payload_txt)["_id"] diff --git a/ai_oca_bridge/tests/test_connection.py b/ai_oca_bridge/tests/test_connection.py index 92ba5f9..a3c2f9f 100644 --- a/ai_oca_bridge/tests/test_connection.py +++ b/ai_oca_bridge/tests/test_connection.py @@ -3,13 +3,14 @@ from unittest import mock -from werkzeug import urls - from odoo.tests.common import HttpCase, tagged +from odoo.tools import urls + +from .common import TrackingDisabledMixin @tagged("post_install", "-at_install") -class TestAsyncConnection(HttpCase): +class TestAsyncConnection(TrackingDisabledMixin, HttpCase): def setUp(self): super().setUp() self.bridge = self.env["ai.bridge"].create( @@ -45,6 +46,7 @@ def test_wrong_key(self): 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( @@ -53,6 +55,7 @@ def test_wrong_id(self): 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( @@ -63,7 +66,8 @@ def test_connection(self): ] ) ) - self.opener.post(self.url, json={"body": "Test response"}) + result = self.opener.post(self.url, json={"body": "Test response"}) + self.assertIn('{"id":', result.text) self.assertEqual( self.env["mail.message"].search_count( [ @@ -106,7 +110,7 @@ def test_connection_expired(self): execution.expiration_date = "2020-01-01 00:00:00" token = execution._generate_token() result = self.opener.post( - urls.url_join( + urls.urljoin( execution.get_base_url(), f"/ai/response/{execution.id}/{token}" ), json={"body": "Test response"}, diff --git a/ai_oca_bridge/tests/test_mixin.py b/ai_oca_bridge/tests/test_mixin.py index fe97847..a4b0e0e 100644 --- a/ai_oca_bridge/tests/test_mixin.py +++ b/ai_oca_bridge/tests/test_mixin.py @@ -3,22 +3,24 @@ from unittest import mock -from odoo_test_helper import FakeModelLoader - 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(TransactionCase): +class TestBridge(TrackingDisabledMixin, TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() from .fake_models import BridgeTest - cls.loader.update_registry((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( { @@ -50,17 +52,12 @@ def setUpClass(cls): } ) - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() - def test_bridge_creation_user(self): self.bridge.write({"usage": "ai_thread_create"}) self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) with self.with_user("bridge_user"), mock.patch("requests.post") as mock_post: @@ -72,7 +69,7 @@ def test_bridge_creation_user(self): self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) @@ -81,7 +78,7 @@ def test_bridge_creation_portal_user(self): self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) with ( @@ -96,7 +93,7 @@ def test_bridge_creation_portal_user(self): self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) @@ -108,7 +105,7 @@ def test_bridge_thread_creation(self): self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) # Create a test record @@ -116,7 +113,7 @@ def test_bridge_thread_creation(self): self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) mock_post.assert_called_once() @@ -124,14 +121,14 @@ def test_bridge_thread_creation(self): self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) record.unlink() self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) mock_post.assert_called_once() @@ -144,7 +141,7 @@ def test_bridge_thread_write(self): self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) # Create a test record @@ -152,21 +149,21 @@ def test_bridge_thread_write(self): self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) record.write({"name": "Updated Record"}) self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) record.unlink() self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) mock_post.assert_called_once() @@ -182,7 +179,7 @@ def test_bridge_thread_unlink(self): self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) # Create a test record @@ -190,21 +187,21 @@ def test_bridge_thread_unlink(self): self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) record.write({"name": "Updated Record"}) self.assertEqual( 0, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) record.unlink() self.assertEqual( 1, self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ), ) mock_post.assert_called_once() @@ -257,13 +254,13 @@ 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( - [("ai_bridge_id", "=", self.bridge.id)] + 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( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertEqual(initial_count, final_count) @@ -278,7 +275,7 @@ def test_execute_ai_bridges_empty_records(self): ) mock_post.assert_not_called() execution_count = self.env["ai.bridge.execution"].search_count( - [("ai_bridge_id", "=", self.bridge.id)] + Domain("ai_bridge_id", "=", self.bridge.id) ) self.assertEqual(execution_count, 0) diff --git a/ai_oca_bridge/views/ai_bridge.xml b/ai_oca_bridge/views/ai_bridge.xml index 8b87352..7e00e38 100644 --- a/ai_oca_bridge/views/ai_bridge.xml +++ b/ai_oca_bridge/views/ai_bridge.xml @@ -37,13 +37,14 @@

+

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.

+

On the external system, you will receive a POST payload with the +following information:

+
    +
  • _id
  • +
  • _model
  • +
  • Fields selected
  • +
+

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

+
+
+

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.
  • +
  • Add an option to know when the AI task is asynchronous and comunicate +to the user once it has been finished.
  • +
+
+
+

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.

+
+
+