Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
2 changes: 2 additions & 0 deletions base_external_system_http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models
25 changes: 25 additions & 0 deletions base_external_system_http/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "External System HTTP",
"version": "16.0.1.0.0",
"author": "Therp BV, Odoo Community Association (OCA)",
"maintainers": ["NL66278", "ntsirintanis"],
"license": "AGPL-3",
"category": "Base",
"website": "https://github.com/OCA/server-backend",
"summary": "HTTP Connector for External Systems",
"depends": ["base_external_system"],
"data": [
"security/ir.model.access.csv",
"demo/external_system_demo.xml",
"demo/external_system_endpoint_demo.xml",
"views/external_system.xml",
],
"demo": [
"demo/external_system_demo.xml",
"demo/external_system_endpoint_demo.xml",
],
"installable": True,
"application": False,
}
16 changes: 16 additions & 0 deletions base_external_system_http/demo/external_system_demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2026 Therp BV.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>

<record id="external_system_github" model="external.system">
<field name="name">Example Connection to GitHub for Testing</field>
<field name="system_type">external.system.adapter.http</field>
<field name="host">github.com</field>
<field name="remote_path">/OCA</field>
<field name="company_ids" eval="[(5, 0), (4, ref('base.main_company'))]" />
</record>

</odoo>
14 changes: 14 additions & 0 deletions base_external_system_http/demo/external_system_endpoint_demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2026 Therp BV.
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>

<record id="external_system_github_server_tools" model="external.system.endpoint">
<field name="system_id" ref="external_system_github" />
<field name="name">tools</field>
<field name="endpoint">/server-tools</field>
</record>

</odoo>
4 changes: 4 additions & 0 deletions base_external_system_http/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import external_system_endpoint # Must be before external_system!
from . import external_system
from . import external_system_adapter_http
17 changes: 17 additions & 0 deletions base_external_system_http/models/external_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

from odoo import fields, models


class ExternalSystem(models.Model):
"""Extend base external system"""

_inherit = "external.system"

endpoint_ids = fields.One2many(
comodel_name="external.system.endpoint",
inverse_name="system_id",
string="Endpoints",
help="Endpoints on remote system",
)
157 changes: 157 additions & 0 deletions base_external_system_http/models/external_system_adapter_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
"""HTTP(S) adapter for :class:`external.system`."""

import logging

import requests
from requests import exceptions as req_exc

from odoo import _, models
from odoo.exceptions import UserError, ValidationError

_logger = logging.getLogger(__name__)


class ExternalSystemAdapterHTTP(models.Model):
"""HTTP external system adapter."""

_name = "external.system.adapter.http"
_inherit = "external.system.adapter"
_description = "External System HTTP"

def external_get_client(self):
self.ensure_one()
return self

def external_destroy_client(self, client):
self.ensure_one()
return super().external_destroy_client(client)

def external_test_connection(self):
"""Test connection in the UI by doing a GET on the base URL."""
self.ensure_one()
try:
self.get(endpoint=None)
except ValidationError:
raise
except Exception as exc:
raise ValidationError(_("Connection failed.\n\nDETAIL: %s") % exc) from exc
return super().external_test_connection()

def get(self, endpoint=None, params=None, timeout=16, **kwargs):
"""GET helper."""
self.ensure_one()
url = self._get_url(endpoint=endpoint)
_logger.debug("Will GET %s", url)
try:
response = requests.get(url, params=params, timeout=timeout, **kwargs)
except req_exc.RequestException as exc:
_logger.error("GET %s failed: %s", url, exc)
raise ValidationError(
_("GET request failed for %(url)s.\n\nDETAIL: %(detail)s")
% {"url": url, "detail": exc}
) from exc
return self._return_checked_response(endpoint, response)

def post(self, endpoint=None, data=None, json=None, timeout=16, **kwargs):
"""POST helper."""
self.ensure_one()
url = self._get_url(endpoint=endpoint)
_logger.debug("Will POST %s", url)
try:
response = requests.post(
url, data=data, json=json, timeout=timeout, **kwargs
)
except req_exc.RequestException as exc:
_logger.error("POST %s failed: %s", url, exc)
raise ValidationError(
_("POST request failed for %(url)s.\n\nDETAIL: %(detail)s")
% {"url": url, "detail": exc}
) from exc
return self._return_checked_response(endpoint, response)

def put(self, endpoint=None, data=None, json=None, timeout=16, **kwargs):
"""PUT helper."""
self.ensure_one()
url = self._get_url(endpoint=endpoint)
_logger.debug("Will PUT %s", url)
try:
response = requests.put(
url, data=data, json=json, timeout=timeout, **kwargs
)
except req_exc.RequestException as exc:
_logger.error("PUT %s failed: %s", url, exc)
raise ValidationError(
_("PUT request failed for %(url)s.\n\nDETAIL: %(detail)s")
% {"url": url, "detail": exc}
) from exc
return self._return_checked_response(endpoint, response)

