From 5d6da34b3d981a5103af344e3e335cc6c1d38c3b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 24 Sep 2026 22:33:59 +0200 Subject: [PATCH 1/3] [ADD] edi_webservice_core_oca: add webservice.endpoint support If the exchange type has a webservice_endpoint_id set, the send handler uses it directly; otherwise configuration comes from the exchange type's advanced settings, under execution_model.send.webservice, as before. send_as_bytes is now a proper field (webservice_send_as_bytes), visible only when the webservice handler is selected (webservice_send_enabled). --- edi_webservice_core_oca/__manifest__.py | 6 +- edi_webservice_core_oca/models/__init__.py | 1 + .../models/edi_exchange_type.py | 42 ++++++++++++ .../models/edi_webservice_send.py | 29 ++++----- edi_webservice_core_oca/readme/CONFIGURE.md | 43 ++++++------- edi_webservice_core_oca/tests/common.py | 25 ++++++++ edi_webservice_core_oca/tests/test_send.py | 64 ++++++------------- .../views/edi_exchange_type.xml | 32 ++++++++++ 8 files changed, 156 insertions(+), 86 deletions(-) create mode 100644 edi_webservice_core_oca/models/edi_exchange_type.py create mode 100644 edi_webservice_core_oca/views/edi_exchange_type.xml diff --git a/edi_webservice_core_oca/__manifest__.py b/edi_webservice_core_oca/__manifest__.py index 1bfdd34cd..b1c70d4b6 100644 --- a/edi_webservice_core_oca/__manifest__.py +++ b/edi_webservice_core_oca/__manifest__.py @@ -14,7 +14,11 @@ "maintainers": ["etobella", "simahawk"], "website": "https://github.com/OCA/edi-framework", "depends": ["edi_core_oca", "webservice_core"], - "data": ["views/edi_backend.xml", "security/ir.model.access.csv"], + "data": [ + "views/edi_backend.xml", + "views/edi_exchange_type.xml", + "security/ir.model.access.csv", + ], "demo": ["demo/edi_backend.xml"], "pre_init_hook": "pre_init_hook", } diff --git a/edi_webservice_core_oca/models/__init__.py b/edi_webservice_core_oca/models/__init__.py index f26266f4d..0b30b605b 100644 --- a/edi_webservice_core_oca/models/__init__.py +++ b/edi_webservice_core_oca/models/__init__.py @@ -1,2 +1,3 @@ from . import edi_backend +from . import edi_exchange_type from . import edi_webservice_send diff --git a/edi_webservice_core_oca/models/edi_exchange_type.py b/edi_webservice_core_oca/models/edi_exchange_type.py new file mode 100644 index 000000000..9efaaa9fa --- /dev/null +++ b/edi_webservice_core_oca/models/edi_exchange_type.py @@ -0,0 +1,42 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class EDIExchangeType(models.Model): + _inherit = "edi.exchange.type" + + webservice_backend_id = fields.Many2one( + related="backend_id.webservice_backend_id", + ) + webservice_send_enabled = fields.Boolean( + compute="_compute_webservice_send_enabled", + help="Technical flag: whether `send_model_id` inherits from the " + "webservice send handler (`edi.webservice.send`), directly or via " + "a custom extension. Used to show/hide webservice-specific " + "settings on this type.", + ) + webservice_endpoint_id = fields.Many2one( + "webservice.endpoint", + string="WebService Endpoint", + help="Webservice endpoint to call when sending through this " + "exchange type (see `edi.webservice.send`). Extra call " + "parameters can still be provided via the advanced YAML settings.", + ) + webservice_send_as_bytes = fields.Boolean( + string="Send As Bytes", + help="By sending as bytes, `requests` won't try to guess and/or " + "alter the encoding of the file being sent.", + ) + + @api.depends("send_model_id") + def _compute_webservice_send_enabled(self): + registry = self.env.registry + handler_cls = registry["edi.webservice.send"] + for rec in self: + model_name = rec.send_model_id.model + rec.webservice_send_enabled = bool( + model_name + and model_name in registry + and issubclass(registry[model_name], handler_cls) + ) diff --git a/edi_webservice_core_oca/models/edi_webservice_send.py b/edi_webservice_core_oca/models/edi_webservice_send.py index 69be8600f..64aaccbd3 100644 --- a/edi_webservice_core_oca/models/edi_webservice_send.py +++ b/edi_webservice_core_oca/models/edi_webservice_send.py @@ -4,14 +4,17 @@ from requests import Response -from odoo import exceptions, models +from odoo import models class EDIWebserviceSend(models.AbstractModel): """Generic handler for sending EDI exchange files through a webservice. - Configuration is expected to come from the exchange type's advanced + The exchange type's ``webservice_endpoint_id`` is used to perform the + call - its own HTTP method applies. Extra call parameters (headers, + query params, ...) can be provided via the exchange type's advanced settings, under ``execution_model.send.webservice``. + """ _name = "edi.webservice.send" @@ -19,11 +22,10 @@ class EDIWebserviceSend(models.AbstractModel): _description = "EDI WebService Send Handler" def send(self, exchange_record): + endpoint = exchange_record.type_id.webservice_endpoint_id ws_settings = self._get_ws_settings(exchange_record) - method, pargs, kwargs = self._get_call_params(exchange_record, ws_settings) - response = exchange_record.backend_id.webservice_backend_id.call( - method, *pargs, **kwargs - ) + kwargs = self._get_call_params(exchange_record, ws_settings) + response = endpoint.call(**kwargs) if not isinstance(response, Response): # backward compat for obsolete `content_only` param return response @@ -34,18 +36,11 @@ def _get_ws_settings(self, exchange_record): return settings.get("execution_model", {}).get("send", {}).get("webservice", {}) def _get_call_params(self, exchange_record, ws_settings): - try: - method = ws_settings["method"].lower() - except KeyError as err: - raise exceptions.UserError( - self.env._("`method` is required in `webservice` type settings.") - ) from err - pargs = ws_settings.get("pargs", []) kwargs = ws_settings.get("kwargs", {}) - kwargs["data"] = self._get_data(exchange_record, ws_settings) - return method, pargs, kwargs + kwargs["data"] = self._get_data(exchange_record) + return kwargs - def _get_data(self, exchange_record, ws_settings): + def _get_data(self, exchange_record): # By sending as bytes `requests` won't try to guess and/or alter the encoding. - as_bytes = ws_settings.get("send_as_bytes") + as_bytes = exchange_record.type_id.webservice_send_as_bytes return exchange_record._get_file_content(as_bytes=as_bytes) diff --git a/edi_webservice_core_oca/readme/CONFIGURE.md b/edi_webservice_core_oca/readme/CONFIGURE.md index 7cc549a14..b93c059b3 100644 --- a/edi_webservice_core_oca/readme/CONFIGURE.md +++ b/edi_webservice_core_oca/readme/CONFIGURE.md @@ -1,39 +1,34 @@ Go to "EDI -\> Config -\> Backends" and edit or create one. Find the tab "Webservice" and add a webservice. On the webservice record you can specify all the general parameters to connect to the service (see -`webservice_core` README for more details). +`webservice_core` README for more details). On the webservice backend +you can configure one or more `webservice.endpoint` records (path, HTTP +method, ...). On the exchange type you want to send through a webservice, go to the -"Execution handlers" tab and set "Sender" to "EDI WebService Send -Handler". +"Execution handlers" tab, set "Sender" to "EDI WebService Send Handler", +then pick the "Endpoint" to call in the "Webservice settings" group that +appears - it drives both the URL (backend URL + endpoint path) and the +HTTP method. -By default the call uses the backend's own configuration (URL, auth, -...). If you want to take full control on if/how the webservice is used -you can do it via the exchange type's advanced settings. +Static headers and querystring params are best set directly on the +`webservice.endpoint` record itself (its "Headers" and "URL Params" +tabs) - prefer that over the exchange type's advanced settings below, +which are meant for one-off, per-exchange-type overrides rather than +the endpoint's own regular configuration. -Hence, assuming your webservice has a URL configured as -\`\`: +If you still need extra call parameters beyond what the endpoint (and +its own headers/URL params) provide, you can add them via the exchange +type's advanced settings: execution_model: send: webservice: - method: post # mandatory kwargs: + headers: + X-Custom-Header: foo url_params: - path: endpoint1/foo - -For each call related to this type, you'll get a POST request against -https://my.endpoint/endpoint/foo. - -`url_params` can contain all the keys needed for URL interpolation. - -In addition, you can use `url` to override the full url used for the -call per exchange type. + foo: baz If you want to send data as bytes you can use the option send_as_bytes -like: - - [...] - webservice: - send_as_bytes: true - [...] +on the exchange type (same tab). diff --git a/edi_webservice_core_oca/tests/common.py b/edi_webservice_core_oca/tests/common.py index 6dd42527e..fc47e6546 100644 --- a/edi_webservice_core_oca/tests/common.py +++ b/edi_webservice_core_oca/tests/common.py @@ -23,4 +23,29 @@ def _setup_records(cls): cls.record.type_id.send_model_id = cls.env.ref( "edi_webservice_core_oca.model_edi_webservice_send" ) + # The demo `webservice.backend` has a `{endpoint}` placeholder in its + # URL, meant for the old `url_params`-based YAML config - not for + # `webservice.endpoint`'s own path-based combining. Use a plain one. + cls.ws_backend = cls.env["webservice.backend"].create( + { + "name": "Test WS Backend", + "tech_name": "test_edi_webservice_send_backend", + "protocol": "http", + "url": "https://foo.test/", + "content_type": "application/xml", + "auth_type": "none", + } + ) + cls.backend.webservice_backend_id = cls.ws_backend + cls.endpoint = cls.env["webservice.endpoint"].create( + { + "name": "Test endpoint", + "tech_name": "test_edi_webservice_send", + "description": "Test endpoint", + "backend_id": cls.ws_backend.id, + "http_method": "post", + "path": "push/here", + } + ) + cls.record.type_id.webservice_endpoint_id = cls.endpoint return result diff --git a/edi_webservice_core_oca/tests/test_send.py b/edi_webservice_core_oca/tests/test_send.py index b6bc78ecb..bb9bcd7bf 100644 --- a/edi_webservice_core_oca/tests/test_send.py +++ b/edi_webservice_core_oca/tests/test_send.py @@ -5,8 +5,6 @@ import responses from requests import PreparedRequest, Session -from odoo import exceptions - from .common import TestEDIWebserviceCoreBase @@ -16,22 +14,6 @@ def setUpClass(cls): cls._super_send = Session.send super().setUpClass() - @classmethod - def _setup_records(cls): - result = super()._setup_records() - cls.ws_backend = cls.backend.webservice_backend_id - cls.settings = """ - execution_model: - send: - webservice: - method: post - kwargs: - url_params: - endpoint: push/here - """ - cls.record.type_id.set_settings(cls.settings) - return result - @classmethod def _request_handler(cls, s: Session, r: PreparedRequest, /, **kw): # Allow request to test custom URL @@ -45,17 +27,23 @@ def _get_handler(self): def test_call_params(self): handler = self._get_handler() ws_settings = handler._get_ws_settings(self.record) - method, pargs, kwargs = handler._get_call_params(self.record, ws_settings) - self.assertEqual(method, "post") - self.assertEqual(pargs, []) + kwargs = handler._get_call_params(self.record, ws_settings) self.assertEqual(kwargs["data"], "This is a simple file") - self.assertEqual(kwargs["url_params"], {"endpoint": "push/here"}) - def test_no_method(self): + def test_call_params_extra_kwargs_from_settings(self): + settings = """ + execution_model: + send: + webservice: + kwargs: + headers: + X-Demo: demo-value + """ + self.record.type_id.set_settings(settings) handler = self._get_handler() - msg = "`method` is required in `webservice` type settings" - with self.assertRaisesRegex(exceptions.UserError, msg): - handler._get_call_params(self.record, {}) + ws_settings = handler._get_ws_settings(self.record) + kwargs = handler._get_call_params(self.record, ws_settings) + self.assertEqual(kwargs["headers"], {"X-Demo": "demo-value"}) @responses.activate def test_send(self): @@ -69,30 +57,18 @@ def test_send(self): self.assertEqual(responses.calls[0].request.body, "This is a simple file") def test_send_as_bytes(self): - settings = """ - execution_model: - send: - webservice: - method: post - send_as_bytes: true - kwargs: - url_params: - endpoint: push/here - """ - self.record.type_id.set_settings(settings) + self.record.type_id.webservice_send_as_bytes = True handler = self._get_handler() ws_settings = handler._get_ws_settings(self.record) - method, pargs, kwargs = handler._get_call_params(self.record, ws_settings) + kwargs = handler._get_call_params(self.record, ws_settings) self.assertEqual(kwargs["data"], b"This is a simple file") @responses.activate def test_exchange_send_dispatch(self): - # `send_model_id` on the exchange type is enough for the regular - # EDI framework dispatch (`backend.exchange_send`) to reach our - # handler - not just direct calls to it. - self.record.type_id.send_model_id = self.env.ref( - "edi_webservice_core_oca.model_edi_webservice_send" - ) + # `send_model_id` + `webservice_endpoint_id` on the exchange type + # are enough for the regular EDI framework dispatch + # (`backend.exchange_send`) to reach our handler - not just direct + # calls to it. self.record.edi_exchange_state = "output_pending" url = "https://foo.test/push/here" responses.add(responses.POST, url, body="{}") diff --git a/edi_webservice_core_oca/views/edi_exchange_type.xml b/edi_webservice_core_oca/views/edi_exchange_type.xml new file mode 100644 index 000000000..a1428d637 --- /dev/null +++ b/edi_webservice_core_oca/views/edi_exchange_type.xml @@ -0,0 +1,32 @@ + + + + + edi.exchange.type.form (webservice, in edi_webservice_core_oca) + edi.exchange.type + + + + + + + + + + + + + From 258d0978263aa610bb56a8869063989b18e4942a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 6 Oct 2026 16:29:19 +0200 Subject: [PATCH 2/3] [IMP] edi_webservice_core_oca: add hooks helper to migrate old exc types --- edi_webservice_core_oca/hooks.py | 266 +++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) diff --git a/edi_webservice_core_oca/hooks.py b/edi_webservice_core_oca/hooks.py index 3dfda8404..62c708cee 100644 --- a/edi_webservice_core_oca/hooks.py +++ b/edi_webservice_core_oca/hooks.py @@ -1,7 +1,14 @@ # Copyright 2026 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +import re +from urllib.parse import parse_qsl, urlsplit, urlunsplit + +import yaml from openupgradelib import openupgrade +_logger = logging.getLogger(__name__) + # xids to pass to the new module MOVED_XMLIDS = [ "access_webservice_backend_edi_manager", @@ -18,3 +25,262 @@ def pre_init_hook(env): for xmlid in MOVED_XMLIDS ], ) + + +def migrate_component_based_exchange_types(env, types=None, handler_model=None): + """Convert exchange types off the `component`-based webservice send. + + Switches them onto the new, component-free `edi.webservice.send` + handler. This is an explicit, opt-in tool - NOT run automatically on + install or upgrade. Staying on `edi_webservice_component_oca` is a + fully supported choice; this is only for exchange types you actively + want to move onto the handler/endpoint-based style. Call it by hand, + e.g. from an Odoo shell, against the types you want migrated:: + + from odoo.addons.edi_webservice_core_oca.hooks import ( + migrate_component_based_exchange_types, + ) + migrated = migrate_component_based_exchange_types(env) + + Pass an explicit `types` recordset to restrict the run (e.g. to test on + a handful of records first); left out, every `edi.exchange.type` whose + `send_model_id` is still the generic `component` handler + (`edi_component_oca.model_edi_oca_component_handler`) and whose backend + has a `webservice_backend_id` is migrated. + + See `migrate_exchange_type` for what happens to each type. Placeholders + moved to an endpoint path are removed from the backend URL here, once + all types are processed. + + """ + component_model = env.ref( + "edi_component_oca.model_edi_oca_component_handler", raise_if_not_found=False + ) + if not component_model: + # `edi_component_oca` was never installed: nothing relied on it. + return env["edi.exchange.type"].browse() + if types is None: + types = env["edi.exchange.type"].search( + [ + ("send_model_id", "=", component_model.id), + ("backend_id.webservice_backend_id", "!=", False), + ] + ) + handler_model = handler_model or env.ref( + "edi_webservice_core_oca.model_edi_webservice_send" + ) + moved_keys_by_backend = {} + for etype in types: + backend = etype.backend_id.webservice_backend_id + moved_keys = migrate_exchange_type(env, etype) + moved_keys_by_backend.setdefault(backend, set()).update(moved_keys or ()) + # Strip only once every type was classified, so that a placeholder + # removed for one type is still recognised for the others on that backend. + for backend, keys in moved_keys_by_backend.items(): + if not backend: + continue + backend.url = _strip_url_placeholders(backend.url, keys) + _move_url_query_to_params(backend) + types.write({"send_model_id": handler_model.id}) + return types + + +# HELPERS FOR CUSTOM TYPES MIGRATION +# +# Help you convert old style exchange types to no component setup +# and webservice.endpoint usage. +# +# DISCLAIMER: do your own tests! + + +def migrate_exchange_type(env, etype): + """Migrate a single exchange type's `component`-based `webservice` settings. + + Updates in place, without touching `send_model_id` (see + `migrate_component_based_exchange_types`, which also does that). + + Reads the old `components.send.work_ctx.webservice` YAML and applies: + + - `method` is removed - it now lives on the `webservice.endpoint` + itself, found or created (named after the type's own `code`, unless + a `webservice_endpoint_id` is already set) and assigned to + `webservice_endpoint_id`. + - `send_as_bytes` is removed and moved to the + `webservice_send_as_bytes` field on the type. + - `pargs`/`kwargs` are kept (still usable as call-time overrides), just + moved from `components.send.work_ctx.webservice` to the new + `execution_model.send.webservice` key. + - `kwargs.url_params` entries are checked against the backend's own URL: + a key that fills a `{placeholder}` in it becomes the endpoint's + `path` and is dropped from the YAML; anything else stays in + `url_params` for manual review. + + NOTE: rewriting the YAML re-serializes it, so any comments in + `advanced_settings_edit` are lost for migrated types. + + Returns the set of `url_params` keys moved to an endpoint path. + + """ + settings = yaml.safe_load(etype.advanced_settings_edit or "") or {} + components = settings.get("components", {}) + send_conf = components.get("send", {}) + work_ctx = send_conf.get("work_ctx", {}) + ws_settings = work_ctx.pop("webservice", None) + if ws_settings is None: + # Nothing to move: the type relied on the backend-level auto + # selection without any explicit `webservice` YAML settings. + return set() + if not work_ctx: + send_conf.pop("work_ctx", None) + if not send_conf: + components.pop("send", None) + if not components: + settings.pop("components", None) + + method = ws_settings.pop("method", None) + send_as_bytes = ws_settings.pop("send_as_bytes", None) + if send_as_bytes is not None: + etype.webservice_send_as_bytes = bool(send_as_bytes) + + endpoint = etype.webservice_endpoint_id + if not endpoint: + if not method: + _logger.warning( + "edi.exchange.type(%s): no `method` found in `webservice` " + "settings, cannot create a webservice.endpoint - please " + "set `webservice_endpoint_id` manually.", + etype.id, + ) + else: + endpoint = _get_or_create_endpoint(env, etype, method) + etype.webservice_endpoint_id = endpoint.id + + moved_keys = set() + if endpoint: + moved_keys = _move_matching_url_params(endpoint, ws_settings) + if ws_settings: + settings.setdefault("execution_model", {}).setdefault("send", {})[ + "webservice" + ] = ws_settings + + etype.advanced_settings_edit = yaml.safe_dump(settings, sort_keys=False) + return moved_keys + + +def _get_or_create_endpoint(env, etype, method): + ws_backend = etype.backend_id.webservice_backend_id + tech_name = etype.code + endpoint = env["webservice.endpoint"].search( + [("backend_id", "=", ws_backend.id), ("tech_name", "=", tech_name)], limit=1 + ) + if endpoint: + return endpoint + endpoint = env["webservice.endpoint"].create( + { + "name": etype.name, + "tech_name": tech_name, + "description": ( + f"Auto-migrated from exchange type '{etype.code}' " "advanced settings." + ), + "backend_id": ws_backend.id, + "http_method": method.lower(), + } + ) + _logger.info( + "edi.exchange.type(%s): created webservice.endpoint(%s) from " + "advanced settings.", + etype.id, + endpoint.id, + ) + return endpoint + + +def _move_matching_url_params(endpoint, ws_settings): + """Move `{placeholder}` url_params into the endpoint's path. + + A key that fills a `{placeholder}` in the backend's URL becomes the + endpoint's `path`, and is dropped from the YAML. Anything else is kept + in the YAML `url_params` for manual review (no querystring param is + created from it). + + Returns the set of keys moved to the path. + + """ + kwargs = ws_settings.get("kwargs", {}) + url_params = kwargs.get("url_params") + if not url_params: + return set() + backend_url = endpoint.backend_id.url or "" + remaining = {} + moved = set() + for key, value in url_params.items(): + if f"{{{key}}}" not in backend_url: + _logger.warning( + "webservice.endpoint(%s): url_param %r is not a placeholder in " + "the backend URL, kept in YAML for manual review.", + endpoint.id, + key, + ) + remaining[key] = value + continue + if endpoint.path and endpoint.path != value: + _logger.warning( + "webservice.endpoint(%s): path already set to %r, url_param " + "%r=%r kept in YAML for manual review.", + endpoint.id, + endpoint.path, + key, + value, + ) + remaining[key] = value + continue + endpoint.path = value + stale = endpoint.querystring_param_ids.filtered(lambda p, k=key: p.name == k) + if stale: + _logger.info( + "webservice.endpoint(%s): removing querystring param %r, " + "now carried by the endpoint path.", + endpoint.id, + key, + ) + stale.unlink() + moved.add(key) + if remaining: + kwargs["url_params"] = remaining + else: + kwargs.pop("url_params", None) + if not kwargs: + ws_settings.pop("kwargs", None) + return moved + + +def _strip_url_placeholders(url, keys): + for key in keys: + url = re.sub(r"/?\{" + re.escape(key) + r"\}", "", url or "") + return url + + +def _move_url_query_to_params(backend): + parts = urlsplit(backend.url or "") + if not parts.query: + return + existing = backend.querystring_param_ids.mapped("name") + pairs = [ + (name, value) + for name, value in parse_qsl(parts.query, keep_blank_values=True) + if name not in existing + ] + backend.env["webservice.querystring.param"].create( + [ + { + "res_model": backend._name, + "res_id": backend.id, + "name": name, + "value": value, + } + for name, value in pairs + ] + ) + backend.url = urlunsplit( + (parts.scheme, parts.netloc, parts.path, "", parts.fragment) + ) From 977dc415c90e92913a282944f1a42e66fc4de9e0 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 16 Oct 2026 16:57:43 +0200 Subject: [PATCH 3/3] [IMP] edi_webservice_core_oca: add exchange types smart button on endpoint --- edi_webservice_core_oca/__manifest__.py | 1 + edi_webservice_core_oca/models/__init__.py | 1 + .../models/webservice_endpoint.py | 32 ++++++++++++++++++ edi_webservice_core_oca/readme/CONFIGURE.md | 2 ++ edi_webservice_core_oca/tests/__init__.py | 1 + .../tests/test_webservice_endpoint.py | 20 +++++++++++ .../views/webservice_endpoint.xml | 33 +++++++++++++++++++ 7 files changed, 90 insertions(+) create mode 100644 edi_webservice_core_oca/models/webservice_endpoint.py create mode 100644 edi_webservice_core_oca/tests/test_webservice_endpoint.py create mode 100644 edi_webservice_core_oca/views/webservice_endpoint.xml diff --git a/edi_webservice_core_oca/__manifest__.py b/edi_webservice_core_oca/__manifest__.py index b1c70d4b6..45bd4d915 100644 --- a/edi_webservice_core_oca/__manifest__.py +++ b/edi_webservice_core_oca/__manifest__.py @@ -17,6 +17,7 @@ "data": [ "views/edi_backend.xml", "views/edi_exchange_type.xml", + "views/webservice_endpoint.xml", "security/ir.model.access.csv", ], "demo": ["demo/edi_backend.xml"], diff --git a/edi_webservice_core_oca/models/__init__.py b/edi_webservice_core_oca/models/__init__.py index 0b30b605b..83df8b14e 100644 --- a/edi_webservice_core_oca/models/__init__.py +++ b/edi_webservice_core_oca/models/__init__.py @@ -1,3 +1,4 @@ from . import edi_backend from . import edi_exchange_type from . import edi_webservice_send +from . import webservice_endpoint diff --git a/edi_webservice_core_oca/models/webservice_endpoint.py b/edi_webservice_core_oca/models/webservice_endpoint.py new file mode 100644 index 000000000..0ddffd629 --- /dev/null +++ b/edi_webservice_core_oca/models/webservice_endpoint.py @@ -0,0 +1,32 @@ +# Copyright 2026 Camptocamp SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class WebserviceEndpoint(models.Model): + _inherit = "webservice.endpoint" + + exchange_type_ids = fields.One2many( + comodel_name="edi.exchange.type", + inverse_name="webservice_endpoint_id", + string="Exchange Types", + ) + exchange_type_count = fields.Integer( + compute="_compute_exchange_type_count", + ) + + @api.depends("exchange_type_ids") + def _compute_exchange_type_count(self): + for rec in self: + rec.exchange_type_count = len(rec.exchange_type_ids) + + def action_view_exchange_types(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": self.env._("Exchange Types"), + "res_model": "edi.exchange.type", + "view_mode": "list,form", + "domain": [("webservice_endpoint_id", "=", self.id)], + } diff --git a/edi_webservice_core_oca/readme/CONFIGURE.md b/edi_webservice_core_oca/readme/CONFIGURE.md index b93c059b3..489218052 100644 --- a/edi_webservice_core_oca/readme/CONFIGURE.md +++ b/edi_webservice_core_oca/readme/CONFIGURE.md @@ -30,5 +30,7 @@ type's advanced settings: url_params: foo: baz +On a webservice endpoint, the *Exchange Types* smart button lists the exchange types sending through it. + If you want to send data as bytes you can use the option send_as_bytes on the exchange type (same tab). diff --git a/edi_webservice_core_oca/tests/__init__.py b/edi_webservice_core_oca/tests/__init__.py index 825af6718..460abeb78 100644 --- a/edi_webservice_core_oca/tests/__init__.py +++ b/edi_webservice_core_oca/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_backend from . import test_send +from . import test_webservice_endpoint diff --git a/edi_webservice_core_oca/tests/test_webservice_endpoint.py b/edi_webservice_core_oca/tests/test_webservice_endpoint.py new file mode 100644 index 000000000..e27299b77 --- /dev/null +++ b/edi_webservice_core_oca/tests/test_webservice_endpoint.py @@ -0,0 +1,20 @@ +# Copyright 2026 Camptocamp SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from .common import TestEDIWebserviceCoreBase + + +class TestWebserviceEndpoint(TestEDIWebserviceCoreBase): + def test_exchange_type_count(self): + """Count reflects exchange types linked to the endpoint.""" + self.assertEqual(self.endpoint.exchange_type_count, 1) + self.record.type_id.webservice_endpoint_id = False + self.assertEqual(self.endpoint.exchange_type_count, 0) + + def test_action_view_exchange_types(self): + """Action opens exchange types filtered on this endpoint.""" + action = self.endpoint.action_view_exchange_types() + self.assertEqual(action["res_model"], "edi.exchange.type") + self.assertEqual( + action["domain"], [("webservice_endpoint_id", "=", self.endpoint.id)] + ) diff --git a/edi_webservice_core_oca/views/webservice_endpoint.xml b/edi_webservice_core_oca/views/webservice_endpoint.xml new file mode 100644 index 000000000..1e957f658 --- /dev/null +++ b/edi_webservice_core_oca/views/webservice_endpoint.xml @@ -0,0 +1,33 @@ + + + + + webservice.endpoint.form (in edi_webservice_core_oca) + webservice.endpoint + + + +
+ +
+
+
+
+