def delete(self, endpoint=None, params=None, timeout=16, **kwargs):
"""DELETE helper."""
self.ensure_one()
url = self._get_url(endpoint=endpoint)
_logger.debug("Will DELETE %s", url)
try:
response = requests.delete(url, params=params, timeout=timeout, **kwargs)
except req_exc.RequestException as exc:
_logger.error("DELETE %s failed: %s", url, exc)
raise ValidationError(
_("DELETE request failed for %(url)s.\n\nDETAIL: %(detail)s")
% {"url": url, "detail": exc}
) from exc
return self._return_checked_response(endpoint, response)

def _get_url(self, endpoint=None, url_suffix=None):
"""Build full URL for an endpoint"""
self.ensure_one()
system = self.system_id
endpoint_record = None
if endpoint:
endpoint_record = self.env["external.system.endpoint"].search(
[("system_id", "=", system.id), ("name", "=", endpoint)],
limit=1,
)
if not endpoint_record:
raise UserError(
_("Endpoint %(endpoint)s not found on system %(system_name)s")
% {"endpoint": endpoint, "system_name": system.name}
)
host = (system.host or "").strip()
if host.startswith(("http://", "https://")):
base = host.rstrip("/")
else:
base = ("https://" + host).rstrip("/")
port = ":" + str(system.port) if system.port else ""
remote_path = (system.remote_path or "").rstrip("/")
endpoint_path = endpoint_record.endpoint if endpoint_record else ""
suffix = url_suffix or ""
return f"{base}{port}{remote_path}{endpoint_path}{suffix}"

def _return_checked_response(self, endpoint, response):
"""Validate response."""
if response.status_code >= 400:
text = response.text or ""
_logger.error(
"Got response with statuscode %(status)s from endpoint %(endpoint)s: %(text)s",
{
"status": str(response.status_code),
"endpoint": endpoint or "<base>",
"text": text,
},
)
raise ValidationError(
_(
"Communication failure with %(endpoint)s "
"(HTTP %(status)s).\n\nDETAIL: %(detail)s"
)
% {
"endpoint": endpoint or "<base>",
"status": response.status_code,
"detail": (text or "").strip(),
}
)

_logger.info("Succesfull communication with endpoint %s", endpoint or "<base>")
return response
19 changes: 19 additions & 0 deletions base_external_system_http/models/external_system_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
"""Endpoint on remote system to get from or post to."""

from odoo import fields, models


class ExternalSystemEndpoint(models.Model):
"""Endpoint on remote system to get from or post to."""

_name = "external.system.endpoint"
_description = __doc__

system_id = fields.Many2one(comodel_name="external.system", required=True)
name = fields.Char(string="Logical name for endpoint", required=True)
endpoint = fields.Char(
required=True,
help="Remote path to append to base url for endpoint",
)
2 changes: 2 additions & 0 deletions base_external_system_http/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Ronald Portier (Therp BV)
* Nikos Tsirintanis <ntsirintanis@therp.nl>
8 changes: 8 additions & 0 deletions base_external_system_http/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
This module provides an HTTP(S) adapter for the ``base_external_system`` framework.

It allows Odoo to communicate with external systems over HTTP using a consistent
adapter interface, supporting GET, POST and PUT requests, endpoint resolution,
and connection testing.

The adapter itself acts as the client and is yielded via the standard
``system.client()`` context manager.
2 changes: 2 additions & 0 deletions base_external_system_http/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Optional authentication helpers
* Optional persistent session support
24 changes: 24 additions & 0 deletions base_external_system_http/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Create an external system record and optionally define endpoints.

Example configuration:

* Host: ``api.example.com``
* Remote path: ``/v1``

Define endpoints such as:

* ``status`` → ``/status``
* ``items`` → ``/items``

Use the adapter:

.. code-block:: python

with system.client() as client:
response = client.get(endpoint="status")
response = client.post(endpoint="items", json={"name": "Item"})
response = client.put(endpoint="items", json={"active": True})

Responses are returned as ``requests.Response`` objects.

Errors raise ``ValidationError`` with detailed messages.
4 changes: 4 additions & 0 deletions base_external_system_http/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
base_external_system_http.access_external_system_adapter_http_user,access_external_system_adapter_http_user,base_external_system_http.model_external_system_adapter_http,base.group_user,1,0,0,0
base_external_system_http.access_external_system_endpoint_user,access_external_system_endpoint_user,base_external_system_http.model_external_system_endpoint,base.group_user,1,0,0,0
base_external_system_http.access_external_system_endpoint_admin,access_external_system_endpoint_admin,base_external_system_http.model_external_system_endpoint,base.group_system,1,1,1,1
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading